Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@
- Implement `xsd:anyUri` codec
- Implement `xsd:decimal` codec
- Implement `xsd:double` codec
- Implement `xsd:float` codec
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
1 change: 1 addition & 0 deletions lib/src/codecs/codecs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
94 changes: 94 additions & 0 deletions lib/src/codecs/float/float_codec.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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<double, String> {
const XsdFloatCodec();

@override
Converter<double, String> get encoder => const XsdFloatEncoder();

@override
Converter<String, double> get decoder => const XsdFloatDecoder();
}

class XsdFloatEncoder extends Converter<double, String> {
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 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 FormatException(
'Value $input underflows 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<String, double> {
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).',
);
}
}
Comment thread
mark-dropbear marked this conversation as resolved.

return value;
}
}
104 changes: 104 additions & 0 deletions test/codecs/float_codec_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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<FormatException>().having(
(e) => e.message,
'message',
contains('overflows the 32-bit float range'),
),
),
);
});
Comment thread
mark-dropbear marked this conversation as resolved.

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<FormatException>().having(
(e) => e.message,
'message',
contains('underflows 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);
Comment thread
mark-dropbear marked this conversation as resolved.
expect(() => codec.decode('inf'), throwsFormatException);
expect(() => codec.decode('-inf'), 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<FormatException>().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<FormatException>().having(
(e) => e.message,
'message',
contains('outside the value range of float (underflow)'),
),
),
);
},
);
});
});
}