A correctness release. Five defects let the library return wrong answers silently — most
importantly, the range guard that is the entire point of the type could be bypassed by writing
roman + 1. Fixing them required breaking changes, hence the major version.
Nothing about the numeral system itself changed: the range is still 1–3999, canonical output is
identical, and every valid canonical string parses to the same value as before.
Upgrading from 2.x
| 2.x | 3.0.0 |
|---|---|
int x = roman; |
int x = (int)roman; or roman.ToInt() |
new Roman(3999) + 1 → 4000 |
throws OverflowException |
roman == 42 → int comparison, disagreed with roman.Equals(42) |
Roman/int operator; the two now agree |
Parse("IIX", RomanStyle.Lenient) → 10 |
→ 8 |
Parse(s, (RomanStyle)7) → parsed leniently |
throws ArgumentOutOfRangeException |
new Roman((string)null) → ArgumentException |
ArgumentNullException |
Existing catch (ArgumentException) handlers keep working — ArgumentNullException derives from it.
The compiler will point you at every site that needs the (int) cast.
Breaking changes
Roman→intis now an explicit conversion. An implicit one madeRoman's own operators
inapplicable in any mixed expression: becauseint→Romanis explicit, they were not
candidates, so overload resolution fell back to the predefinedintoperators. `new Roman(3999)- 1
evaluated to4000with no exception, andnew Roman(10) + 5produced theint15rather thanXV. Use(int)romanorroman.ToInt()`.
- 1
- Mixed
Roman/intarithmetic is range-checked.+ - * /now validate the result against
1–3999 and throwOverflowException, exactly as the all-Romanoperators always did. Operands
widen tolongfirst, so a largeintoperand cannot overflow the intermediate value. - Lenient parsing of multi-symbol subtractive runs is corrected. A symbol is subtractive when
it is smaller than the largest symbol anywhere to its right, not merely smaller than its
immediate neighbour."IIX"was10and is now8;"XIIX"was20, now18;"IIC"was
100, now98. Strict parsing is unaffected — it rejected these forms before and still does. - An undefined
RomanStyleis rejected. Any value outside the declared members — from a cast
or an uninitialized field — silently selected lenient parsing, skipping the canonical check
entirely. It now throwsArgumentOutOfRangeException.TryParsethrows as well rather than
returningfalse, matchingint.TryParsewith an invalidNumberStyles.default(RomanStyle)
isStrictand still parses strictly. - A null string throws
ArgumentNullException.string.IsNullOrWhiteSpacecollapsed null and
empty into one branch, so a missing argument was reported asArgumentException: Roman numeral cannot be empty.Empty and whitespace-only strings still throwArgumentException. AssemblyVersionnow tracks the package version. Every release up to and including 2.0.1
shipped an assembly stamped1.0.0.0, because onlyPackageVersionwas set and that feeds the
nuspec alone.typeof(Roman).Assembly.GetName().Versionnow returns3.0.0.ArgumentExceptionfor an invalid character now names its parameter.ParamNamewasnull;
it is now"roman", and the message gains the usual(Parameter 'roman')suffix.
Added
-
Operators
+ - * /,< > <= >=and== !=for(Roman, int)and(int, Roman), in both
operand orders. -
Equals(int)andCompareTo(int), plusIEquatable<int>andIComparable<int>, so
roman.Equals(42)agrees withroman == 42. -
Comparison against an
intnever constructs aRoman, so an unrepresentable bound is a fair
question:new Roman(3999) < 5000istrue. -
roman / 0reachesDivideByZeroException, which was unreachable while both operands were
Roman.operand orders.
-
Equals(int)andCompareTo(int), plusIEquatable<int>andIComparable<int>, so
roman.Equals(42)agrees withroman == 42. -
Comparison against an
intnever constructs aRoman, so an unrepresentable bound is a fair
question:new Roman(3999) < 5000istrue. -
roman / 0reachesDivideByZeroException, which was unreachable while both operands were
Roman.
Fixed
- The leading-minus check used a culture-sensitive
StartsWith(string). Under ICU, characters such
as U+200B and U+00AD are collation-ignorable, so"\u200B-X""started with"-and was reported
as a sign problem rather than an invalid character — and the answer changed with the consuming
application's globalization mode. Now ordinal (CA1310), and 11× cheaper on a check that runs once
per parse. - Dead links and a mismatched table-of-contents anchor in both READMEs.
README.mdships inside
the package, so these were visible on the nuget.org page.
Compatibility
.NET 9, no dependencies. 220 unit tests.