diff --git a/Extensions.Test/StringExtensionsTests.cs b/Extensions.Test/StringExtensionsTests.cs index fbc7678..bc10561 100644 --- a/Extensions.Test/StringExtensionsTests.cs +++ b/Extensions.Test/StringExtensionsTests.cs @@ -470,6 +470,32 @@ public void NominalWordWrapThrowsArgumentOutOfRangeExceptionWhenGlyphWidthNotPos Assert.ThrowsExactly(() => input.NominalWordWrap(100f, 0f).ToList()); } + [TestMethod] + public void NominalWordWrapThrowsArgumentOutOfRangeExceptionWhenWrapWidthIsNaN() + { + string input = "hello world foo"; + ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => input.NominalWordWrap(float.NaN, 1f).ToList()); + Assert.AreEqual("wrapWidth", exception.ParamName); + } + + [TestMethod] + public void NominalWordWrapThrowsArgumentOutOfRangeExceptionWhenGlyphWidthIsNaN() + { + string input = "hello world foo"; + ArgumentOutOfRangeException exception = Assert.ThrowsExactly(() => input.NominalWordWrap(10f, float.NaN).ToList()); + Assert.AreEqual("nominalGlyphWidth", exception.ParamName); + } + + [TestMethod] + [DataRow(float.PositiveInfinity, 1f)] + [DataRow(float.MaxValue, float.Epsilon)] + public void NominalWordWrapKeepsEachLineWholeWhenWidthRatioIsUnbounded(float wrapWidth, float nominalGlyphWidth) + { + string input = "hello world foo\nbar baz"; + List result = [.. input.NominalWordWrap(wrapWidth, nominalGlyphWidth)]; + Assert.AreSequenceEqual(new List { "hello world foo", "bar baz" }, result); + } + [TestMethod] public void NominalWordWrapBreaksAfterVisibleHyphen() { diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs index f1ea5b2..87212d6 100644 --- a/Extensions/StringExtensions.cs +++ b/Extensions/StringExtensions.cs @@ -235,12 +235,13 @@ public static IEnumerable NominalWordWrap(this string text, float wrapWi { Ensure.NotNull(text); - if (wrapWidth <= 0) + // NaN fails every comparison, so it has to be rejected explicitly. + if (float.IsNaN(wrapWidth) || wrapWidth <= 0) { throw new ArgumentOutOfRangeException(nameof(wrapWidth), wrapWidth, "Wrap width must be greater than zero."); } - if (nominalGlyphWidth <= 0) + if (float.IsNaN(nominalGlyphWidth) || nominalGlyphWidth <= 0) { throw new ArgumentOutOfRangeException(nameof(nominalGlyphWidth), nominalGlyphWidth, "Nominal glyph width must be greater than zero."); } @@ -250,7 +251,10 @@ public static IEnumerable NominalWordWrap(this string text, float wrapWi return []; } - int maxCharsPerLine = Math.Max(1, (int)Math.Floor(wrapWidth / nominalGlyphWidth)); + // Clamp before casting: float-to-int conversion does not saturate before .NET 9, so an infinite or huge ratio + // would otherwise become int.MinValue and wrap one character per line. + double ratio = Math.Floor((double)wrapWidth / nominalGlyphWidth); + int maxCharsPerLine = ratio >= int.MaxValue ? int.MaxValue : Math.Max(1, (int)ratio); return NominalWordWrapIterator(text, maxCharsPerLine); }