Line data Source code
1 : // Copyright 2026 The Authors
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'package:characters/characters.dart';
16 :
17 : const romanDigits = <String, int>{
18 : 'i': 1,
19 : 'v': 5,
20 : 'x': 10,
21 : 'l': 50,
22 : 'c': 100,
23 : 'd': 500,
24 : 'm': 1000,
25 : 'I': 1,
26 : 'V': 5,
27 : 'X': 10,
28 : 'L': 50,
29 : 'C': 100,
30 : 'D': 500,
31 : 'M': 1000,
32 : };
33 :
34 : /// Represents a basic Roman numeral-based number
35 : ///
36 : /// This is a simplistic rendition of handling Roman numerals.
37 : ///
38 : /// The apostrophus and the vinculum are not supported. Fractions are not
39 : /// supported.
40 : ///
41 : /// See also: [Wikipedia: Roman numerals](https://en.wikipedia.org/wiki/Roman_numerals)
42 : class RomanNumerals {
43 : final String value;
44 :
45 1 : RomanNumerals._(this.value);
46 :
47 : /// Creates a new instance
48 : ///
49 : /// Only a basic check of [input] is performed - namely that it is
50 : /// not empty and contains only valid Roman numerals (as defined in
51 : /// [romanDigits]).
52 1 : static RomanNumerals? tryParse(String input) {
53 : // Check for empty string
54 1 : if (input.isEmpty) {
55 : return null;
56 : }
57 :
58 2 : for (final c in input.characters) {
59 1 : if (!romanDigits.containsKey(c)) {
60 : return null;
61 : }
62 : }
63 1 : return RomanNumerals._(input);
64 : }
65 :
66 : /// Attempts to evaluate a Roman numeral as an integer
67 : ///
68 : /// Evaluates a Roman numeral string as an integer.
69 : ///
70 : /// Returns the integer value, or `null` if [value] is empty or contains
71 : /// any character that is not a recognised Roman numeral digit.
72 : ///
73 : /// Allows additive repetition (e.g. `"IIII"` is accepted as 4, even though
74 : /// the canonical form is `"IV"`).
75 1 : int? toInt() {
76 : // go backwards through the digits
77 : var total = 0;
78 : var prevDigit = 0;
79 4 : final reversed = value.characters.toList().reversed;
80 :
81 2 : for (final c in reversed) {
82 1 : var n = romanDigits[c];
83 :
84 : if (n == null) {
85 : return null;
86 : }
87 :
88 1 : if (n < prevDigit) {
89 : // e.g. ix
90 1 : total -= n;
91 : } else {
92 1 : total += n;
93 : }
94 : prevDigit = n;
95 : }
96 : return total;
97 : }
98 :
99 2 : static bool isValid(String value) => tryParse(value) != null;
100 : }
|