bump package version to 1.0.1
[squeep-roman] / index.js
1 'use strict';
2
3 /**
4 * Integer to Unicode character.
5 * These pre-combined numbers are only used for direct digit mappings
6 * for these numbers.
7 * A previous implementation would compact occurrences of these multi-character
8 * combinations in larger numbers; however, some fonts render the multi-character
9 * combinations as slightly different sizes than the stand-alone characters,
10 * which made them render oddly.
11 */
12 const singleMapping = {
13 1: '\u2160', // Ⅰ Ⅰ
14 2: '\u2161', // Ⅱ Ⅱ
15 3: '\u2162', // Ⅲ Ⅲ
16 4: '\u2163', // Ⅳ Ⅳ
17 5: '\u2164', // Ⅴ Ⅴ
18 6: '\u2165', // Ⅵ Ⅵ
19 7: '\u2166', // Ⅶ Ⅶ
20 8: '\u2167', // Ⅷ Ⅷ
21 9: '\u2168', // Ⅸ Ⅸ
22 10: '\u2169', // Ⅹ Ⅹ
23 11: '\u216a', // Ⅺ Ⅺ
24 12: '\u216b', // Ⅻ Ⅻ
25 };
26
27 /**
28 * Viable mappings from a number to the Roman representation.
29 */
30 const mapping = {
31 1: '\u2160', // Ⅰ Ⅰ
32 4: '\u2160\u2164', // ⅠⅤ ⅠⅤ
33 5: '\u2164', // Ⅴ Ⅴ
34 9: '\u2160\u2169', // Ⅸ Ⅸ
35 10: '\u2169', // Ⅹ Ⅹ
36 40: '\u2169\u216c', // ⅩⅬ ⅩⅬ
37 50: '\u216c', // Ⅼ Ⅼ
38 90: '\u2169\u216d', // ⅩⅭ ⅩⅭ
39 100: '\u216d', // Ⅽ Ⅽ
40 400: '\u216d\u216e', // ⅭⅮ ⅭⅮ
41 500: '\u216e', // Ⅾ Ⅾ
42 900: '\u216d\u216f', // ⅭⅯ ⅭⅯ
43 1000: '\u216f', // Ⅿ Ⅿ
44 };
45
46 /**
47 * Render all characters of #s as HTML-encoded entities.
48 * @param {String} s
49 * @returns {string}
50 */
51 function htmlEncode(s) {
52 return s
53 .split('')
54 .map((c) => `&#${c.codePointAt(0)};`)
55 .join('');
56 }
57
58 /**
59 * The keys of our number->roman mappings, sorted highest-first.
60 * We will walk this list, rendering input-value-divided-
61 * by-the-key-number mapped values, modulus-ing the input-value, and
62 * repeating for the remaining key-numbers.
63 */
64 const divisors = Object.keys(mapping)
65 .sort((a, b) => b - a)
66 .map(Number);
67
68 /**
69 * Convert a number to its Roman representation, using Unicode characters
70 * or HTML entities.
71 * @param {Number} num
72 * @param {Boolean} asEntities if true, return html instead of unicode
73 * @returns {String}
74 * @throws {RangeError}
75 */
76 function toRoman(num, asEntities = false) {
77 if (!num || isNaN(Number(num))) {
78 throw new RangeError(`cannot convert '${num}' (${typeof num})`);
79 }
80 if (num < 0) {
81 throw new RangeError('cannot convert negative numbers');
82 }
83 const romanDigits = [];
84 if (num <= 12) {
85 romanDigits.push(singleMapping[num]);
86 } else {
87 divisors.forEach((d) => {
88 const length = Math.floor(num / d);
89 romanDigits.push(mapping[d].repeat(length));
90 num %= d;
91 });
92 }
93 const romanString = romanDigits.join('');
94
95 return asEntities ? htmlEncode(romanString) : romanString;
96 }
97
98 module.exports = {
99 toRoman,
100 };