Skip to content

Commit

Permalink
Implement fixed point handling (#1543)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
clangenb authored Nov 2, 2023
1 parent 1aec156 commit 49af7e9
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 0 deletions.
30 changes: 30 additions & 0 deletions packages/ew_substrate_fixed/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
3 changes: 3 additions & 0 deletions packages/ew_substrate_fixed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Substrate Fixed

Implements substrate fixed support for some unsigned fixed point types.
6 changes: 6 additions & 0 deletions packages/ew_substrate_fixed/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions packages/ew_substrate_fixed/lib/src/exception.dart
Original file line number Diff line number Diff line change
@@ -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';
}
}
38 changes: 38 additions & 0 deletions packages/ew_substrate_fixed/lib/src/fixed_point.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
32 changes: 32 additions & 0 deletions packages/ew_substrate_fixed/lib/src/fixed_point_util.dart
Original file line number Diff line number Diff line change
@@ -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<T extends BitCount> {
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;
}
37 changes: 37 additions & 0 deletions packages/ew_substrate_fixed/lib/src/parse_fixed_point.dart
Original file line number Diff line number Diff line change
@@ -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();
}
41 changes: 41 additions & 0 deletions packages/ew_substrate_fixed/lib/src/to_fixed_point.dart
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 4 additions & 0 deletions packages/ew_substrate_fixed/lib/substrate_fixed.dart
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 11 additions & 0 deletions packages/ew_substrate_fixed/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions packages/ew_substrate_fixed/test/parse_fixed_point_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
50 changes: 50 additions & 0 deletions packages/ew_substrate_fixed/test/to_fixed_point_test.dart
Original file line number Diff line number Diff line change
@@ -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<FixedPointException>()),
);
});
});
}

0 comments on commit 49af7e9

Please sign in to comment.