Skip to content

Commit 86550f2

Browse files
committed
feat: Enhance numeric comparison in HeapEquableValue and add unit tests for double vs int equality
1 parent 0d51961 commit 86550f2

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed

source/fluentasserts/core/conversion/floats.d

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ version (unittest) {
1111

1212
/// Parses a string as a double value.
1313
///
14-
/// A simple parser for numeric strings that handles integers and decimals.
15-
/// Does not support scientific notation.
14+
/// A simple parser for numeric strings that handles integers, decimals,
15+
/// and scientific notation (e.g., "1.0032e+06").
1616
///
1717
/// Params:
1818
/// s = The string to parse
@@ -52,6 +52,54 @@ double parseDouble(const(char)[] s, out bool success) @nogc nothrow pure @safe {
5252
}
5353
} else if (c == '.' && !seenDot) {
5454
seenDot = true;
55+
} else if ((c == 'e' || c == 'E') && seenDigit) {
56+
// Handle scientific notation
57+
i++;
58+
if (i >= s.length) {
59+
return 0.0;
60+
}
61+
62+
bool expNegative = false;
63+
if (s[i] == '-') {
64+
expNegative = true;
65+
i++;
66+
} else if (s[i] == '+') {
67+
i++;
68+
}
69+
70+
if (i >= s.length) {
71+
return 0.0;
72+
}
73+
74+
int exponent = 0;
75+
bool seenExpDigit = false;
76+
for (; i < s.length; i++) {
77+
char ec = s[i];
78+
if (ec >= '0' && ec <= '9') {
79+
seenExpDigit = true;
80+
exponent = exponent * 10 + (ec - '0');
81+
} else {
82+
return 0.0;
83+
}
84+
}
85+
86+
if (!seenExpDigit) {
87+
return 0.0;
88+
}
89+
90+
// Apply exponent
91+
double multiplier = 1.0;
92+
for (int j = 0; j < exponent; j++) {
93+
multiplier *= 10.0;
94+
}
95+
96+
if (expNegative) {
97+
result /= multiplier;
98+
} else {
99+
result *= multiplier;
100+
}
101+
102+
break;
55103
} else {
56104
return 0.0;
57105
}
@@ -198,3 +246,21 @@ unittest {
198246
expect(result.value).to.be.approximately(0.125, 0.001);
199247
}
200248

249+
@("parseDouble parses scientific notation 1.0032e+06")
250+
unittest {
251+
import std.math : abs;
252+
bool success;
253+
double val = parseDouble("1.0032e+06", success);
254+
assert(success, "parseDouble should succeed for scientific notation");
255+
// Use approximate comparison for floating point
256+
assert(abs(val - 1003200.0) < 0.01, "1.0032e+06 should parse to approximately 1003200.0");
257+
}
258+
259+
@("parseDouble parses integer 1003200")
260+
unittest {
261+
bool success;
262+
double val = parseDouble("1003200", success);
263+
assert(success, "parseDouble should succeed for integer");
264+
assert(val == 1003200.0, "1003200 should parse to 1003200.0");
265+
}
266+

source/fluentasserts/core/memory/heapequable.d

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,17 @@ struct HeapEquableValue {
8181
return false;
8282
}
8383

84-
// Otherwise fall back to string comparison
85-
return _serialized == other._serialized;
84+
// Try string comparison first
85+
if (_serialized == other._serialized) {
86+
return true;
87+
}
88+
89+
// For scalars, try numeric comparison (handles double vs int, scientific notation)
90+
if (_kind == Kind.scalar && other._kind == Kind.scalar) {
91+
return numericEquals(_serialized[], other._serialized[]);
92+
}
93+
94+
return false;
8695
}
8796

8897
bool isEqualTo(const HeapEquableValue other) nothrow const @trusted {
@@ -96,8 +105,52 @@ struct HeapEquableValue {
96105
return false;
97106
}
98107

99-
// Otherwise fall back to string comparison
100-
return _serialized == other._serialized;
108+
// Try string comparison first
109+
if (_serialized == other._serialized) {
110+
return true;
111+
}
112+
113+
// For scalars, try numeric comparison (handles double vs int, scientific notation)
114+
if (_kind == Kind.scalar && other._kind == Kind.scalar) {
115+
return numericEquals(_serialized[], other._serialized[]);
116+
}
117+
118+
return false;
119+
}
120+
121+
/// Compares two string representations as numbers if both are numeric.
122+
/// Uses relative epsilon comparison for floating point tolerance.
123+
private static bool numericEquals(const(char)[] a, const(char)[] b) @nogc nothrow pure @safe {
124+
bool aIsNum, bIsNum;
125+
double aVal = parseDouble(a, aIsNum);
126+
double bVal = parseDouble(b, bIsNum);
127+
128+
if (aIsNum && bIsNum) {
129+
return approxEqual(aVal, bVal);
130+
}
131+
132+
return false;
133+
}
134+
135+
/// Approximate equality check for floating point numbers.
136+
/// Uses relative epsilon for large numbers and absolute epsilon for small numbers.
137+
private static bool approxEqual(double a, double b) @nogc nothrow pure @safe {
138+
import core.stdc.math : fabs;
139+
140+
// Handle exact equality (including infinities)
141+
if (a == b) {
142+
return true;
143+
}
144+
145+
double diff = fabs(a - b);
146+
double larger = fabs(a) > fabs(b) ? fabs(a) : fabs(b);
147+
148+
// Use relative epsilon scaled to the magnitude of the numbers
149+
// For numbers around 1e6, epsilon of ~1e-9 relative gives ~1e-3 absolute tolerance
150+
enum double relEpsilon = 1e-9;
151+
enum double absEpsilon = 1e-9;
152+
153+
return diff <= larger * relEpsilon || diff <= absEpsilon;
101154
}
102155

103156
bool isLessThan(ref const HeapEquableValue other) @nogc nothrow const @trusted {
@@ -376,6 +429,18 @@ version (unittest) {
376429
assert(!v1.isEqualTo(v3));
377430
}
378431

432+
@("isEqualTo handles numeric comparison for double vs int")
433+
unittest {
434+
// 1003200.0 serialized as scientific notation vs integer
435+
auto doubleVal = HeapEquableValue.createScalar("1.0032e+06");
436+
auto intVal = HeapEquableValue.createScalar("1003200");
437+
438+
assert(doubleVal.kind() == HeapEquableValue.Kind.scalar);
439+
assert(intVal.kind() == HeapEquableValue.Kind.scalar);
440+
assert(doubleVal.isEqualTo(intVal), "1.0032e+06 should equal 1003200");
441+
assert(intVal.isEqualTo(doubleVal), "1003200 should equal 1.0032e+06");
442+
}
443+
379444
@("array type stores elements")
380445
unittest {
381446
auto arr = HeapEquableValue.createArray("[1, 2, 3]");

source/fluentasserts/operations/equality/equal.d

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,23 @@ unittest {
939939
evaluation.result.messageString.should.contain("should equal null.");
940940
}
941941

942+
@("double equals int with same value passes")
943+
unittest {
944+
// 1003200.0 serializes as "1.0032e+06" and 1003200 as "1003200"
945+
// Numeric comparison should still work
946+
(1003200.0).should.equal(1003200);
947+
(1003200).should.equal(1003200.0);
948+
}
949+
950+
@("double equals int with different value fails")
951+
unittest {
952+
auto evaluation = ({
953+
(1003200.0).should.equal(1003201);
954+
}).recordEvaluation;
955+
956+
evaluation.result.hasContent().should.equal(true);
957+
}
958+
942959
version (unittest):
943960
class EqualThing {
944961
int x;

0 commit comments

Comments
 (0)