From 86a2378d9fdf10cddd9ee45b2fbc3c93c60486de Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 27 Nov 2025 08:02:15 +0000 Subject: [PATCH 1/3] feat: implement `xsd:float` codec --- CHANGELOG.md | 1 + README.md | 2 +- lib/src/codecs/codecs.dart | 1 + lib/src/codecs/float/float_codec.dart | 87 ++++++++++++++++++++++++++ test/codecs/float_codec_test.dart | 88 +++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 lib/src/codecs/float/float_codec.dart create mode 100644 test/codecs/float_codec_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 582eb4a..172e8b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,3 +30,4 @@ - Implement `xsd:anyUri` codec - Implement `xsd:decimal` codec - Implement `xsd:double` codec +- Implement `xsd:float` codec diff --git a/README.md b/README.md index b17335a..3880167 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This library aims to support the following XSD 1.1 built-in datatypes that are c | `xsd:NCName` | ✅ | ✅ | `String` | `dart:core` | | `xsd:decimal` | ✅ | ✅ | `Decimal` | `package:decimal` | | `xsd:double` | ✅ | ✅ | `double` | `dart:core` | -| `xsd:float` | ✅ | ❌ | `double` | `dart:core` | +| `xsd:float` | ✅ | ✅ | `double` | `dart:core` | | `xsd:integer` | ✅ | ❌ | `BigInt` | `dart:core` | | `xsd:nonPositiveInteger` | ✅ | ✅ | `BigInt` | `dart:core` | | `xsd:negativeInteger` | ✅ | ❌ | `BigInt` | `dart:core` | diff --git a/lib/src/codecs/codecs.dart b/lib/src/codecs/codecs.dart index a59210e..6193d6d 100644 --- a/lib/src/codecs/codecs.dart +++ b/lib/src/codecs/codecs.dart @@ -27,3 +27,4 @@ export 'datetime/xsd_datetime_codec.dart'; export 'any_uri/any_uri_codec.dart'; export 'decimal/decimal_codec.dart'; export 'double/double_codec.dart'; +export 'float/float_codec.dart'; diff --git a/lib/src/codecs/float/float_codec.dart b/lib/src/codecs/float/float_codec.dart new file mode 100644 index 0000000..32df32e --- /dev/null +++ b/lib/src/codecs/float/float_codec.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:xsd/src/helpers/whitespace.dart'; + +/// A [Codec] that converts between XSD `float` strings and [double] values. +/// +/// Note: Dart uses 64-bit doubles for floating point numbers. This codec +/// uses [double] to represent XSD `float` values, which are 32-bit. It will +/// throw an error if a value cannot be represented as a 32-bit float. +class XsdFloatCodec extends Codec { + const XsdFloatCodec(); + + @override + Converter get encoder => const XsdFloatEncoder(); + + @override + Converter get decoder => const XsdFloatDecoder(); +} + +class XsdFloatEncoder extends Converter { + const XsdFloatEncoder(); + + @override + String convert(double input) { + // Validate that the double can be represented as a 32-bit float. + if (input.isFinite) { + final f32 = Float32List(1); + f32[0] = input; + // An overflow results in INFINITY, which changes the meaning of the value. + if (f32[0].isInfinite) { + throw ArgumentError( + 'Value $input overflows the 32-bit float range and cannot be encoded.', + ); + } + } + + if (input.isNaN) return 'NaN'; + if (input == double.infinity) return 'INF'; + if (input == double.negativeInfinity) return '-INF'; + // XSD canonical form uses 'E'. + return input.toString().toUpperCase(); + } +} + +class XsdFloatDecoder extends Converter { + const XsdFloatDecoder(); + + @override + double convert(String input) { + final trimmed = processWhiteSpace(input, Whitespace.collapse); + if (trimmed == 'INF') return double.infinity; + if (trimmed == '-INF') return double.negativeInfinity; + if (trimmed == 'NaN') return double.nan; + + final value = double.tryParse(trimmed); + + if (value == null) { + throw FormatException('Invalid XSD float value: "$input"'); + } + + // Dart's double is 64-bit. We need to validate that the value + // fits within the 32-bit float value space as defined by XSD. + if (value.isFinite) { + // Use Float32List to check for overflow. + final f32 = Float32List(1); + f32[0] = value; + final floatValue = f32[0]; + + // If the conversion results in infinity, but the original was not, it's an overflow. + if (floatValue.isInfinite) { + throw FormatException( + 'The literal "$input" is outside the value range of float (overflow).', + ); + } + + // If the conversion results in 0, but the original was not, it's an underflow. + if (value != 0.0 && floatValue == 0.0) { + throw FormatException( + 'The literal "$input" is outside the value range of float (underflow).', + ); + } + } + + return value; + } +} diff --git a/test/codecs/float_codec_test.dart b/test/codecs/float_codec_test.dart new file mode 100644 index 0000000..4d06891 --- /dev/null +++ b/test/codecs/float_codec_test.dart @@ -0,0 +1,88 @@ +import 'package:test/test.dart'; +import 'package:xsd/xsd.dart'; + +void main() { + group('XsdFloatCodec', () { + const codec = XsdFloatCodec(); + + group('encoder', () { + test('should encode valid float values', () { + expect(codec.encode(123.45), '123.45'); + expect(codec.encode(-123.45), '-123.45'); + expect(codec.encode(0.0), '0.0'); + expect(codec.encode(-0.0), '-0.0'); + expect(codec.encode(1.23e4), '12300.0'); + expect(codec.encode(double.infinity), 'INF'); + expect(codec.encode(double.negativeInfinity), '-INF'); + expect(codec.encode(double.nan), 'NaN'); + }); + + test('should throw on values that overflow the float range', () { + // double.maxFinite is much larger than what a 32-bit float can hold. + expect( + () => codec.encode(double.maxFinite), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('overflows the 32-bit float range'), + ), + ), + ); + }); + }); + + group('decoder', () { + test('should decode valid float strings', () { + expect(codec.decode('123.45'), 123.45); + expect(codec.decode(' -123.45 '), -123.45); + expect(codec.decode('INF'), double.infinity); + expect(codec.decode(' -INF '), double.negativeInfinity); + expect(codec.decode('NaN').isNaN, isTrue); + expect(codec.decode('1.2E3'), 1200.0); + expect(codec.decode('0'), 0.0); + expect(codec.decode('-0'), -0.0); + }); + + test('should throw FormatException for invalid string formats', () { + expect(() => codec.decode('abc'), throwsFormatException); + expect(() => codec.decode('1.2.3'), throwsFormatException); + expect(() => codec.decode('INFinity'), throwsFormatException); + }); + + test( + 'should throw FormatException on values that overflow the float range', + () { + // A value larger than the max float value (~3.4e38) + expect( + () => codec.decode('3.5e38'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('outside the value range of float (overflow)'), + ), + ), + ); + }, + ); + + test( + 'should throw FormatException on values that underflow the float range', + () { + // A value smaller than the min float value (~1.4e-45) + expect( + () => codec.decode('1.0e-46'), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('outside the value range of float (underflow)'), + ), + ), + ); + }, + ); + }); + }); +} From ecbd789ee0b81de69a86c4516e2c542eb15c2bde Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 27 Nov 2025 10:26:13 +0000 Subject: [PATCH 2/3] Address code review feedback --- lib/src/codecs/float/float_codec.dart | 7 +++++++ test/codecs/float_codec_test.dart | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/src/codecs/float/float_codec.dart b/lib/src/codecs/float/float_codec.dart index 32df32e..b69e717 100644 --- a/lib/src/codecs/float/float_codec.dart +++ b/lib/src/codecs/float/float_codec.dart @@ -33,6 +33,13 @@ class XsdFloatEncoder extends Converter { 'Value $input overflows the 32-bit float range and cannot be encoded.', ); } + + // An underflow to 0 also changes the meaning of the value. + if (input != 0.0 && f32[0] == 0.0) { + throw ArgumentError( + 'Value $input underflows the 32-bit float range and cannot be encoded.', + ); + } } if (input.isNaN) return 'NaN'; diff --git a/test/codecs/float_codec_test.dart b/test/codecs/float_codec_test.dart index 4d06891..f312d08 100644 --- a/test/codecs/float_codec_test.dart +++ b/test/codecs/float_codec_test.dart @@ -30,6 +30,20 @@ void main() { ), ); }); + + test('should throw on values that underflow the float range', () { + // A value that underflows to 0 in 32-bit float. + expect( + () => codec.encode(1.0e-46), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('underflows the 32-bit float range'), + ), + ), + ); + }); }); group('decoder', () { @@ -48,6 +62,8 @@ void main() { expect(() => codec.decode('abc'), throwsFormatException); expect(() => codec.decode('1.2.3'), throwsFormatException); expect(() => codec.decode('INFinity'), throwsFormatException); + expect(() => codec.decode('inf'), throwsFormatException); + expect(() => codec.decode('-inf'), throwsFormatException); }); test( From 651b20cb9b58d690b195bc6d2cdb1b48fb416193 Mon Sep 17 00:00:00 2001 From: mark Date: Thu, 27 Nov 2025 12:58:29 +0000 Subject: [PATCH 3/3] Ensure we throw an Exception not an Error in the XSDFloatCodec --- lib/src/codecs/float/float_codec.dart | 4 ++-- test/codecs/float_codec_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/codecs/float/float_codec.dart b/lib/src/codecs/float/float_codec.dart index b69e717..3b1a8d1 100644 --- a/lib/src/codecs/float/float_codec.dart +++ b/lib/src/codecs/float/float_codec.dart @@ -29,14 +29,14 @@ class XsdFloatEncoder extends Converter { f32[0] = input; // An overflow results in INFINITY, which changes the meaning of the value. if (f32[0].isInfinite) { - throw ArgumentError( + throw FormatException( 'Value $input overflows the 32-bit float range and cannot be encoded.', ); } // An underflow to 0 also changes the meaning of the value. if (input != 0.0 && f32[0] == 0.0) { - throw ArgumentError( + throw FormatException( 'Value $input underflows the 32-bit float range and cannot be encoded.', ); } diff --git a/test/codecs/float_codec_test.dart b/test/codecs/float_codec_test.dart index f312d08..d74fa7b 100644 --- a/test/codecs/float_codec_test.dart +++ b/test/codecs/float_codec_test.dart @@ -22,7 +22,7 @@ void main() { expect( () => codec.encode(double.maxFinite), throwsA( - isA().having( + isA().having( (e) => e.message, 'message', contains('overflows the 32-bit float range'), @@ -36,7 +36,7 @@ void main() { expect( () => codec.encode(1.0e-46), throwsA( - isA().having( + isA().having( (e) => e.message, 'message', contains('underflows the 32-bit float range'),