fix(cutoffs): preserve exact integral scores in strip search

Avoid shortest round-trip decimal text for already-integral doubles. The failing tie regression and the independent 9,163-case exact-integer sweep now keep the fewer-pierce result.
This commit is contained in:
aj committed 2026-10-13 20:12:44 -04:00
1 parent 57450ecea8
commit c107ab13f7
2 files changed
+23 -10

No files matched your search

+9 -10
View File
@@ -104,18 +104,17 @@ internal static class AutomaticCutOffStripSearch
if (!double.IsFinite(length) || length < 0)
throw new ArgumentException("Cut-off segment length must be finite and nonnegative.", nameof(length));
var rounded = System.Math.Round(length, 6);
// A shortest round-trip decimal may spell a different integer near 1e16
// (for example 26833294515438512d prints as 26833294515438510).
// An already-integral double is exact; never round it through text.
if (rounded == System.Math.Truncate(rounded))
return new BigInteger(rounded) * 1_000_000;
// Compare sums of the specified decimal-rounded model units, not binary
// approximations such as 0.1 + 0.2 versus 0.3. The round-trip string
// retains micro-unit digits even at larger (but still fractional) scales.
if (rounded < 1e22)
{
var value = decimal.Parse(rounded.ToString("R", CultureInfo.InvariantCulture),
NumberStyles.Float, CultureInfo.InvariantCulture);
return new BigInteger(decimal.Round(value * 1_000_000m, 0));
}
// At this magnitude a double has no fractional model units; avoid a
// decimal overflow while retaining an unbounded exact accumulated score.
return new BigInteger(rounded) * 1_000_000;
// retains micro-unit digits for fractional scores.
var value = decimal.Parse(rounded.ToString("R", CultureInfo.InvariantCulture),
NumberStyles.Float, CultureInfo.InvariantCulture);
return new BigInteger(decimal.Round(value * 1_000_000m, 0));
}
private static int Compare(State left, State right)
@@ -22,6 +22,20 @@ public class AutomaticCutOffStripSearchTests
Assert.Equal(new[] { 20.0, 40.0 }, result);
}
[Fact]
public void IntegralDoubleTie_UsesExactValueBeforePierces()
{
var first = 10576245042702564d;
var second = 16257049472735948d;
var combined = 26833294515438512d;
Assert.Equal((decimal)first + (decimal)second, (decimal)combined);
var result = AutomaticCutOffStripSearch.Solve(0,
new[] { new[] { 20.0, 35.0 }, new[] { 40.0, 70.0 } },
new[] { 35.0, 70.0 }, 35, 17.5,
x => x switch { 20 => (first, 1), 40 => (second, 1), 35 => (combined, 3), _ => (0.0, 0) });
Assert.Equal(new[] { 20.0, 40.0 }, result);
}
[Fact]
public void GlobalOptimum_IsBetterThanGreedyFirstLine()
{