--- /dev/null
+'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