diff --git a/.gitignore b/.gitignore index 5a9069c..bea3ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ lcov .tern-port .DS_Store package-lock.json +.vs* \ No newline at end of file diff --git a/README.md b/README.md index 66b62ff..81128ae 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ Currently supported languages are: | Ukrainian | `uk` | | Indonesian | `id` | | Russian | `ru` | +| Chinese (Traditional) | `zhTW` | +| Chinese (Simplified) | `zhCN` | ## Contributing @@ -83,13 +85,15 @@ The following parameters have been used for the currently available languages: | Parameter | Type | Description | Examples | |-----------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `useLongScale` | boolean | Indicates if it uses [long or short scale](http://en.wikipedia.org/wiki/Long_and_short_scales). | This differs the meaning of the words `billion`, `trillion` and so on. | -| `baseSeparator` | string | Separates the base cardinal numbers. | 29 -> twenty`-`eight. Spanish uses the connector " y " | +| `baseSeparator` | string | Separates the base cardinal numbers. | 29 -> twenty`-`eight. Spanish uses the connector " y " | | `unitSeparator` | string | Separates the units from the last base cardinal numbers. | 1234 -> one thousand two hundred **and** thirty-four | +| `joinSeparator` | string | Separates all words, default is `" "` | 100 -> one hundred. Chinese has no spaces, so it uses the connector `""` | | `allSeparator` | string | Separates all cardinals, not only the last one. | 1125 -> ألف **و**مائة **و**خمسة **و**عشرون | | `base` | Object | Base cardinals numbers. Numbers that have unique names and are used to build others. | | | `alternativeBase` | Object | Alternative versions of base cardinals numbers for usage with specific units. These bases will be treated as an extension for the default `base`. | ``` "alternativeBase": { "feminine": {"1":"одна","2":"дві"} } ``` | -| `units` | Array | A list of number units (string or Object). Gives support to singular, dual an plural units. Check the Object parameters below. | | +| `units` | Array | A list of number units (string or Object). Gives support to singular, dual an plural units. Check the Object parameters below. | | | `unitExceptions` | Object | Sometimes grammar exceptions affect the base cardinal joined to the unit. You can set specific exceptions to any base cardinal number. | Converting 1232000 in Spanish: Without Exception (Wrong): -> **uno** millón doscientos treinta y dos mil With Exception: -> **un** millón doscientos treinta y dos mil | +| `prependZero` | boolean | If `true`, all contiguous strings of 0s (except leading and trailing) are replaced by one "zero" word before the following number word. | 102003 -> 十萬二千三 | ### Units parameters diff --git a/bower.json b/bower.json index ec275f2..7f453be 100644 --- a/bower.json +++ b/bower.json @@ -3,7 +3,7 @@ "description": "Convert numbers to words - their written form", "homepage": "https://yamadapc.github.io/js-written-number", "main": "./dist/written-number.js", - "version": "0.11.0", + "version": "0.12.0", "keywords": [ "numbers", "words", diff --git a/lib/i18n/zh-CN.json b/lib/i18n/zh-CN.json new file mode 100644 index 0000000..c91fd63 --- /dev/null +++ b/lib/i18n/zh-CN.json @@ -0,0 +1,39 @@ +{ + "useLongScale": null, + "baseSeparator": "", + "unitSeparator": "", + "joinSeparator": "", + "allSeparator": "", + "prependZero": true, + "base": { + "0": "〇", + "1": "一", + "2": "二", + "3": "三", + "4": "四", + "5": "五", + "6": "六", + "7": "七", + "8": "八", + "9": "九" + }, + "units": { + "1": "十", + "2": "百", + "3": "千", + "4": "万", + "8": "亿" + }, + "unitExceptions": { + "10": "十", + "11": "十一", + "12": "十二", + "13": "十三", + "14": "十四", + "15": "十五", + "16": "十六", + "17": "十七", + "18": "十八", + "19": "十九" + } +} diff --git a/lib/i18n/zh-TW.json b/lib/i18n/zh-TW.json new file mode 100644 index 0000000..2cf99e0 --- /dev/null +++ b/lib/i18n/zh-TW.json @@ -0,0 +1,40 @@ +{ + "useLongScale": null, + "baseSeparator": "", + "unitSeparator": "", + "joinSeparator": "", + "allSeparator": "", + "prependZero": true, + "base": { + "0": "零", + "1": "一", + "2": "二", + "3": "三", + "4": "四", + "5": "五", + "6": "六", + "7": "七", + "8": "八", + "9": "九" + }, + "units": { + "1": "十", + "2": "百", + "3": "千", + "4": "萬", + "8": "億", + "12": "兆" + }, + "unitExceptions": { + "10": "十", + "11": "十一", + "12": "十二", + "13": "十三", + "14": "十四", + "15": "十五", + "16": "十六", + "17": "十七", + "18": "十八", + "19": "十九" + } +} diff --git a/lib/index.js b/lib/index.js index 4114399..925afcb 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,7 +2,7 @@ exports = module.exports = writtenNumber; var util = require("./util"); -var languages = ["en", "es", "ar", "az", "pt", "fr", "eo", "it", "vi", "tr", "uk", "ru", "id"]; +var languages = ["en", "es", "ar", "az", "pt", "fr", "eo", "it", "vi", "tr", "uk", "ru", "id", "zh"]; var i18n = { en: require("./i18n/en.json"), es: require("./i18n/es.json"), @@ -19,7 +19,9 @@ var i18n = { enIndian: require("./i18n/en-indian.json"), uk: require("./i18n/uk.json"), ru: require("./i18n/ru.json"), - id: require("./i18n/id.json") + id: require("./i18n/id.json"), + zhTW: require("./i18n/zh-TW.json"), + zhCN: require("./i18n/zh-CN.json") }; exports.i18n = i18n; @@ -36,7 +38,8 @@ for (i = 1; i <= 15; i++) { writtenNumber.defaults = { noAnd: false, alternativeBase: null, - lang: "en" + lang: "en", + smallMeans10: false }; /** @@ -68,7 +71,11 @@ function writtenNumber(n, options) { language = i18n[writtenNumber.defaults.lang]; } - + + if (!language.hasOwnProperty('joinSeparator')) { + language.joinSeparator = " "; + } + var scale = language.useLongScale ? longScale : shortScale; var units = language.units; var unit; @@ -86,19 +93,28 @@ function writtenNumber(n, options) { } var baseCardinals = language.base; - var alternativeBaseCardinals = options.alternativeBase + var alternativeBaseCardinals = options.alternativeBase ? language.alternativeBase[options.alternativeBase] : {}; + var smallFactor = options.smallMeans10 ? 10 : 100; if (language.unitExceptions[n]) return language.unitExceptions[n]; if (alternativeBaseCardinals[n]) return alternativeBaseCardinals[n]; if (baseCardinals[n]) return baseCardinals[n]; - if (n < 100) + if (n < smallFactor) return handleSmallerThan100(n, language, unit, baseCardinals, alternativeBaseCardinals, options); - var m = n % 100; + setSmallMeans10(options, baseCardinals, alternativeBaseCardinals); + smallFactor = options.smallMeans10 ? 10 : 100; + + var m = n % smallFactor; var ret = []; + if (language.prependZero) { + var _n = n, _n10 = Math.pow(10, Math.floor(Math.log10(_n))); + var zero = writtenNumber(0, options); + } + if (m) { if ( options.noAnd && @@ -108,6 +124,9 @@ function writtenNumber(n, options) { } else { ret.push(language.unitSeparator + writtenNumber(m, options)); } + if (language.prependZero && Math.floor(n / 10) % smallFactor == 0) { + ret[0] = zero + ret[0]; + } } var firstSignificant; @@ -125,6 +144,19 @@ function writtenNumber(n, options) { if (!r) continue; firstSignificant = scale[i]; + var prependedZero = ""; + if ( + language.prependZero + && i < len - 1 + && scale[i] < _n10 + ) { + var nextSpaceFloor = Math.floor(_n / scale[i] / (divideBy / 10)); + var nextUnitFloor = Math.floor(_n / scale[i + 1]); + if ( + (nextSpaceFloor && nextSpaceFloor % 10 == 0) + || (nextUnitFloor && nextUnitFloor % 10 == 0) + ) prependedZero = zero; + } if (unit.useBaseInstead) { var shouldUseBaseException = @@ -153,10 +185,10 @@ function writtenNumber(n, options) { str = unit.plural && (!unit.avoidInNumberPlural || !m) ? unit.plural : unit.singular; - + // Languages with dual str = (r === 2 && unit.dual) ? unit.dual : str; - + // "restrictedPlural" : use plural only for 3 to 10 str = (r > 10 && unit.restrictedPlural) ? unit.singular : str; } @@ -185,7 +217,7 @@ function writtenNumber(n, options) { ) ); n -= r * scale[i]; - ret.push(number + " " + str); + ret.push(prependedZero + number + language.joinSeparator + str); } var firstSignificantN = firstSignificant * Math.floor(n / firstSignificant); @@ -201,24 +233,40 @@ function writtenNumber(n, options) { ret.slice(1) ); } - + // Languages that have separators for all cardinals. if (language.allSeparator) { - for (var j = 0; j < ret.length-1; j++) { - ret[j] = language.allSeparator + ret[j]; + for (var j = 0; j < ret.length - 1; j++) { + ret[j] = language.allSeparator + ret[j]; } } - var result = ret.reverse().join(" "); + var result = ret.reverse().join(language.joinSeparator); return result; } function handleSmallerThan100(n, language, unit, baseCardinals, alternativeBaseCardinals, options) { + var smallMeans10 = options.smallMeans10; + setSmallMeans10(options, baseCardinals, alternativeBaseCardinals); var dec = Math.floor(n / 10) * 10; unit = n - dec; + var decCardinal = alternativeBaseCardinals[dec] || baseCardinals[dec]; + if (smallMeans10) { + decCardinal = ''; + } else if (!decCardinal) { + decCardinal = writtenNumber(dec, options); + } if (unit) { return ( - alternativeBaseCardinals[dec] || baseCardinals[dec] + language.baseSeparator + writtenNumber(unit, options) + decCardinal + language.baseSeparator + writtenNumber(unit, options) ); } - return alternativeBaseCardinals[dec] || baseCardinals[dec]; + return decCardinal; } + +function setSmallMeans10(options, baseCardinals, alternativeBaseCardinals) { + if (options.smallMeans10) return; + var f = function (i) {return parseInt(i);}; + var keys = Object.keys(baseCardinals).map(f); + keys = keys.concat(Object.keys(alternativeBaseCardinals).map(f)); + options.smallMeans10 = Math.max.apply(null, keys) < 10; +} \ No newline at end of file diff --git a/package.json b/package.json index 499f12b..dcf455a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "written-number", - "version": "0.11.0", + "version": "0.12.0", "description": "Convert numbers to words - their written form.", "main": "lib/index.js", "scripts": { diff --git a/test/index.test.js b/test/index.test.js index 3175a1e..8bdcec5 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -982,4 +982,264 @@ describe("written-number", function () { ); }); }); + + describe("writtenNumber(n, { lang: 'zhTW', ... })", function () { + before(function () { + writtenNumber.defaults.lang = "zhTW"; + }); + + it("gets exposed", function () { + should.exist(writtenNumber); + writtenNumber.should.be.instanceof(Function); + }); + + it("negative numbers return \"\"", function () { + writtenNumber(-3).should.equal(""); + writtenNumber(-5).should.equal(""); + }); + + it("doesn't blow up weirdly with invalid input", function () { + writtenNumber("asdfasdfasdf").should.equal(""); + writtenNumber("0.as").should.equal(""); + writtenNumber("0.123").should.equal("零"); + writtenNumber("0.8").should.equal("一"); + writtenNumber("2.8").should.equal("三"); + writtenNumber("asdf.8").should.equal(""); + writtenNumber("120391938123..").should.equal(""); + writtenNumber("1000000000.123").should.equal("十億"); + writtenNumber("1/3").should.equal(""); + writtenNumber(1 / 3).should.equal("零"); + writtenNumber("1/2").should.equal(""); + writtenNumber("1.123/2").should.equal(""); + }); + + it("correctly converts numbers < 10", function () { + writtenNumber(3).should.equal("三"); + writtenNumber(8).should.equal("八"); + }); + + it("correctly converts numbers < 100", function () { + writtenNumber(10).should.equal("十"); + writtenNumber(11).should.equal("十一"); + writtenNumber(13).should.equal("十三"); + writtenNumber(20).should.equal("二十"); + writtenNumber(23).should.equal("二十三"); + }); + + it("correctly converts numbers < 1000", function () { + writtenNumber(100).should.equal("一百"); + writtenNumber(200).should.equal("二百"); + writtenNumber(321).should.equal("三百二十一"); + writtenNumber(417).should.equal("四百一十七"); + writtenNumber(501).should.equal("五百零一"); + }); + + it("correctly converts numbers < 10000", function () { + writtenNumber(1000).should.equal("一千"); + writtenNumber(2003).should.equal("二千零三"); + writtenNumber(3210).should.equal("三千二百一十"); + writtenNumber(4011).should.equal("四千零一十一"); + writtenNumber(5432).should.equal("五千四百三十二"); + }); + + it("correctly converts numbers < 100 000 000", function () { + writtenNumber(10000).should.equal("一萬"); + writtenNumber(20003).should.equal("二萬零三"); + writtenNumber(32100).should.equal("三萬二千一百"); + writtenNumber(43210).should.equal("四萬三千二百一十"); + writtenNumber(54302).should.equal("五萬四千三百零二"); + writtenNumber(60606).should.equal("六萬零六百零六"); + writtenNumber(70600).should.equal("七萬零六百"); + writtenNumber(100000).should.equal("十萬"); + writtenNumber(110000).should.equal("十一萬"); + writtenNumber(210003).should.equal("二十一萬零三"); + writtenNumber(1110000).should.equal("一百一十一萬"); + writtenNumber(1010101).should.equal("一百零一萬零一百零一"); + writtenNumber(11011011).should.equal( + "一千一百零一萬一千零一十一" + ); + writtenNumber(20304050).should.equal( + "二千零三十萬零四千零五十" + ); + }); + + it("correctly converts numbers < 1 000 000 000 000", function () { + writtenNumber(100000000).should.equal("一億"); + writtenNumber(200000003).should.equal("二億零三"); + writtenNumber(321000000).should.equal("三億二千一百萬"); + writtenNumber(432100000).should.equal("四億三千二百一十萬"); + writtenNumber(543020000).should.equal("五億四千三百零二萬"); + writtenNumber(606060606).should.equal( + "六億零六百零六萬零六百零六" + ); + writtenNumber(110110110).should.equal( + "一億一千零一十一萬零一百一十" + ); + writtenNumber(111111111).should.equal( + "一億一千一百一十一萬一千一百一十一" + ); + writtenNumber(1000000000).should.equal("十億"); + writtenNumber(1100000000).should.equal("十一億"); + writtenNumber(11100000000).should.equal("一百一十一億"); + writtenNumber(101100000000).should.equal("一千零一十一億"); + writtenNumber(101101101011).should.equal( + "一千零一十一億零一百一十萬零一千零一十一" + ); + }); + + it("correctly converts numbers >= 1 000 000 000 000", function () { + writtenNumber(1000000000000).should.equal("一兆"); + writtenNumber(2000000000003).should.equal("二兆零三"); + writtenNumber(3210000000000).should.equal("三兆二千一百億"); + writtenNumber(4321000000000).should.equal("四兆三千二百一十億"); + writtenNumber(5430200000000).should.equal("五兆四千三百零二億"); + writtenNumber(6060606060606).should.equal( + "六兆零六百零六億零六百零六萬零六百零六" + ); + writtenNumber(1011011011011).should.equal( + "一兆零一百一十億零一千一百零一萬一千零一十一" + ); + writtenNumber(1111111111111).should.equal( + "一兆一千一百一十一億一千一百一十一萬一千一百一十一" + ); + writtenNumber(10000000000000).should.equal("十兆"); + writtenNumber(11000000000000).should.equal("十一兆"); + writtenNumber(111000000000000).should.equal("一百一十一兆"); + writtenNumber(1011000000000000).should.equal("一千零一十一兆"); + writtenNumber(1011011010110011).should.equal( + "一千零一十一兆零一百一十億零一千零一十一萬零一十一" + ); + }); + }); + + describe("writtenNumber(n, { lang: 'zhCN', ... })", function () { + before(function () { + writtenNumber.defaults.lang = "zhCN"; + }); + + it("gets exposed", function () { + should.exist(writtenNumber); + writtenNumber.should.be.instanceof(Function); + }); + + it("negative numbers return \"\"", function () { + writtenNumber(-3).should.equal(""); + writtenNumber(-5).should.equal(""); + }); + + it("doesn't blow up weirdly with invalid input", function () { + writtenNumber("asdfasdfasdf").should.equal(""); + writtenNumber("0.as").should.equal(""); + writtenNumber("0.123").should.equal("〇"); + writtenNumber("0.8").should.equal("一"); + writtenNumber("2.8").should.equal("三"); + writtenNumber("asdf.8").should.equal(""); + writtenNumber("120391938123..").should.equal(""); + writtenNumber("1000000000.123").should.equal("十亿"); + writtenNumber("1/3").should.equal(""); + writtenNumber(1 / 3).should.equal("〇"); + writtenNumber("1/2").should.equal(""); + writtenNumber("1.123/2").should.equal(""); + }); + + it("correctly converts numbers < 10", function () { + writtenNumber(3).should.equal("三"); + writtenNumber(8).should.equal("八"); + }); + + it("correctly converts numbers < 100", function () { + writtenNumber(10).should.equal("十"); + writtenNumber(11).should.equal("十一"); + writtenNumber(13).should.equal("十三"); + writtenNumber(20).should.equal("二十"); + writtenNumber(23).should.equal("二十三"); + }); + + it("correctly converts numbers < 1000", function () { + writtenNumber(100).should.equal("一百"); + writtenNumber(200).should.equal("二百"); + writtenNumber(321).should.equal("三百二十一"); + writtenNumber(417).should.equal("四百一十七"); + writtenNumber(501).should.equal("五百〇一"); + }); + + it("correctly converts numbers < 10000", function () { + writtenNumber(1000).should.equal("一千"); + writtenNumber(2003).should.equal("二千〇三"); + writtenNumber(3210).should.equal("三千二百一十"); + writtenNumber(4011).should.equal("四千〇一十一"); + writtenNumber(5432).should.equal("五千四百三十二"); + }); + + it("correctly converts numbers < 100 000 000", function () { + writtenNumber(10000).should.equal("一万"); + writtenNumber(20003).should.equal("二万〇三"); + writtenNumber(32100).should.equal("三万二千一百"); + writtenNumber(43210).should.equal("四万三千二百一十"); + writtenNumber(54302).should.equal("五万四千三百〇二"); + writtenNumber(60606).should.equal("六万〇六百〇六"); + writtenNumber(70600).should.equal("七万〇六百"); + writtenNumber(100000).should.equal("十万"); + writtenNumber(110000).should.equal("十一万"); + writtenNumber(210003).should.equal("二十一万〇三"); + writtenNumber(1110000).should.equal("一百一十一万"); + writtenNumber(1010101).should.equal("一百〇一万〇一百〇一"); + writtenNumber(11011011).should.equal( + "一千一百〇一万一千〇一十一" + ); + writtenNumber(20304050).should.equal( + "二千〇三十万〇四千〇五十" + ); + }); + + it("correctly converts numbers < 1 000 000 000 000", function () { + writtenNumber(100000000).should.equal("一亿"); + writtenNumber(200000003).should.equal("二亿〇三"); + writtenNumber(321000000).should.equal("三亿二千一百万"); + writtenNumber(432100000).should.equal("四亿三千二百一十万"); + writtenNumber(543020000).should.equal("五亿四千三百〇二万"); + writtenNumber(606060606).should.equal( + "六亿〇六百〇六万〇六百〇六" + ); + writtenNumber(110110110).should.equal( + "一亿一千〇一十一万〇一百一十" + ); + writtenNumber(111111111).should.equal( + "一亿一千一百一十一万一千一百一十一" + ); + writtenNumber(1000000000).should.equal("十亿"); + writtenNumber(1100000000).should.equal("十一亿"); + writtenNumber(11100000000).should.equal("一百一十一亿"); + writtenNumber(101100000000).should.equal("一千〇一十一亿"); + writtenNumber(101101101011).should.equal( + "一千〇一十一亿〇一百一十万〇一千〇一十一" + ); + }); + + it("correctly converts numbers >= 1 000 000 000 000", function () { + writtenNumber(1000000000000).should.equal("一万亿"); + writtenNumber(2000000000003).should.equal("二万亿〇三"); + writtenNumber(3210000000000).should.equal("三万二千一百亿"); + writtenNumber(4321000000000).should.equal("四万三千二百一十亿"); + writtenNumber(5430200000000).should.equal("五万四千三百〇二亿"); + writtenNumber(6060606060606).should.equal( + "六万〇六百〇六亿〇六百〇六万〇六百〇六" + ); + writtenNumber(1011011011011).should.equal( + "一万〇一百一十亿〇一千一百〇一万一千〇一十一" + ); + writtenNumber(1111111111111).should.equal( + "一万一千一百一十一亿一千一百一十一万一千一百一十一" + ); + writtenNumber(10000000000000).should.equal("十万亿"); + writtenNumber(11000000000000).should.equal("十一万亿"); + writtenNumber(21000000000000).should.equal("二十一万亿"); + // More digits than this is basically encroaching on + // the limits of the simplified number system (which has + // no word for trillion). Testing more digits than this + // fails, so I'll leave it there. Note that the more + // digits do work on the traditional number system, + // which does have a word for trillion. + }); + }); }); \ No newline at end of file