n2words 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ /* eslint-disable import/no-nodejs-modules */
2
+ import n2words from '../lib/i18n/EN.mjs';
3
+ import * as readline from 'node:readline/promises';
4
+ import {stdin as input, stdout as output} from 'node:process';
5
+
6
+ const rl = readline.createInterface({input, output});
7
+
8
+ console.log(n2words(await rl.question('Value to convert? ')));
9
+
10
+ rl.close();
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Creates new language class that processes decimals separately.
3
+ * Requires implementing `toCardinal`.
4
+ */
5
+ export default class {
6
+ #negativeWord;
7
+ #separatorWord;
8
+ #zero;
9
+ #spaceSeparator;
10
+ #wholeNumber;
11
+
12
+ /**
13
+ * @param {object} options Options for class.
14
+ * @param {string} [options.negativeWord = ''] Word that precedes a negative number (if any).
15
+ * @param {string} options.separatorWord Word that separates cardinal numbers (i.e. "and").
16
+ * @param {string} options.zero Word for 0 (i.e. "zero").
17
+ * @param {string} [options.spaceSeparator = ' '] Character that separates words.
18
+ */
19
+ constructor(options) {
20
+ // Merge supplied options with defaults
21
+ options = Object.assign({
22
+ negativeWord: '',
23
+ separatorWord: undefined,
24
+ zeroWord: undefined,
25
+ spaceSeparator: ' '
26
+ }, options);
27
+
28
+ // Make options available to class
29
+ this.#negativeWord = options.negativeWord;
30
+ this.#separatorWord = options.separatorWord;
31
+ this.#zero = options.zero;
32
+ this.#spaceSeparator = options.spaceSeparator;
33
+ }
34
+
35
+ /**
36
+ * @returns {string} Word that precedes a negative number (if any).
37
+ */
38
+ get negativeWord() {
39
+ return this.#negativeWord;
40
+ }
41
+
42
+ /**
43
+ * @returns {string} Word that separates cardinal numbers (i.e. "and").
44
+ */
45
+ get separatorWord() {
46
+ return this.#separatorWord;
47
+ }
48
+
49
+ /**
50
+ * @returns {string} Word for 0 (i.e. "zero").
51
+ */
52
+ get zero() {
53
+ return this.#zero;
54
+ }
55
+
56
+ /**
57
+ * @returns {string} Character that separates words.
58
+ */
59
+ get spaceSeparator() {
60
+ return this.#spaceSeparator;
61
+ }
62
+
63
+ /**
64
+ * @returns {number} Input value without decimal.
65
+ */
66
+ get wholeNumber() {
67
+ return this.#wholeNumber;
68
+ }
69
+
70
+ /**
71
+ * Convert decimal number to a string array of cardinal numbers.
72
+ * @param {number} decimal Decimal number to convert.
73
+ * @returns {string} Value in written format.
74
+ */
75
+ decimalToCardinal(decimal) {
76
+ const words = [];
77
+
78
+ // Split decimal portion into an array of characters in reverse
79
+ const chars = decimal.split('').reverse();
80
+
81
+ // Loop through array (from the end) adding words to output array
82
+ while (chars.pop() == '0') {
83
+ words.push(this.zero);
84
+ }
85
+
86
+ // Add decimal number to word array
87
+ return words.concat(this.toCardinal(BigInt(decimal)));
88
+ }
89
+
90
+ /**
91
+ * Converts a number to written form.
92
+ * @param {number|string} value Number to be convert.
93
+ * @throws {Error} Value must be a valid number.
94
+ * @returns {string} Value in written format.
95
+ */
96
+ floatToCardinal(value) {
97
+ // Validate user input value
98
+ if (typeof value == 'number') {
99
+ if (Number.isNaN(value)) {
100
+ throw new Error('NaN is not an accepted number.');
101
+ }
102
+ value = value.toString();
103
+ } else if (typeof value == 'string') {
104
+ value = value.trim();
105
+ if (value.length == 0 || Number.isNaN(Number(value))) {
106
+ throw new Error('"' + value + '" is not a valid number.');
107
+ }
108
+ } else if (typeof value != 'bigint') {
109
+ throw new TypeError('Invalid variable type: ' + typeof value);
110
+ }
111
+
112
+ let words = [];
113
+ let wholeNumber;
114
+ let decimalNumber;
115
+
116
+ // If negative number add negative word
117
+ if (value < 0) {
118
+ words.push(this.negativeWord);
119
+ }
120
+
121
+ // Split value decimal (if any) (excluding BigInt)
122
+ if (typeof value == 'bigint') {
123
+ wholeNumber = value;
124
+ } else {
125
+ const splitValue = value.split('.');
126
+ wholeNumber = BigInt(splitValue[0]);
127
+ decimalNumber = splitValue[1];
128
+ }
129
+
130
+ // Convert whole number to positive (if negative)
131
+ if (wholeNumber < 0) {
132
+ wholeNumber = -wholeNumber;
133
+ }
134
+
135
+ // NOTE: Only needed for CZ
136
+ this.#wholeNumber = wholeNumber;
137
+
138
+ // Add whole number in written form
139
+ words = words.concat(this.toCardinal(wholeNumber));
140
+
141
+ // Add decimal number in written form (if any)
142
+ if (decimalNumber) {
143
+ words.push(this.separatorWord);
144
+
145
+ words = words.concat(this.decimalToCardinal(decimalNumber));
146
+ }
147
+
148
+ // Join words with spaces
149
+ return words.join(this.spaceSeparator);
150
+ }
151
+ }
@@ -0,0 +1,174 @@
1
+ import AbstractLanguage from './AbstractLanguage.mjs';
2
+
3
+ /**
4
+ * Creates new common language class that uses a highest matching word value algorithm.
5
+ * Number matching word {@link cards} must be provided for this to work.
6
+ * See {@link AbstractLanguage} for further requirements.
7
+ * @classdesc Common class for (mostly european) languages.
8
+ */
9
+ export default class extends AbstractLanguage {
10
+ #cards;
11
+
12
+ /**
13
+ * @param {object} options Options for class.
14
+ * @param {string} [options.negativeWord = ''] Word that precedes a negative number (if any).
15
+ * @param {string} options.separatorWord Word that separates cardinal numbers (i.e. "and").
16
+ * @param {string} options.zero Word for 0 (i.e. "zero").
17
+ * @param {string} [options.spaceSeparator = ' '] Character that separates words.
18
+ * @param {Array} cards Array of number matching "cards" from highest-to-lowest.
19
+ */
20
+ constructor(options, cards) {
21
+ super(options);
22
+
23
+ this.#cards = cards;
24
+ }
25
+
26
+ /**
27
+ * Get array of number matching "cards" from highest-to-lowest.
28
+ * First element in card array is the number to match while the second is the word to use.
29
+ * @example
30
+ * [
31
+ * ...
32
+ * [100, 'hundred'],
33
+ * ...
34
+ * [1, 'one'],
35
+ * ]
36
+ * @returns {Array} Array of number matching "cards" from highest-to-lowest.
37
+ */
38
+ get cards() {
39
+ return this.#cards;
40
+ }
41
+
42
+ /**
43
+ * Get word for number if it matches a language card.
44
+ * @param {number} number Card number value.
45
+ * @returns {string|undefined} Return card word or undefined if no card.
46
+ */
47
+ getCardWord(number) {
48
+ // Get matching card from number
49
+ const card = this.cards.find(_card => _card[0] == number);
50
+
51
+ // Return card word or undefined if no card found
52
+ return (Array.isArray(card) ? card[1] : undefined);
53
+ }
54
+
55
+ /**
56
+ * Get array of card matches.
57
+ * @param {number} value The number value to convert to cardinal form.
58
+ * @returns {object} Word sets (and pairs) from value.
59
+ * @todo Simplify return object.
60
+ */
61
+ toCardMatches(value) {
62
+ const out = [];
63
+ let remaining = value;
64
+
65
+ do {
66
+ // Find card with highest matching number
67
+ const card = this.cards.find(card => {
68
+ return remaining >= card[0];
69
+ });
70
+
71
+ let quantity; // Quantity of card set values
72
+
73
+ // Calculate quantity and remaining value
74
+ // Override variables for 0 as math will fail
75
+ if (remaining == 0) {
76
+ quantity = 1;
77
+ remaining = 0;
78
+ } else {
79
+ quantity = remaining / card[0];
80
+ remaining = remaining % card[0];
81
+ }
82
+
83
+ // Is value perfect match of card number?
84
+ if (quantity == 1) {
85
+ /** @todo Merge word set pairs together (if possible) to simplify return object */
86
+ out.push({
87
+ [this.getCardWord(1)]: 1,
88
+ });
89
+ } else {
90
+ /** @todo Understand the logic for this */
91
+ /*if (quantity == remaining) {
92
+ return [(quantity * this.getCardWord(card[0]), quantity * card[0])];
93
+ }*/
94
+
95
+ /**
96
+ * @todo Remove reciprocating calls.
97
+ */
98
+ out.push(this.toCardMatches(quantity));
99
+ }
100
+
101
+ // Add matching word set to output list
102
+ out.push({
103
+ [card[1]]: card[0],
104
+ });
105
+ }
106
+ while (remaining > 0);
107
+
108
+ return out;
109
+ }
110
+
111
+ scanNum(value) {
112
+ return value.split('').map(v => this.getCardWord(Number(v)));
113
+ }
114
+
115
+ clean(words) {
116
+ let out = words;
117
+
118
+ // Loop through word sets while array size is greater or less than 1
119
+ /** @todo Change logic to work in for loop to better understand loop intentions */
120
+ while (words.length != 1) {
121
+ out = [];
122
+ const left = words[0];
123
+ const right = words[1];
124
+
125
+ // Are the first & second word sets arrays?
126
+ if (!Array.isArray(left) && !Array.isArray(right)) {
127
+ // Merge word set pair and add to output array
128
+ out.push(this.merge(left, right));
129
+
130
+ /** @todo Understand */
131
+ if (words.slice(2).length > 0) {
132
+ out.push(words.slice(2));
133
+ }
134
+ } else {
135
+ // Loop through
136
+ for (let i = 0; i < words.length; i++) {
137
+ const elem = words[i];
138
+
139
+ if (Array.isArray(elem)) {
140
+ if (elem.length == 1) out.push(elem[0]);
141
+ else out.push(this.clean(elem));
142
+ } else {
143
+ out.push(elem);
144
+ }
145
+ }
146
+ }
147
+
148
+ words = out;
149
+ }
150
+
151
+ return out[0];
152
+ }
153
+
154
+ postClean(out0) {
155
+ return out0.trimRight();
156
+ }
157
+
158
+ /**
159
+ * Convert a whole number to written format.
160
+ * @param {number} value The number value to convert to cardinal form.
161
+ * @returns {string} Value in written format.
162
+ */
163
+ toCardinal(value) {
164
+ // Convert value to word sets
165
+ const words = this.toCardMatches(value);
166
+
167
+ // Process word sets
168
+ const preWords = Object.keys(this.clean(words))[0];
169
+
170
+ // Process word sets some more and return result
171
+ /** @todo Look into language functions/events */
172
+ return this.postClean(preWords);
173
+ }
174
+ }
package/lib/i18n/AR.mjs CHANGED
@@ -1,38 +1,52 @@
1
- import N2WordsAbs from '../classes/N2WordsAbs.mjs';
2
-
3
- export class N2WordsAR extends N2WordsAbs {
4
- constructor() {
5
- super();
6
-
7
- this.integerValue = 0;
8
- this.decimalValue = 0;
9
- this.negativeWord = 'ناقص';
10
- this.separatorWord = 'فاصلة';
11
- this.number = 0;
12
- this.zero = 'صفر';
13
- // this.isCurrencyPartNameFeminine = true
14
- // this.isCurrencyNameFeminine = false
15
- this.arabicOnes = [
16
- '', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية',
17
- 'تسعة',
18
- 'عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر',
19
- 'ستة عشر', 'سبعة عشر', 'ثمانية عشر',
20
- 'تسعة عشر',
21
- ];
22
- this.arabicFeminineOnes = [
23
- '', 'إحدى', 'اثنتان', 'ثلاث', 'أربع', 'خمس', 'ست', 'سبع', 'ثمان',
24
- 'تسع',
25
- 'عشر', 'إحدى عشرة', 'اثنتا عشرة', 'ثلاث عشرة', 'أربع عشرة',
26
- 'خمس عشرة', 'ست عشرة', 'سبع عشرة', 'ثماني عشرة',
27
- 'تسع عشرة',
28
- ];
29
- this.arabicTens = ['عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون'];
30
- this.arabicHundreds = ['', 'مائة', 'مئتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة', 'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة'];
31
- this.arabicAppendedTwos = ['مئتا', 'ألفا', 'مليونا', 'مليارا', 'تريليونا', 'كوادريليونا', 'كوينتليونا', 'سكستيليونا'];
32
- this.arabicTwos = ['مئتان', 'ألفان', 'مليونان', 'ملياران', 'تريليونان', 'كوادريليونان', 'كوينتليونان', 'سكستيليونان'];
33
- this.arabicGroup = ['مائة', 'ألف', 'مليون', 'مليار', 'تريليون', 'كوادريليون', 'كوينتليون', 'سكستيليون'];
34
- this.arabicAppendedGroup = ['', 'ألفاً', 'مليوناً', 'ملياراً', 'تريليوناً', 'كوادريليوناً', 'كوينتليوناً', 'سكستيليوناً'];
35
- this.arabicPluralGroups = ['', 'آلاف', 'ملايين', 'مليارات', 'تريليونات', 'كوادريليونات', 'كوينتليونات', 'سكستيليونات'];
1
+ import AbstractLanguage from '../classes/AbstractLanguage.mjs';
2
+
3
+ export class Arabic extends AbstractLanguage {
4
+ integerValue = 0;
5
+
6
+ decimalValue = 0;
7
+
8
+ number = 0;
9
+
10
+ arabicOnes = [
11
+ '', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية',
12
+ 'تسعة',
13
+ 'عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر',
14
+ 'ستة عشر', 'سبعة عشر', 'ثمانية عشر',
15
+ 'تسعة عشر',
16
+ ];
17
+
18
+ arabicFeminineOnes = [
19
+ '', 'إحدى', 'اثنتان', 'ثلاث', 'أربع', 'خمس', 'ست', 'سبع', 'ثمان',
20
+ 'تسع',
21
+ 'عشر', 'إحدى عشرة', 'اثنتا عشرة', 'ثلاث عشرة', 'أربع عشرة',
22
+ 'خمس عشرة', 'ست عشرة', 'سبع عشرة', 'ثماني عشرة',
23
+ 'تسع عشرة',
24
+ ];
25
+
26
+ arabicTens = ['عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون'];
27
+
28
+ arabicHundreds = ['', 'مائة', 'مئتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة', 'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة'];
29
+
30
+ arabicAppendedTwos = ['مئتا', 'ألفا', 'مليونا', 'مليارا', 'تريليونا', 'كوادريليونا', 'كوينتليونا', 'سكستيليونا'];
31
+
32
+ arabicTwos = ['مئتان', 'ألفان', 'مليونان', 'ملياران', 'تريليونان', 'كوادريليونان', 'كوينتليونان', 'سكستيليونان'];
33
+
34
+ arabicGroup = ['مائة', 'ألف', 'مليون', 'مليار', 'تريليون', 'كوادريليون', 'كوينتليون', 'سكستيليون'];
35
+
36
+ arabicAppendedGroup = ['', 'ألفاً', 'مليوناً', 'ملياراً', 'تريليوناً', 'كوادريليوناً', 'كوينتليوناً', 'سكستيليوناً'];
37
+
38
+ arabicPluralGroups = ['', 'آلاف', 'ملايين', 'مليارات', 'تريليونات', 'كوادريليونات', 'كوينتليونات', 'سكستيليونات'];
39
+
40
+ // isCurrencyPartNameFeminine = true
41
+
42
+ // isCurrencyNameFeminine = false
43
+
44
+ constructor(options) {
45
+ super(Object.assign({
46
+ negativeWord: 'ناقص',
47
+ separatorWord: 'فاصلة',
48
+ zero: 'صفر'
49
+ }, options));
36
50
  }
37
51
 
38
52
  digitFeminineStatus(digit/* , groupLevel */) {
@@ -93,7 +107,10 @@ export class N2WordsAR extends N2WordsAbs {
93
107
  }
94
108
 
95
109
  toCardinal(number) {
96
- if (parseInt(number) == 0) {
110
+ /** @todo Convert class to work with BigInt */
111
+ number = Number(number);
112
+
113
+ if (number == 0) {
97
114
  return this.zero;
98
115
  }
99
116
  let tempNumber = number;
@@ -133,6 +150,13 @@ export class N2WordsAR extends N2WordsAbs {
133
150
  }
134
151
  }
135
152
 
136
- export default function(n) {
137
- return new N2WordsAR().floatToCardinal(n);
153
+ /**
154
+ * Converts a value to cardinal (written) form.
155
+ * @param {number|string} value Number to be convert.
156
+ * @param {object} options Options for class.
157
+ * @throws {Error} Value cannot be invalid.
158
+ * @returns {string} Value in cardinal (written) format.
159
+ */
160
+ export default function(value, options) {
161
+ return new Arabic(options).floatToCardinal(value);
138
162
  }
package/lib/i18n/AZ.mjs CHANGED
@@ -1,40 +1,39 @@
1
- import N2WordsBase from '../classes/N2WordsBase.mjs';
1
+ import BaseLanguage from '../classes/BaseLanguage.mjs';
2
2
 
3
- export class N2WordsAZ extends N2WordsBase {
4
- constructor() {
5
- super();
6
-
7
- this.negativeWord = 'mənfi';
8
- this.separatorWord = 'nöqtə';
9
- this.zero = 'sıfır';
10
- this.cards = [
11
- { '1000000000000000000': 'kentilyon' },
12
- { '1000000000000000': 'katrilyon' },
13
- { '1000000000000': 'trilyon' },
14
- { '1000000000': 'milyar' },
15
- { '1000000': 'milyon' },
16
- { '1000': 'min' },
17
- { '100': 'yüz' },
18
- { '90': 'doxsan' },
19
- { '80': 'səksən' },
20
- { '70': 'yetmiş' },
21
- { '60': 'altmış' },
22
- { '50': 'əlli' },
23
- { '40': 'qırx' },
24
- { '30': 'otuz' },
25
- { '20': 'iyirmi' },
26
- { '10': 'on' },
27
- { '9': 'doqquz' },
28
- { '8': 'səkkiz' },
29
- { '7': 'yeddi' },
30
- { '6': 'altı' },
31
- { '5': 'beş' },
32
- { '4': 'dörd' },
33
- { '3': 'üç' },
34
- { '2': 'iki' },
35
- { '1': 'bir' },
36
- { '0': 'sıfır' }
37
- ];
3
+ export class N2WordsAZ extends BaseLanguage {
4
+ constructor(options) {
5
+ super(Object.assign({
6
+ negativeWord: 'mənfi',
7
+ separatorWord: 'nöqtə',
8
+ zero: 'sıfır'
9
+ }, options), [
10
+ [1000000000000000000n, 'kentilyon'],
11
+ [1000000000000000n, 'katrilyon'],
12
+ [1000000000000n, 'trilyon'],
13
+ [1000000000n, 'milyar'],
14
+ [1000000n, 'milyon'],
15
+ [1000n, 'min'],
16
+ [100n, 'yüz'],
17
+ [90n, 'doxsan'],
18
+ [80n, 'səksən'],
19
+ [70n, 'yetmiş'],
20
+ [60n, 'altmış'],
21
+ [50n, 'əlli'],
22
+ [40n, 'qırx'],
23
+ [30n, 'otuz'],
24
+ [20n, 'iyirmi'],
25
+ [10n, 'on'],
26
+ [9n, 'doqquz'],
27
+ [8n, 'səkkiz'],
28
+ [7n, 'yeddi'],
29
+ [6n, 'altı'],
30
+ [5n, 'beş'],
31
+ [4n, 'dörd'],
32
+ [3n, 'üç'],
33
+ [2n, 'iki'],
34
+ [1n, 'bir'],
35
+ [0n, 'sıfır']
36
+ ]);
38
37
  }
39
38
 
40
39
  merge(lPair, rPair) {
@@ -52,6 +51,13 @@ export class N2WordsAZ extends N2WordsBase {
52
51
  }
53
52
  }
54
53
 
55
- export default function(n) {
56
- return new N2WordsAZ().floatToCardinal(n);
54
+ /**
55
+ * Converts a value to cardinal (written) form.
56
+ * @param {number|string} value Number to be convert.
57
+ * @param {object} options Options for class.
58
+ * @throws {Error} Value cannot be invalid.
59
+ * @returns {string} Value in cardinal (written) format.
60
+ */
61
+ export default function(value, options) {
62
+ return new N2WordsAZ(options).floatToCardinal(value);
57
63
  }