number-master

web application to practice korean numbers
git clone git://brookjeynes.dev/bjeynes/number-master.git
Log | Files | Refs | README | LICENSE

commit 5932e3061f68b1ec22385097531f909e3d894996
parent 6e0eecba61a371a44891972b2c4750ab79ecd792
Author: brookjeynes <me@brookjeynes.dev>
Date:   Sun, 31 May 2026 12:26:02 +1000

feat: add native korean number lookups

Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Asrc/native.js | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/number.js | 105-------------------------------------------------------------------------------
Asrc/sino.js | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/store.js | 7++++---
4 files changed, 172 insertions(+), 108 deletions(-)

diff --git a/src/native.js b/src/native.js @@ -0,0 +1,63 @@ +/** + * @param {number} num + * @returns {string} + */ +export function nativeStringFromNumber(num) { + if (num <= 0) throw new Error("not a valid native korean number"); + if (num > 99) throw new Error("number cannot be larger than 99") + + const ONES = ["", "하나", "둘", "셋", "넷", "다섯", "여섯", "일곱", "여덟", "아홉"]; + const TENS = ["", "열", "스물", "서른", "마흔", "쉰", "예순", "일흔", "여든", "아흔"]; + + const groups = []; + let temp = num; + while (temp > 0) { + groups.push(temp % 10); + temp = Math.floor(temp / 10); + } + + const ones = ONES[groups[0]]; + + if (groups.length === 1) { + return ones; + } + + const tens = TENS[groups[1]]; + return tens + ones; +} + +/** + * @param {string} str + * @returns {number} + */ +export function numberFromNativeString(str) { + if (str.length === 0) throw new Error("string cannot be of zero length"); + if (str.length > 4) throw new Error("string cannot be greater than 4 characters"); + + const ONES = { "하나": 1, "둘": 2, "셋": 3, "넷": 4, "다섯": 5, "여섯": 6, "일곱": 7, "여덟": 8, "아홉": 9 }; + const TENS = { "열": 10, "스물": 20, "서른": 30, "마흔": 40, "쉰": 50, "예순": 60, "일흔": 70, "여든": 80, "아흔": 90 }; + + /** @type{number} */ + let result = 0; + /** @type{string} */ + let temp = str; + + for (const ten of Object.keys(TENS)) { + if (temp.startsWith(ten)) { + result += TENS[ten]; + temp = temp.slice(ten.length); + break; + } + } + for (const one of Object.keys(ONES)) { + if (temp.startsWith(one)) { + result += ONES[one]; + temp = temp.slice(one.length); + break; + } + } + + if (temp.length !== 0) throw new Error("invalid input"); + + return result; +} diff --git a/src/number.js b/src/number.js @@ -1,105 +0,0 @@ -/** - * @param {number} num - * @returns {string} - */ -export function hangeulStringFromNumber(num) { - const UNITS = ["", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"]; - const SMALL = ["", "십", "백", "천"]; - const LARGE = ["", "만", "억", "조"]; - - if (num === 0) return "영"; - - /** - * Holds the number split into groups of four ordered by least significant - * group. - * @type {number[]} - */ - const groups = []; - /** @type {number} */ - let temp = num; - while (temp > 0) { - groups.push(temp % 1_0000); - temp = Math.floor(temp / 1_0000) - } - - /** @type {string} */ - let result = []; - for (let g = groups.length - 1; g >= 0; g--) { - /** @type {number} */ - const group = groups[g]; - if (group === 0) continue; - - /** @type {number[]} */ - const digits = [ - Math.floor(group / 1000), // 천 - Math.floor((group % 1000) / 100), // 백 - Math.floor((group % 100) / 10), // 십 - group % 10, // 일 - ] - - /** @type {string} */ - let groupStr = ""; - for (let d = 0; d < 4; d++) { - /** @type {number} */ - const digit = digits[d]; - if (digit === 0) continue; - - /** - * Drop 일 before 십/백/천, but keep it before 만/억/조 - * @type {string} - */ - const prefix = (digit === 1 && d > 0 && d < 3) ? "" : UNITS[digit]; - groupStr += prefix + SMALL[3 - d]; - } - - result.push(groupStr + LARGE[g]); - } - - return result.join(" "); -} - -/** - * @param {string} str - * @returns {number} - */ -export function numberFromHangeulString(str) { - const UNITS = { "일": 1, "이": 2, "삼": 3, "사": 4, "오": 5, "육": 6, "칠": 7, "팔": 8, "구": 9 }; - const SMALL = { "십": 10, "백": 100, "천": 1000 }; - const LARGE = { "만": 10000, "억": 100000000, "조": 1000000000000 }; - - if (str.length === 0) throw new Error("string cannot be of zero length"); - if (str === "영") return 0; - - /** @type {number} */ - let result = 0; - /** - * Accumulates value within current large-unit group. - * @type {number} - */ - let currentGroup = 0; - /** - * Defaults to 1 to handle omitted 일 (e.g. "십" = 10). - * @type {number} - */ - let currentDigit = 1; - - for (const char of str) { - if (UNITS[char] !== undefined) { - currentDigit = UNITS[char]; - } else if (SMALL[char] !== undefined) { - currentGroup += currentDigit * SMALL[char]; - // reset for next digit (restores 일-omission default) - currentDigit = 1; - } else if (LARGE[char] !== undefined) { - // flush any trailing 일-place digit into group, then apply large unit - result += (currentGroup + currentDigit) * LARGE[char]; - currentGroup = 0; - currentDigit = 1; - } - } - - // flush the final group (no large unit suffix) - result += currentGroup + currentDigit; - - return result; -} diff --git a/src/sino.js b/src/sino.js @@ -0,0 +1,105 @@ +/** + * @param {number} num + * @returns {string} + */ +export function sinoStringFromNumber(num) { + const UNITS = ["", "일", "이", "삼", "사", "오", "육", "칠", "팔", "구"]; + const SMALL = ["", "십", "백", "천"]; + const LARGE = ["", "만", "억", "조"]; + + if (num === 0) return "영"; + + /** + * Holds the number split into groups of four ordered by least significant + * group. + * @type {number[]} + */ + const groups = []; + /** @type {number} */ + let temp = num; + while (temp > 0) { + groups.push(temp % 1_0000); + temp = Math.floor(temp / 1_0000) + } + + /** @type {string} */ + let result = []; + for (let g = groups.length - 1; g >= 0; g--) { + /** @type {number} */ + const group = groups[g]; + if (group === 0) continue; + + /** @type {number[]} */ + const digits = [ + Math.floor(group / 1000), // 천 + Math.floor((group % 1000) / 100), // 백 + Math.floor((group % 100) / 10), // 십 + group % 10, // 일 + ] + + /** @type {string} */ + let groupStr = ""; + for (let d = 0; d < 4; d++) { + /** @type {number} */ + const digit = digits[d]; + if (digit === 0) continue; + + /** + * Drop 일 before 십/백/천, but keep it before 만/억/조 + * @type {string} + */ + const prefix = (digit === 1 && d > 0 && d < 3) ? "" : UNITS[digit]; + groupStr += prefix + SMALL[3 - d]; + } + + result.push(groupStr + LARGE[g]); + } + + return result.join(" "); +} + +/** + * @param {string} str + * @returns {number} + */ +export function numberFromSinoString(str) { + const UNITS = { "일": 1, "이": 2, "삼": 3, "사": 4, "오": 5, "육": 6, "칠": 7, "팔": 8, "구": 9 }; + const SMALL = { "십": 10, "백": 100, "천": 1000 }; + const LARGE = { "만": 10000, "억": 100000000, "조": 1000000000000 }; + + if (str.length === 0) throw new Error("string cannot be of zero length"); + if (str === "영") return 0; + + /** @type {number} */ + let result = 0; + /** + * Accumulates value within current large-unit group. + * @type {number} + */ + let currentGroup = 0; + /** + * Defaults to 1 to handle omitted 일 (e.g. "십" = 10). + * @type {number} + */ + let currentDigit = 1; + + for (const char of str) { + if (UNITS[char] !== undefined) { + currentDigit = UNITS[char]; + } else if (SMALL[char] !== undefined) { + currentGroup += currentDigit * SMALL[char]; + // reset for next digit (restores 일-omission default) + currentDigit = 1; + } else if (LARGE[char] !== undefined) { + // flush any trailing 일-place digit into group, then apply large unit + result += (currentGroup + currentDigit) * LARGE[char]; + currentGroup = 0; + currentDigit = 1; + } + } + + // flush the final group (no large unit suffix) + result += currentGroup + currentDigit; + + return result; +} diff --git a/src/store.js b/src/store.js @@ -1,4 +1,5 @@ -import { hangeulStringFromNumber, numberFromHangeulString } from "./number.js"; +import { nativeStringFromNumber, numberFromNativeString } from "./native.js"; +import { sinoStringFromNumber, numberFromSinoString } from "./sino.js"; /** * @typedef { @@ -73,14 +74,14 @@ window.Alpine.store("app", { /** @returns {string} */ get string() { - return hangeulStringFromNumber(this.number); + return sinoStringFromNumber(this.number); }, /** @returns {string} */ get answer() { switch (this.mode) { case "number": - return hangeulStringFromNumber(this.number); + return sinoStringFromNumber(this.number); case "hangeul": return this.number.toString(); }