initial commit
[squeep-roman] / index.js
diff --git a/index.js b/index.js
new file mode 100644 (file)
index 0000000..daed9d7
--- /dev/null
+++ b/index.js
@@ -0,0 +1,100 @@
+'use strict';
+
+/**
+ * Integer to Unicode character.
+ * These pre-combined numbers are only used for direct digit mappings
+ * for these numbers.
+ * A previous implementation would compact occurrences of these multi-character
+ * combinations in larger numbers; however, some fonts render the multi-character
+ * combinations as slightly different sizes than the stand-alone characters,
+ * which made them render oddly.
+ */
+const singleMapping = {
+  1: '\u2160', // Ⅰ Ⅰ
+  2: '\u2161', // Ⅱ Ⅱ
+  3: '\u2162', // Ⅲ Ⅲ
+  4: '\u2163', // Ⅳ Ⅳ
+  5: '\u2164', // Ⅴ Ⅴ
+  6: '\u2165', // Ⅵ Ⅵ
+  7: '\u2166', // Ⅶ Ⅶ
+  8: '\u2167', // Ⅷ Ⅷ
+  9: '\u2168', // Ⅸ Ⅸ
+  10: '\u2169', // Ⅹ Ⅹ
+  11: '\u216a', // Ⅺ Ⅺ
+  12: '\u216b', // Ⅻ Ⅻ
+};
+
+/**
+ * Viable mappings from a number to the Roman representation.
+ */
+const mapping = {
+  1: '\u2160', // Ⅰ Ⅰ
+  4: '\u2160\u2164', // ⅠⅤ ⅠⅤ
+  5: '\u2164', // Ⅴ Ⅴ
+  9: '\u2160\u2169', // Ⅸ Ⅸ
+  10: '\u2169', // Ⅹ Ⅹ
+  40: '\u2169\u216c', // ⅩⅬ ⅩⅬ
+  50: '\u216c', // Ⅼ Ⅼ
+  90: '\u2169\u216d', // ⅩⅭ ⅩⅭ
+  100: '\u216d', // Ⅽ Ⅽ
+  400: '\u216d\u216e', // ⅭⅮ ⅭⅮ
+  500: '\u216e', // Ⅾ Ⅾ
+  900: '\u216d\u216f', // ⅭⅯ ⅭⅯ
+  1000: '\u216f', // Ⅿ Ⅿ
+};
+
+/**
+ * Render all characters of #s as HTML-encoded entities.
+ * @param {String} s
+ * @returns {string}
+ */
+function htmlEncode(s) {
+  return s
+    .split('')
+    .map((c) => `&#${c.codePointAt(0)};`)
+    .join('');
+}
+
+/**
+ * The keys of our number->roman mappings, sorted highest-first.
+ * We will walk this list, rendering input-value-divided-
+ * by-the-key-number mapped values, modulus-ing the input-value, and
+ * repeating for the remaining key-numbers.
+ */
+const divisors = Object.keys(mapping)
+  .sort((a, b) => b - a)
+  .map(Number);
+
+/**
+ * Convert a number to its Roman representation, using Unicode characters
+ * or HTML entities.
+ * @param {Number} num
+ * @param {Boolean} asEntities if true, return html instead of unicode
+ * @returns {String}
+ * @throws {RangeError}
+ */
+function toRoman(num, asEntities = false) {
+  if (!num || isNaN(Number(num))) {
+    throw new RangeError(`cannot convert '${num}' (${typeof num})`);
+  }
+  if (num < 0) {
+    throw new RangeError('cannot convert negative numbers');
+  }
+  const romanDigits = [];
+  if (num <= 12) {
+    romanDigits.push(singleMapping[num]);
+  } else {
+    divisors.forEach((d) => {
+      const length = Math.floor(num / d);
+      romanDigits.push(mapping[d].repeat(length));
+      num %= d;
+    });
+  }
+  const romanString = romanDigits.join('');
+
+  return asEntities ? htmlEncode(romanString) : romanString;
+}
+
+module.exports = {
+  toRoman,
+};
\ No newline at end of file