Roman 3.0.0

dotnet add package Roman --version 3.0.0
                    
NuGet\Install-Package Roman -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Roman" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Roman" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Roman" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Roman --version 3.0.0
                    
#r "nuget: Roman, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Roman@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Roman&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Roman&version=3.0.0
                    
Install as a Cake Tool

Roman

NuGet Downloads NuGet Version .NET C# License CodeOfConduct

A small, dependency-free C# library for Roman numerals: conversion to and from Arabic integers, arithmetic, and comparison. Immutable, allocation-light, and fully unit-tested.

🌐 Language: English · Русский

Table of contents

Features

  • Convert Arabic numbers (1–3999) to Roman and back
  • Arithmetic: addition, subtraction, multiplication, division
  • Comparison operators: >, <, >=, <=, ==, !=
  • Implements IComparable<Roman> and IEquatable<Roman>
  • Immutable type — every operation returns a new value
  • Well-defined null semantics in comparison operators
  • Case-insensitive parsing with surrounding whitespace trimmed
  • Strict (default) and lenient (RomanStyle.Lenient) parsing modes
  • TryParse for exception-free parsing
  • Allocation-light conversion via stackalloc Span<char>
  • No external dependencies

Installation

# .NET CLI
dotnet add package Roman
# Package Manager
Install-Package Roman

<PackageReference Include="Roman" Version="3.0.0" />

Quick start

using RomanNumerals;

// From an integer
var a = new Roman(42);
Console.WriteLine(a);            // XLII

// From a string
var b = new Roman("XIX");
Console.WriteLine(b.ToInt());    // 19

// Arithmetic
Console.WriteLine(a + b);        // LXI  (61)

// Comparison
if (a > b)
    Console.WriteLine("42 > 19");

// Exception-free parsing
if (Roman.TryParse("MCMXCIV", out var year))
    Console.WriteLine(year.ToInt());   // 1994

Note: the type is Roman, the namespace is RomanNumerals. Add using RomanNumerals; and use new Roman(...).

Usage

Creating values

var fromInt    = new Roman(1984);        // MCMLXXXIV
var fromString = new Roman("MMXXIII");   // 2023
var lower      = new Roman("  xlii  ");  // 42 (trimmed, case-insensitive)
var copy       = new Roman(fromInt);     // copy constructor

Conversions

// Parse — throws on invalid input
var r1 = Roman.Parse(500);
var r2 = Roman.Parse("D");

// TryParse — never throws on bad input
if (Roman.TryParse(42, out var r3)) { /* ... */ }
if (Roman.TryParse("invalid", out var r4)) { /* not reached */ }

// Explicit Roman -> int (implicit would let mixed expressions bypass the range guard)
var value = (int)new Roman(42);   // 42
var same  = new Roman(42).ToInt(); // 42

// Explicit int -> Roman (can throw)
var r5 = (Roman)99;            // XCIX

Arithmetic

Operations compute on the underlying integer and re-validate the result against the 1–3999 range, throwing OverflowException when the result overflows (> 3999) or underflows (< 1). Division is integer division.

new Roman(10) + new Roman(5);   // XV  (15)
new Roman(50) - new Roman(20);  // XXX (30)
new Roman(7)  * new Roman(8);   // LVI (56)
new Roman(20) / new Roman(4);   // V   (5)
new Roman(10) / new Roman(3);   // III (3, integer division)

new Roman(3999) + new Roman(1); // throws: result > 3999
new Roman(5)    - new Roman(5); // throws: result < 1

An int operand works on either side and is range-checked identically — the operand is read as an Arabic quantity, so only the result has to be representable:

new Roman(10) + 5;              // XV  (15)
50 - new Roman(20);             // XXX (30)
new Roman(5)  + -3;             // II  (2, out-of-range operand, in-range result)
new Roman(3999) + 1;            // throws OverflowException
new Roman(10) / 0;              // throws DivideByZeroException

Comparison and equality against an int never construct a Roman, so an unrepresentable bound is a fair question:

new Roman(3999) < 5000;         // true
new Roman(42) == 42;            // true  (agrees with .Equals(42))

Comparison

var a = new Roman(50);
var b = new Roman(30);

a > b;            // true
a == b;           // false
a.CompareTo(b);   // > 0
a.Equals(b);      // false

null semantics mirror Comparer<T> (null sorts lowest):

Roman x = new Roman(5);
Roman y = null;

x > y;    // true   (a value is greater than null)
y < x;    // true   (null is less than any value)
x == y;   // false
x != y;   // true

Roman p = null, q = null;
p == q;   // true
p >= q;   // true

Parsing modes (strict / lenient)

By default parsing is strict — it accepts only the canonical form and rejects non-canonical input such as "IIII". This applies to the constructors, Parse(string) and TryParse(string, …):

new Roman("IV");    // OK -> 4
new Roman("IIII");  // FormatException: not canonical

For lenient parsing (accept non-canonical forms), pass RomanStyle.Lenient to the Parse / TryParse overloads — modeled after int.Parse(string, NumberStyles):

var roman = Roman.Parse("IIII", RomanStyle.Lenient);  // parses as 4
Console.WriteLine(roman);                              // IV (output is always canonical)

if (Roman.TryParse("IIII", RomanStyle.Lenient, out var r))
    Console.WriteLine(r);                              // IV

RomanStyle.Strict (the default) rejects garbage characters (ArgumentException), out-of-range values (ArgumentOutOfRangeException), and otherwise valid but non-canonical forms (FormatException). A null string is an ArgumentNullException; empty and whitespace-only strings are an ArgumentException. TryParse still returns false for all of those rather than throwing.

An undefined RomanStyle — anything produced by a cast, such as (RomanStyle)7 — is rejected with ArgumentOutOfRangeException rather than silently selecting a mode. TryParse throws in that case too, the way int.TryParse does for an invalid NumberStyles: a bad argument is a caller error, not a parse failure to recover from. default(RomanStyle) is Strict, so an uninitialized field parses strictly.

Lenient parsing subtracts every symbol that is smaller than the largest symbol to its right, not just one that precedes a larger neighbour. That gives the readings attested in Roman inscriptions for multi-symbol subtractive runs:

Roman.Parse("IIX",  RomanStyle.Lenient).ToInt();   // 8
Roman.Parse("XIIX", RomanStyle.Lenient).ToInt();   // 18
Roman.Parse("IIC",  RomanStyle.Lenient).ToInt();   // 98

If the subtractions take the result below 1, parsing throws ArgumentOutOfRangeException rather than returning a value: Roman.Parse("IIIIIIIIIIX", RomanStyle.Lenient) would be 0. Lenient mode is not a validator — it still accepts forms no convention allows, such as "IC" for 99.

Limitations

  • Range is 1–3999 (MMMCMXCIX). Values outside this range throw ArgumentOutOfRangeException. There is no representation for 0 or negatives.
  • Round-trip is value-preserving, not string-preserving. Roman.Parse("IIII", RomanStyle.Lenient).ToString() returns "IV". Strict parsing (the default) rejects non-canonical input outright.
  • Arithmetic results must stay within 1–3999, otherwise they throw.

API reference

Constructors

Constructor Description
Roman(int value) Creates a value from an integer (1–3999)
Roman(string roman) Creates a value from a string (strict)
Roman(Roman other) Copy constructor

Static methods

Method Description
Parse(int value) Parses an int; throws on error
Parse(string roman) Parses a string (strict); throws on error
Parse(string roman, RomanStyle style) Parses with a mode; Strict throws FormatException on non-canonical input
TryParse(int value, out Roman? result) Exception-free int parsing
TryParse(string roman, out Roman? result) Exception-free string parsing (strict)
TryParse(string roman, RomanStyle style, out Roman? result) Exception-free string parsing with a mode

Enums

RomanStyle Description
Strict Strict parsing (default): canonical form only
Lenient Lenient parsing: accepts non-canonical forms

Instance methods

Method Description
ToInt() Returns the Arabic value
ToString() Returns the canonical Roman string
CompareTo(Roman? other) Compares with another value
CompareTo(int other) Compares with an integer value
Equals(Roman? other) Value equality
Equals(int other) Value equality against an integer
GetHashCode() Hash code

Operators

Operator Description
+ - * / Arithmetic (integer division), Roman or int on either side
> < >= <= Comparison (null sorts lowest), Roman or int on either side
== != Equality, Roman or int on either side
(int)roman Explicit conversion Roman -> int
(Roman)value Explicit conversion int -> Roman

Performance

The conversion path is allocation-light:

  • The int -> Roman conversion writes into a stackalloc Span<char> buffer — no heap allocation beyond the resulting string.
  • The type is immutable and wraps a single int, so instances are cheap to copy and compare.

Testing

The library ships with a comprehensive MSTest suite covering constructors, arithmetic, comparison, parsing (both modes), conversions, and int → Roman → string round-trips.

dotnet test                                   # run all tests
dotnet test --collect:"XPlat Code Coverage"   # with coverage

Contributing

Issues and pull requests are welcome. When filing a bug, please include the library version, the .NET version, a minimal reproduction, and the expected vs. actual behavior.

License

Licensed under the MIT License.

Author

deliciousNesquik@deliciousNesquik

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 46 8/24/2026
2.0.1 74 8/17/2026
2.0.0 82 7/7/2026
1.1.1 88 6/17/2026
1.0.0 94 4/8/2026

Breaking changes.

Roman-to-int conversion is now explicit: use (int)roman or roman.ToInt(). Mixed Roman/int arithmetic is range-checked instead of falling back to the predefined int operators, so new Roman(3999) + 1 throws instead of yielding 4000. Lenient parsing of multi-symbol subtractive runs is corrected: "IIX" is 8, not 10. An undefined RomanStyle and a null string now throw instead of being reinterpreted. AssemblyVersion now matches the package version. Added: operators, Equals and CompareTo against int.

Ломающие изменения.

Приведение Roman к int стало явным: используйте (int)roman или roman.ToInt(). Смешанная арифметика Roman/int проверяет диапазон вместо отката к встроенным операторам int, поэтому new Roman(3999) + 1 бросает исключение, а не даёт 4000. Исправлен мягкий разбор многосимвольных вычитаний: "IIX" теперь 8, а не 10. Неопределённое значение RomanStyle и строка null теперь бросают исключение. AssemblyVersion соответствует версии пакета. Добавлены операторы, Equals и CompareTo против int.

https://github.com/deliciousNesquik/roman/releases/tag/v3.0.0