From 49af7e9546b1fff60dffebda772601cf94676c75 Mon Sep 17 00:00:00 2001 From: clangenb <37865735+clangenb@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:12:18 +0900 Subject: [PATCH] Implement fixed point handling (#1543) * working wip that parses fixed point number 1. * extract `ew_substrate_fixed` package * [ew_substrate_fixed] improve code quality. * [ew_substrate_fixed] add some tests, all passing * [ew_substrate_fixed] fix handling of negative numbers and add more tests * [ew_substrate_fixed] add some more documentation * [ew_substrate_fixed] rename source file and its test to `parse_fixed_point` * [ew_substrate_fixed] add `to_fixed_point` and tests * [ew_substrate_fixed] restructure fixed point library and implement fixed arithmetics for certain types. * [ew_substrate_fixed] add note that the function does not work for negative numbers yet. * [ew_substrate_fixed] update library description * fix backslash in pubspec overrides * fmt * extend doc comment * [ew_substrate_fixed] consistent naming: fraction -> fractional * [ew_substrate_fixed] fix test naming * [ew_substrate_fixed] remove unnecessary check * [ew_substrate_fixed] add exception * [ew_substrate_fixed] fix documentation * fmt --- packages/ew_substrate_fixed/.gitignore | 30 +++++++++++ packages/ew_substrate_fixed/README.md | 3 ++ .../ew_substrate_fixed/analysis_options.yaml | 6 +++ .../ew_substrate_fixed/lib/src/exception.dart | 14 +++++ .../lib/src/fixed_point.dart | 38 +++++++++++++ .../lib/src/fixed_point_util.dart | 32 +++++++++++ .../lib/src/parse_fixed_point.dart | 37 +++++++++++++ .../lib/src/to_fixed_point.dart | 41 ++++++++++++++ .../lib/substrate_fixed.dart | 4 ++ packages/ew_substrate_fixed/pubspec.yaml | 11 ++++ .../test/parse_fixed_point_test.dart | 54 +++++++++++++++++++ .../test/to_fixed_point_test.dart | 50 +++++++++++++++++ 12 files changed, 320 insertions(+) create mode 100644 packages/ew_substrate_fixed/.gitignore create mode 100644 packages/ew_substrate_fixed/README.md create mode 100644 packages/ew_substrate_fixed/analysis_options.yaml create mode 100644 packages/ew_substrate_fixed/lib/src/exception.dart create mode 100644 packages/ew_substrate_fixed/lib/src/fixed_point.dart create mode 100644 packages/ew_substrate_fixed/lib/src/fixed_point_util.dart create mode 100644 packages/ew_substrate_fixed/lib/src/parse_fixed_point.dart create mode 100644 packages/ew_substrate_fixed/lib/src/to_fixed_point.dart create mode 100644 packages/ew_substrate_fixed/lib/substrate_fixed.dart create mode 100644 packages/ew_substrate_fixed/pubspec.yaml create mode 100644 packages/ew_substrate_fixed/test/parse_fixed_point_test.dart create mode 100644 packages/ew_substrate_fixed/test/to_fixed_point_test.dart diff --git a/packages/ew_substrate_fixed/.gitignore b/packages/ew_substrate_fixed/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/packages/ew_substrate_fixed/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/ew_substrate_fixed/README.md b/packages/ew_substrate_fixed/README.md new file mode 100644 index 000000000..c8496007a --- /dev/null +++ b/packages/ew_substrate_fixed/README.md @@ -0,0 +1,3 @@ +# Substrate Fixed + +Implements substrate fixed support for some unsigned fixed point types. \ No newline at end of file diff --git a/packages/ew_substrate_fixed/analysis_options.yaml b/packages/ew_substrate_fixed/analysis_options.yaml new file mode 100644 index 000000000..4d0959530 --- /dev/null +++ b/packages/ew_substrate_fixed/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:very_good_analysis/analysis_options.3.1.0.yaml + +linter: + rules: + public_member_api_docs: false + lines_longer_than_80_chars: false diff --git a/packages/ew_substrate_fixed/lib/src/exception.dart b/packages/ew_substrate_fixed/lib/src/exception.dart new file mode 100644 index 000000000..b057b4f4b --- /dev/null +++ b/packages/ew_substrate_fixed/lib/src/exception.dart @@ -0,0 +1,14 @@ +const unsupportedNegativeValuesMsg = 'Negative values are not supported yet'; + +class FixedPointException implements Exception { + const FixedPointException(this.message); + + factory FixedPointException.unsupportedNegativeValues() => const FixedPointException(unsupportedNegativeValuesMsg); + + final String message; + + @override + String toString() { + return 'FixedPointException: $message'; + } +} diff --git a/packages/ew_substrate_fixed/lib/src/fixed_point.dart b/packages/ew_substrate_fixed/lib/src/fixed_point.dart new file mode 100644 index 000000000..572b27b92 --- /dev/null +++ b/packages/ew_substrate_fixed/lib/src/fixed_point.dart @@ -0,0 +1,38 @@ +import 'package:ew_substrate_fixed/src/fixed_point_util.dart' show u16F16Util, u32F32Util, u64F64Util; + +abstract class FixedPoint { + FixedPoint(this._bits); + + final BigInt _bits; + + BigInt get bits => _bits; + + double asDouble(); +} + +class U16F16 extends FixedPoint { + U16F16(super.bits); + + @override + double asDouble() { + return u16F16Util.toDouble(_bits); + } +} + +class U32F32 extends FixedPoint { + U32F32(super.bits); + + @override + double asDouble() { + return u32F32Util.toDouble(_bits); + } +} + +class U64F64 extends FixedPoint { + U64F64(super.bits); + + @override + double asDouble() { + return u64F64Util.toDouble(_bits); + } +} diff --git a/packages/ew_substrate_fixed/lib/src/fixed_point_util.dart b/packages/ew_substrate_fixed/lib/src/fixed_point_util.dart new file mode 100644 index 000000000..45a109881 --- /dev/null +++ b/packages/ew_substrate_fixed/lib/src/fixed_point_util.dart @@ -0,0 +1,32 @@ +import 'package:ew_substrate_fixed/src/parse_fixed_point.dart' as pf; +import 'package:ew_substrate_fixed/src/to_fixed_point.dart' as tf; + +const u16F16Util = FixedPointUtil(BitCount(16, 16)); +const u32F32Util = FixedPointUtil(BitCount(32, 32)); +const u64F64Util = FixedPointUtil(BitCount(64, 64)); + +/// Util class that allows to have consts knowing how to handle +/// fixed point numbers. +class FixedPointUtil { + const FixedPointUtil(this.bitCount); + + final T bitCount; + + BigInt toFixed(double value) { + return tf.toFixedPoint(value, integerBitCount: bitCount.integer, fractionalBitCount: bitCount.fractional); + } + + double toDouble(BigInt value) { + return pf.parseFixedPoint(value, integerBitCount: bitCount.integer, fractionalBitCount: bitCount.fractional); + } +} + +class BitCount { + const BitCount(this._integerBitCount, this._fractionalBitCount); + + final int _integerBitCount; + final int _fractionalBitCount; + + int get integer => _integerBitCount; + int get fractional => _fractionalBitCount; +} diff --git a/packages/ew_substrate_fixed/lib/src/parse_fixed_point.dart b/packages/ew_substrate_fixed/lib/src/parse_fixed_point.dart new file mode 100644 index 000000000..0f68f9301 --- /dev/null +++ b/packages/ew_substrate_fixed/lib/src/parse_fixed_point.dart @@ -0,0 +1,37 @@ +import 'dart:math' as math; + +/// Parses a BigInt representing a fixed point number with the given +/// number of integer and fractional bits. +double parseFixedPoint( + BigInt input, { + required int integerBitCount, + required int fractionalBitCount, +}) { + final len = integerBitCount + fractionalBitCount; + final signed = input.toSigned(len); + + // we have to get rid of the `-` within the string. We prepend it later again + // if necessary. + final bits = signed.toRadixString(2).replaceFirst('-', '').padLeft(len, '0'); + final fractionalBits = bits.substring(bits.length - fractionalBitCount); + var integerBits = bits.substring(0, bits.length - fractionalBitCount); + + if (signed.isNegative) { + integerBits = '-$integerBits'; + } + + // print('bits: $bits'); + // print('fractionalBits: $fractionalBits'); + // print('integerBits: $integerBits'); + + final fractionalPart = fractionalBits + .split('') + .asMap() + .entries + .map((entry) => entry.value == '1' ? 1 / math.pow(2, entry.key + 1) : 0) + .reduce((acc, val) => acc + val); + + final integerPart = integerBits.isNotEmpty ? int.parse(integerBits, radix: 2) : 0; + + return (integerPart + (signed.isNegative ? -fractionalPart : fractionalPart)).toDouble(); +} diff --git a/packages/ew_substrate_fixed/lib/src/to_fixed_point.dart b/packages/ew_substrate_fixed/lib/src/to_fixed_point.dart new file mode 100644 index 000000000..21be9f9ee --- /dev/null +++ b/packages/ew_substrate_fixed/lib/src/to_fixed_point.dart @@ -0,0 +1,41 @@ +import 'package:ew_substrate_fixed/src/exception.dart'; + +/// Returns a BigInt representing a positive fixed point number with the given +/// number of integer and fractional bits. +/// +/// Note: This doesn't work for negative numbers yet, but as the runtime uses +/// unsigned types, we don't really need it anyhow. +BigInt toFixedPoint( + double input, { + required int integerBitCount, + required int fractionalBitCount, +}) { + if (input.isNegative) throw FixedPointException.unsupportedNegativeValues(); + + final integerBits = input.toInt(); + final bits = integerBits.toRadixString(2) + getFractionalBits(input, fractionalBitCount); + return BigInt.parse(bits, radix: 2); +} + +/// Extract the fractional bits of a double number. +String getFractionalBits(double number, int bitCount) { + // Extract the fractional part of the double number + var fractionalPart = number - number.toInt(); + + // Convert the fractional part to its binary representation + var binaryFractionalPart = ''; + while (fractionalPart > 0) { + // Multiply the fractional part by 2 and add the bit to the binary representation + fractionalPart *= 2; + if (fractionalPart >= 1) { + binaryFractionalPart += '1'; + fractionalPart -= 1; + } else { + binaryFractionalPart += '0'; + } + } + + // Pads the `fractionalPart` if shorter than `bitCount`, and truncates it + // if longer than `bigCount`. + return binaryFractionalPart.padRight(bitCount, '0').substring(0, bitCount); +} diff --git a/packages/ew_substrate_fixed/lib/substrate_fixed.dart b/packages/ew_substrate_fixed/lib/substrate_fixed.dart new file mode 100644 index 000000000..acaecf4af --- /dev/null +++ b/packages/ew_substrate_fixed/lib/substrate_fixed.dart @@ -0,0 +1,4 @@ +library substrate_fixed; + +export 'src/fixed_point.dart' show U16F16, U32F32, U64F64; +export 'src/fixed_point_util.dart' show u16F16Util, u32F32Util, u64F64Util; diff --git a/packages/ew_substrate_fixed/pubspec.yaml b/packages/ew_substrate_fixed/pubspec.yaml new file mode 100644 index 000000000..ab85ebb9e --- /dev/null +++ b/packages/ew_substrate_fixed/pubspec.yaml @@ -0,0 +1,11 @@ +name: ew_substrate_fixed +description: Implements fixed point handling in dart +version: 0.0.1 +publish_to: none + +environment: + sdk: ">=3.0.3 <4.0.0" + +dev_dependencies: + test: ^1.24.3 + very_good_analysis: ^5.0.0+1 diff --git a/packages/ew_substrate_fixed/test/parse_fixed_point_test.dart b/packages/ew_substrate_fixed/test/parse_fixed_point_test.dart new file mode 100644 index 000000000..e26a832f8 --- /dev/null +++ b/packages/ew_substrate_fixed/test/parse_fixed_point_test.dart @@ -0,0 +1,54 @@ +import 'package:ew_substrate_fixed/src/parse_fixed_point.dart'; +import 'package:test/test.dart'; + +void main() { + group('parseFixedPoint', () { + test('parses 0 as U16F16', () { + final zero = BigInt.from(0x0); + final output = parseFixedPoint(zero, integerBitCount: 16, fractionalBitCount: 16); + expect(output, 0); + }); + + test('parses 0 as U64F64', () { + final zero = BigInt.parse('00000000000000000000000000000000', radix: 16); + final output = parseFixedPoint(zero, integerBitCount: 64, fractionalBitCount: 64); + expect(output, 0); + }); + + test('parses -0 as U64F64', () { + final zero = BigInt.parse('-0', radix: 16); + final output = parseFixedPoint(zero, integerBitCount: 64, fractionalBitCount: 64); + expect(output, 0); + }); + + test('parses 1 as U64F64', () { + final one = BigInt.parse('18446744073709551616'); + final output = parseFixedPoint(one, integerBitCount: 64, fractionalBitCount: 64); + expect(output, 1); + }); + + test('parses 1 as U16F16', () { + final one = BigInt.from(0x10000); + final output = parseFixedPoint(one, integerBitCount: 16, fractionalBitCount: 16); + expect(output, 1); + }); + + test('parses 18.5435...', () { + final input = BigInt.from(0x128b260000); + final output = parseFixedPoint(input, integerBitCount: 32, fractionalBitCount: 32); + expect(output, 18.543548583984375); + }); + + test('parses -18.1234', () { + final input = BigInt.parse('FFFFFFFFFFFFFFEDE068DB8BAC710000', radix: 16); + final output = parseFixedPoint(input, integerBitCount: 64, fractionalBitCount: 64); + expect(output, -18.1234); + }); + + test('parses 18.1234', () { + final input = BigInt.parse('00000000000000121F972474538F0000', radix: 16); + final output = parseFixedPoint(input, integerBitCount: 64, fractionalBitCount: 64); + expect(output, 18.1234); + }); + }); +} diff --git a/packages/ew_substrate_fixed/test/to_fixed_point_test.dart b/packages/ew_substrate_fixed/test/to_fixed_point_test.dart new file mode 100644 index 000000000..3d54adc80 --- /dev/null +++ b/packages/ew_substrate_fixed/test/to_fixed_point_test.dart @@ -0,0 +1,50 @@ +import 'package:ew_substrate_fixed/src/exception.dart'; +import 'package:ew_substrate_fixed/src/to_fixed_point.dart'; +import 'package:test/test.dart'; + +void main() { + group('toFixedPoint', () { + test('1.0 to I16F16 works', () { + const one = 1.0; + final output = toFixedPoint(one, integerBitCount: 16, fractionalBitCount: 16); + expect(output, BigInt.from(0x10000)); + }); + + test('0.0 to I16F16 works', () { + const zero = 0.0; + final output = toFixedPoint(zero, integerBitCount: 16, fractionalBitCount: 16); + expect(output, BigInt.from(0x0)); + }); + + test('1.1 to I16F16 works', () { + const input = 1.1; + final output = toFixedPoint(input, integerBitCount: 16, fractionalBitCount: 16); + expect(output, BigInt.from(0x011999)); + }); + + test('18.4062... to I64F64 works', () { + const one = 18.4062194824218714473; + final output = toFixedPoint(one, integerBitCount: 64, fractionalBitCount: 64); + expect(output, BigInt.parse('1267fdffffffff0000', radix: 16)); + }); + + test('18.1234 to I64F64 works', () { + const input = 18.1234; + final output = toFixedPoint(input, integerBitCount: 64, fractionalBitCount: 64); + expect(output, BigInt.parse('00000000000000121F972474538F0000', radix: 16)); + }); + + test('returns 0 for small number', () { + const input = 0.000000000000000000000000000000000000001; + final output = toFixedPoint(input, integerBitCount: 64, fractionalBitCount: 64); + expect(output, BigInt.from(0)); + }); + + test('throws exception for negative values', () { + expect( + () => toFixedPoint(-1, integerBitCount: 64, fractionalBitCount: 64), + throwsA(isA()), + ); + }); + }); +}