tanisa 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rija Nifaliana
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # tanisa
2
+ An utility to convert Malagasy 🇲🇬 numbers, including decimals, into their word representations.
@@ -0,0 +1,8 @@
1
+ export declare class MalagasyNumberToWords {
2
+ toWords(number: number | string): string;
3
+ private convertInteger;
4
+ private formatLargeNumber;
5
+ private convertBelowThousand;
6
+ private convertBelowHundred;
7
+ }
8
+ //# sourceMappingURL=converter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../../src/converter.ts"],"names":[],"mappings":"AAEA,qBAAa,qBAAqB;IACzB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM;IA6C/C,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,oBAAoB;IAyB5B,OAAO,CAAC,mBAAmB;CAmB5B"}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MalagasyNumberToWords = void 0;
4
+ const _1 = require(".");
5
+ class MalagasyNumberToWords {
6
+ toWords(number) {
7
+ const numStr = String(number);
8
+ const [integerPartStr, decimalPartStr] = numStr.split('.');
9
+ const integerPartNum = parseInt(integerPartStr || '0', 10);
10
+ if (isNaN(integerPartNum) ||
11
+ (decimalPartStr && isNaN(parseInt(decimalPartStr, 10)))) {
12
+ throw new TypeError(`Invalid number input: "${number}"`);
13
+ }
14
+ if (numStr.trim().startsWith('-') && integerPartNum !== 0) {
15
+ throw new RangeError('Negative numbers are not supported.');
16
+ }
17
+ if (integerPartNum >= _1.MalagasyNumerals.MAX_SUPPORTED_INTEGER ||
18
+ integerPartStr == _1.MalagasyNumerals.MAX_SUPPORTED_INTEGER.toString()) {
19
+ throw new RangeError(`Number ${integerPartNum} exceeds the maximum supported value (${_1.MalagasyNumerals.MAX_SUPPORTED_INTEGER}).`);
20
+ }
21
+ const integerWords = this.convertInteger(integerPartNum);
22
+ let decimalWords = '';
23
+ if (decimalPartStr && decimalPartStr.length > 0) {
24
+ for (let i = 0; i < decimalPartStr.length; i++) {
25
+ const digit = decimalPartStr[i];
26
+ if (digit === '0') {
27
+ decimalWords += _1.MalagasyNumerals.GLUE_DECIMAL_ZERO;
28
+ }
29
+ else {
30
+ const remainingDecimal = decimalPartStr.substring(i);
31
+ decimalWords += this.convertInteger(parseInt(remainingDecimal, 10));
32
+ break;
33
+ }
34
+ }
35
+ return integerWords + _1.MalagasyNumerals.GLUE_FAINGO + decimalWords;
36
+ }
37
+ else {
38
+ return integerWords;
39
+ }
40
+ }
41
+ convertInteger(num) {
42
+ if (num === 0) {
43
+ return _1.MalagasyNumerals.ZERO;
44
+ }
45
+ for (const unit of _1.MalagasyNumerals.LARGE_NUMBER_UNITS) {
46
+ if (num >= unit.threshold) {
47
+ return this.formatLargeNumber(num, unit);
48
+ }
49
+ }
50
+ return this.convertBelowThousand(num);
51
+ }
52
+ formatLargeNumber(num, unit) {
53
+ const multiple = Math.floor(num / unit.threshold);
54
+ const remainder = num % unit.threshold;
55
+ let prefix = '';
56
+ if (multiple > 1) {
57
+ prefix = this.convertInteger(multiple) + ' ';
58
+ }
59
+ else if (multiple === 1 && unit.threshold > 1000) {
60
+ prefix = _1.MalagasyNumerals.DIGITS[1] + ' ';
61
+ }
62
+ const basePart = prefix + unit.name;
63
+ if (remainder > 0) {
64
+ const remainderText = this.convertInteger(remainder);
65
+ return remainderText + _1.MalagasyNumerals.GLUE_SY + basePart;
66
+ }
67
+ else {
68
+ return basePart;
69
+ }
70
+ }
71
+ convertBelowThousand(num) {
72
+ if (num >= 100) {
73
+ const hundredMultiple = Math.floor(num / 100);
74
+ const remainder = num % 100;
75
+ const hundredWord = _1.MalagasyNumerals.HUNDREDS[hundredMultiple];
76
+ if (remainder === 0) {
77
+ return hundredWord;
78
+ }
79
+ else {
80
+ const remainderWords = this.convertBelowHundred(remainder);
81
+ let glue = _1.MalagasyNumerals.GLUE_AMBY;
82
+ if (hundredMultiple >= 2 && remainder >= 10) {
83
+ glue = _1.MalagasyNumerals.GLUE_SY;
84
+ }
85
+ const finalRemainderWords = remainder === 1 ? _1.MalagasyNumerals.CUSTOM_ONE : remainderWords;
86
+ return finalRemainderWords + glue + hundredWord;
87
+ }
88
+ }
89
+ return this.convertBelowHundred(num);
90
+ }
91
+ convertBelowHundred(num) {
92
+ if (num >= 10) {
93
+ const tenMultiple = Math.floor(num / 10);
94
+ const remainder = num % 10;
95
+ const tenWord = _1.MalagasyNumerals.TENS[tenMultiple];
96
+ if (remainder === 0) {
97
+ return tenWord;
98
+ }
99
+ else {
100
+ const digitWord = remainder === 1
101
+ ? _1.MalagasyNumerals.CUSTOM_ONE
102
+ : _1.MalagasyNumerals.DIGITS[remainder];
103
+ return digitWord + _1.MalagasyNumerals.GLUE_AMBY + tenWord;
104
+ }
105
+ }
106
+ return _1.MalagasyNumerals.DIGITS[num];
107
+ }
108
+ }
109
+ exports.MalagasyNumberToWords = MalagasyNumberToWords;
@@ -0,0 +1,63 @@
1
+ export declare const MalagasyNumerals: {
2
+ readonly GLUE_SY: " sy ";
3
+ readonly GLUE_AMBY: " amby ";
4
+ readonly GLUE_FAINGO: " faingo ";
5
+ readonly GLUE_DECIMAL_ZERO: "aotra ";
6
+ readonly CUSTOM_ONE: "iraika";
7
+ readonly ZERO: "aotra";
8
+ readonly DIGITS: readonly ["", "iray", "roa", "telo", "efatra", "dimy", "enina", "fito", "valo", "sivy"];
9
+ readonly TENS: readonly ["", "folo", "roapolo", "telopolo", "efapolo", "dimampolo", "enimpolo", "fitopolo", "valopolo", "sivifolo"];
10
+ readonly HUNDREDS: readonly ["", "zato", "roanjato", "telonjato", "efajato", "dimanjato", "enin-jato", "fitonjato", "valonjato", "sivinjato"];
11
+ readonly LARGE_NUMBER_UNITS: readonly [{
12
+ readonly threshold: 1000000000000000000;
13
+ readonly name: "tsipesimpesinafaharoa";
14
+ }, {
15
+ readonly threshold: 100000000000000000;
16
+ readonly name: "alinkisafaharoa";
17
+ }, {
18
+ readonly threshold: 10000000000000000;
19
+ readonly name: "lavitrisafaharoa";
20
+ }, {
21
+ readonly threshold: 1000000000000000;
22
+ readonly name: "tsitamboisafaharoa";
23
+ }, {
24
+ readonly threshold: 100000000000000;
25
+ readonly name: "safatsiroafaharoa";
26
+ }, {
27
+ readonly threshold: 10000000000000;
28
+ readonly name: "tsitanoanoa";
29
+ }, {
30
+ readonly threshold: 1000000000000;
31
+ readonly name: "tsitokotsiforohana";
32
+ }, {
33
+ readonly threshold: 100000000000;
34
+ readonly name: "tsipesimpesina";
35
+ }, {
36
+ readonly threshold: 10000000000;
37
+ readonly name: "alinkisa";
38
+ }, {
39
+ readonly threshold: 1000000000;
40
+ readonly name: "lavitrisa";
41
+ }, {
42
+ readonly threshold: 100000000;
43
+ readonly name: "tsitamboisa";
44
+ }, {
45
+ readonly threshold: 10000000;
46
+ readonly name: "safatsiroa";
47
+ }, {
48
+ readonly threshold: 1000000;
49
+ readonly name: "tapitrisa";
50
+ }, {
51
+ readonly threshold: 100000;
52
+ readonly name: "hetsy";
53
+ }, {
54
+ readonly threshold: 10000;
55
+ readonly name: "alina";
56
+ }, {
57
+ readonly threshold: 1000;
58
+ readonly name: "arivo";
59
+ }];
60
+ readonly MAX_SUPPORTED_INTEGER: number;
61
+ };
62
+ export type LargeNumberUnit = (typeof MalagasyNumerals.LARGE_NUMBER_UNITS)[number];
63
+ //# sourceMappingURL=dictionary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dictionary.d.ts","sourceRoot":"","sources":["../../src/dictionary.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqEnB,CAAA;AAEV,MAAM,MAAM,eAAe,GACzB,CAAC,OAAO,gBAAgB,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAA"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MalagasyNumerals = void 0;
4
+ exports.MalagasyNumerals = {
5
+ GLUE_SY: ' sy ',
6
+ GLUE_AMBY: ' amby ',
7
+ GLUE_FAINGO: ' faingo ',
8
+ GLUE_DECIMAL_ZERO: 'aotra ',
9
+ CUSTOM_ONE: 'iraika',
10
+ ZERO: 'aotra',
11
+ DIGITS: [
12
+ '',
13
+ 'iray',
14
+ 'roa',
15
+ 'telo',
16
+ 'efatra',
17
+ 'dimy',
18
+ 'enina',
19
+ 'fito',
20
+ 'valo',
21
+ 'sivy',
22
+ ],
23
+ TENS: [
24
+ '',
25
+ 'folo',
26
+ 'roapolo',
27
+ 'telopolo',
28
+ 'efapolo',
29
+ 'dimampolo',
30
+ 'enimpolo',
31
+ 'fitopolo',
32
+ 'valopolo',
33
+ 'sivifolo',
34
+ ],
35
+ HUNDREDS: [
36
+ '',
37
+ 'zato',
38
+ 'roanjato',
39
+ 'telonjato',
40
+ 'efajato',
41
+ 'dimanjato',
42
+ 'enin-jato',
43
+ 'fitonjato',
44
+ 'valonjato',
45
+ 'sivinjato',
46
+ ],
47
+ LARGE_NUMBER_UNITS: [
48
+ { threshold: 1000000000000000000, name: 'tsipesimpesinafaharoa' },
49
+ { threshold: 100000000000000000, name: 'alinkisafaharoa' },
50
+ { threshold: 10000000000000000, name: 'lavitrisafaharoa' },
51
+ { threshold: 1000000000000000, name: 'tsitamboisafaharoa' },
52
+ { threshold: 100000000000000, name: 'safatsiroafaharoa' },
53
+ { threshold: 10000000000000, name: 'tsitanoanoa' },
54
+ { threshold: 1000000000000, name: 'tsitokotsiforohana' },
55
+ { threshold: 100000000000, name: 'tsipesimpesina' },
56
+ { threshold: 10000000000, name: 'alinkisa' },
57
+ { threshold: 1000000000, name: 'lavitrisa' },
58
+ { threshold: 100000000, name: 'tsitamboisa' },
59
+ { threshold: 10000000, name: 'safatsiroa' },
60
+ { threshold: 1000000, name: 'tapitrisa' },
61
+ { threshold: 100000, name: 'hetsy' },
62
+ { threshold: 10000, name: 'alina' },
63
+ { threshold: 1000, name: 'arivo' },
64
+ ],
65
+ MAX_SUPPORTED_INTEGER: 1000000000000000000 * 1000,
66
+ };
@@ -0,0 +1,3 @@
1
+ export { MalagasyNumberToWords } from './converter';
2
+ export { MalagasyNumerals, LargeNumberUnit } from './dictionary';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MalagasyNumerals = exports.MalagasyNumberToWords = void 0;
4
+ var converter_1 = require("./converter");
5
+ Object.defineProperty(exports, "MalagasyNumberToWords", { enumerable: true, get: function () { return converter_1.MalagasyNumberToWords; } });
6
+ var dictionary_1 = require("./dictionary");
7
+ Object.defineProperty(exports, "MalagasyNumerals", { enumerable: true, get: function () { return dictionary_1.MalagasyNumerals; } });
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const src_1 = require("../src");
5
+ (0, vitest_1.describe)('MalagasyNumberToWords', () => {
6
+ let converter;
7
+ (0, vitest_1.beforeEach)(() => {
8
+ converter = new src_1.MalagasyNumberToWords();
9
+ });
10
+ const testCases = [
11
+ [0, 'aotra'],
12
+ [1, 'iray'],
13
+ [5, 'dimy'],
14
+ [9, 'sivy'],
15
+ [10, 'folo'],
16
+ [11, 'iraika amby folo'],
17
+ [15, 'dimy amby folo'],
18
+ [20, 'roapolo'],
19
+ [21, 'iraika amby roapolo'],
20
+ [35, 'dimy amby telopolo'],
21
+ [99, 'sivy amby sivifolo'],
22
+ [100, 'zato'],
23
+ [101, 'iraika amby zato'],
24
+ [110, 'folo amby zato'],
25
+ [111, 'iraika amby folo amby zato'],
26
+ [121, 'iraika amby roapolo amby zato'],
27
+ [200, 'roanjato'],
28
+ [201, 'iraika amby roanjato'],
29
+ [210, 'folo sy roanjato'],
30
+ [225, 'dimy amby roapolo sy roanjato'],
31
+ [600, 'enin-jato'],
32
+ [999, 'sivy amby sivifolo sy sivinjato'],
33
+ [1000, 'arivo'],
34
+ [1001, 'iray sy arivo'],
35
+ [1010, 'folo sy arivo'],
36
+ [1011, 'iraika amby folo sy arivo'],
37
+ [1378, 'valo amby fitopolo sy telonjato sy arivo'],
38
+ [2000, 'roa arivo'],
39
+ [2023, 'telo amby roapolo sy roa arivo'],
40
+ [10000, 'iray alina'],
41
+ [15000, 'dimy arivo sy iray alina'],
42
+ [20000, 'roa alina'],
43
+ [98765, 'dimy amby enimpolo sy fitonjato sy valo arivo sy sivy alina'],
44
+ [100000, 'iray hetsy'],
45
+ [
46
+ 123456,
47
+ 'enina amby dimampolo sy efajato sy telo arivo sy roa alina sy iray hetsy',
48
+ ],
49
+ [500000, 'dimy hetsy'],
50
+ [
51
+ 654321,
52
+ 'iraika amby roapolo sy telonjato sy efatra arivo sy dimy alina sy enina hetsy',
53
+ ],
54
+ [1000000, 'iray tapitrisa'],
55
+ [1000001, 'iray sy iray tapitrisa'],
56
+ [2500000, 'dimy hetsy sy roa tapitrisa'],
57
+ [
58
+ 9876543,
59
+ 'telo amby efapolo sy dimanjato sy enina arivo sy fito alina sy valo hetsy sy sivy tapitrisa',
60
+ ],
61
+ [10000000, 'iray safatsiroa'],
62
+ [100000000, 'iray tsitamboisa'],
63
+ [1000000000, 'iray lavitrisa'],
64
+ [
65
+ 12345678901,
66
+ 'iraika amby sivinjato sy valo arivo sy fito alina sy enina hetsy sy dimy tapitrisa sy efatra safatsiroa sy telo tsitamboisa sy roa lavitrisa sy iray alinkisa',
67
+ ],
68
+ [1100000000000, 'iray tsipesimpesina sy iray tsitokotsiforohana'],
69
+ [0.5, 'aotra faingo dimy'],
70
+ [0.1, 'aotra faingo iray'],
71
+ ['2.00100', 'roa faingo aotra aotra zato'],
72
+ [
73
+ 1378.23,
74
+ 'valo amby fitopolo sy telonjato sy arivo faingo telo amby roapolo',
75
+ ],
76
+ [100.01, 'zato faingo aotra iray'],
77
+ [0.005, 'aotra faingo aotra aotra dimy'],
78
+ ];
79
+ vitest_1.it.each(testCases)('should convert %s to "%s"', (input, expected) => {
80
+ (0, vitest_1.expect)(converter.toWords(input)).toBe(expected);
81
+ });
82
+ (0, vitest_1.describe)('Error Handling', () => {
83
+ (0, vitest_1.it)('should throw TypeError for invalid input', () => {
84
+ (0, vitest_1.expect)(() => converter.toWords('not a number')).toThrow(TypeError);
85
+ (0, vitest_1.expect)(() => converter.toWords('123.abc')).toThrow(TypeError);
86
+ (0, vitest_1.expect)(() => converter.toWords(NaN)).toThrow(TypeError);
87
+ });
88
+ (0, vitest_1.it)('should throw RangeError for negative numbers', () => {
89
+ (0, vitest_1.expect)(() => converter.toWords(-1)).toThrow(RangeError);
90
+ (0, vitest_1.expect)(() => converter.toWords(-100.5)).toThrow(RangeError);
91
+ (0, vitest_1.expect)(() => converter.toWords('-0')).not.toThrow(RangeError);
92
+ (0, vitest_1.expect)(converter.toWords('-0')).toBe('aotra');
93
+ });
94
+ (0, vitest_1.it)('should throw RangeError for numbers exceeding the limit', () => {
95
+ const limit = src_1.MalagasyNumerals.MAX_SUPPORTED_INTEGER;
96
+ const largeNumber = BigInt(limit) + BigInt(1);
97
+ (0, vitest_1.expect)(() => converter.toWords(limit)).toThrow(RangeError);
98
+ (0, vitest_1.expect)(() => converter.toWords(largeNumber.toString())).toThrow(RangeError);
99
+ });
100
+ (0, vitest_1.it)('should handle string input correctly', () => {
101
+ (0, vitest_1.expect)(converter.toWords('123')).toBe('telo amby roapolo amby zato');
102
+ (0, vitest_1.expect)(converter.toWords('1000.5')).toBe('arivo faingo dimy');
103
+ });
104
+ (0, vitest_1.it)('should handle number input with maximum safe integer', () => {
105
+ const safeInt = Number.MAX_SAFE_INTEGER;
106
+ (0, vitest_1.expect)(() => converter.toWords(safeInt)).not.toThrow();
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=converter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converter.test.d.ts","sourceRoot":"","sources":["../../tests/converter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const src_1 = require("../src");
5
+ (0, vitest_1.describe)('MalagasyNumberToWords', () => {
6
+ let converter;
7
+ (0, vitest_1.beforeEach)(() => {
8
+ converter = new src_1.MalagasyNumberToWords();
9
+ });
10
+ const testCases = [
11
+ [0, 'aotra'],
12
+ [1, 'iray'],
13
+ [5, 'dimy'],
14
+ [9, 'sivy'],
15
+ [10, 'folo'],
16
+ [11, 'iraika amby folo'],
17
+ [15, 'dimy amby folo'],
18
+ [20, 'roapolo'],
19
+ [21, 'iraika amby roapolo'],
20
+ [35, 'dimy amby telopolo'],
21
+ [99, 'sivy amby sivifolo'],
22
+ [100, 'zato'],
23
+ [101, 'iraika amby zato'],
24
+ [110, 'folo amby zato'],
25
+ [111, 'iraika amby folo amby zato'],
26
+ [121, 'iraika amby roapolo amby zato'],
27
+ [200, 'roanjato'],
28
+ [201, 'iraika amby roanjato'],
29
+ [210, 'folo sy roanjato'],
30
+ [225, 'dimy amby roapolo sy roanjato'],
31
+ [600, 'enin-jato'],
32
+ [999, 'sivy amby sivifolo sy sivinjato'],
33
+ [1000, 'arivo'],
34
+ [1001, 'iray sy arivo'],
35
+ [1010, 'folo sy arivo'],
36
+ [1011, 'iraika amby folo sy arivo'],
37
+ [1378, 'valo amby fitopolo sy telonjato sy arivo'],
38
+ [2000, 'roa arivo'],
39
+ [2023, 'telo amby roapolo sy roa arivo'],
40
+ [10000, 'iray alina'],
41
+ [15000, 'dimy arivo sy iray alina'],
42
+ [20000, 'roa alina'],
43
+ [98765, 'dimy amby enimpolo sy fitonjato sy valo arivo sy sivy alina'],
44
+ [100000, 'iray hetsy'],
45
+ [
46
+ 123456,
47
+ 'enina amby dimampolo sy efajato sy telo arivo sy roa alina sy iray hetsy',
48
+ ],
49
+ [500000, 'dimy hetsy'],
50
+ [
51
+ 654321,
52
+ 'iraika amby roapolo sy telonjato sy efatra arivo sy dimy alina sy enina hetsy',
53
+ ],
54
+ [1000000, 'iray tapitrisa'],
55
+ [1000001, 'iray sy iray tapitrisa'],
56
+ [2500000, 'dimy hetsy sy roa tapitrisa'],
57
+ [
58
+ 9876543,
59
+ 'telo amby efapolo sy dimanjato sy enina arivo sy fito alina sy valo hetsy sy sivy tapitrisa',
60
+ ],
61
+ [10000000, 'iray safatsiroa'],
62
+ [100000000, 'iray tsitamboisa'],
63
+ [1000000000, 'iray lavitrisa'],
64
+ [
65
+ 12345678901,
66
+ 'iraika amby sivinjato sy valo arivo sy fito alina sy enina hetsy sy dimy tapitrisa sy efatra safatsiroa sy telo tsitamboisa sy roa lavitrisa sy iray alinkisa',
67
+ ],
68
+ [1100000000000, 'iray tsipesimpesina sy iray tsitokotsiforohana'],
69
+ [0.5, 'aotra faingo dimy'],
70
+ [0.1, 'aotra faingo iray'],
71
+ ['2.00100', 'roa faingo aotra aotra zato'],
72
+ [
73
+ 1378.23,
74
+ 'valo amby fitopolo sy telonjato sy arivo faingo telo amby roapolo',
75
+ ],
76
+ [100.01, 'zato faingo aotra iray'],
77
+ [0.005, 'aotra faingo aotra aotra dimy'],
78
+ ];
79
+ vitest_1.it.each(testCases)('should convert %s to "%s"', (input, expected) => {
80
+ (0, vitest_1.expect)(converter.toWords(input)).toBe(expected);
81
+ });
82
+ (0, vitest_1.describe)('Error Handling', () => {
83
+ (0, vitest_1.it)('should throw TypeError for invalid input', () => {
84
+ (0, vitest_1.expect)(() => converter.toWords('not a number')).toThrow(TypeError);
85
+ (0, vitest_1.expect)(() => converter.toWords('123.abc')).toThrow(TypeError);
86
+ (0, vitest_1.expect)(() => converter.toWords(NaN)).toThrow(TypeError);
87
+ });
88
+ (0, vitest_1.it)('should throw RangeError for negative numbers', () => {
89
+ (0, vitest_1.expect)(() => converter.toWords(-1)).toThrow(RangeError);
90
+ (0, vitest_1.expect)(() => converter.toWords(-100.5)).toThrow(RangeError);
91
+ (0, vitest_1.expect)(() => converter.toWords('-0')).not.toThrow(RangeError);
92
+ (0, vitest_1.expect)(converter.toWords('-0')).toBe('aotra');
93
+ });
94
+ (0, vitest_1.it)('should throw RangeError for numbers exceeding the limit', () => {
95
+ const limit = src_1.MalagasyNumerals.MAX_SUPPORTED_INTEGER;
96
+ const largeNumber = BigInt(limit) + BigInt(1);
97
+ (0, vitest_1.expect)(() => converter.toWords(limit)).toThrow(RangeError);
98
+ (0, vitest_1.expect)(() => converter.toWords(largeNumber.toString())).toThrow(RangeError);
99
+ });
100
+ (0, vitest_1.it)('should handle string input correctly', () => {
101
+ (0, vitest_1.expect)(converter.toWords('123')).toBe('telo amby roapolo amby zato');
102
+ (0, vitest_1.expect)(converter.toWords('1000.5')).toBe('arivo faingo dimy');
103
+ });
104
+ (0, vitest_1.it)('should handle number input with maximum safe integer', () => {
105
+ const safeInt = Number.MAX_SAFE_INTEGER;
106
+ (0, vitest_1.expect)(() => converter.toWords(safeInt)).not.toThrow();
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,3 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
3
+ //# sourceMappingURL=vitest.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":";AAEA,wBAIE"}
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vite_1 = require("vite");
4
+ exports.default = (0, vite_1.defineConfig)({
5
+ test: {
6
+ exclude: ['./dist', './node_modules'],
7
+ },
8
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "tanisa",
3
+ "version": "1.0.0",
4
+ "main": "src/converter.ts",
5
+ "description": "An utility to convert Malagasy 🇲🇬 numbers, including decimals, into their word representations.",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Rija Nifaliana",
9
+ "email": "rija.nifaliana@gmail.com"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "format": "prettier . --write",
18
+ "lint": "eslint",
19
+ "lint:fix": "eslint . --fix",
20
+ "build": "tsc",
21
+ "_postinstall": "husky",
22
+ "prepack": "pinst --disable",
23
+ "postpack": "pinst --enable",
24
+ "prepare": "husky"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.24.0",
28
+ "@types/node": "^22.14.1",
29
+ "eslint": "^9.24.0",
30
+ "globals": "^16.0.0",
31
+ "husky": "^9.1.7",
32
+ "lint-staged": "^15.5.1",
33
+ "pinst": "^3.0.0",
34
+ "prettier": "^3.5.3",
35
+ "ts-node": "^10.9.2",
36
+ "typescript": "^5.8.3",
37
+ "typescript-eslint": "^8.30.1",
38
+ "vitest": "^3.1.1"
39
+ },
40
+ "dependencies": {
41
+ "husky-init": "^8.0.0"
42
+ },
43
+ "keywords": [
44
+ "malagasy",
45
+ "number",
46
+ "words",
47
+ "converter",
48
+ "i18n",
49
+ "l10n",
50
+ "toWords",
51
+ "fanisana",
52
+ "typescript",
53
+ "javascript"
54
+ ]
55
+ }
@@ -0,0 +1,130 @@
1
+ import { LargeNumberUnit, MalagasyNumerals } from '.'
2
+
3
+ export class MalagasyNumberToWords {
4
+ public toWords(number: number | string): string {
5
+ const numStr = String(number)
6
+ const [integerPartStr, decimalPartStr] = numStr.split('.')
7
+ const integerPartNum = parseInt(integerPartStr || '0', 10)
8
+
9
+ if (
10
+ isNaN(integerPartNum) ||
11
+ (decimalPartStr && isNaN(parseInt(decimalPartStr, 10)))
12
+ ) {
13
+ throw new TypeError(`Invalid number input: "${number}"`)
14
+ }
15
+
16
+ if (numStr.trim().startsWith('-') && integerPartNum !== 0) {
17
+ throw new RangeError('Negative numbers are not supported.')
18
+ }
19
+
20
+ if (
21
+ integerPartNum >= MalagasyNumerals.MAX_SUPPORTED_INTEGER ||
22
+ integerPartStr == MalagasyNumerals.MAX_SUPPORTED_INTEGER.toString()
23
+ ) {
24
+ throw new RangeError(
25
+ `Number ${integerPartNum} exceeds the maximum supported value (${MalagasyNumerals.MAX_SUPPORTED_INTEGER}).`
26
+ )
27
+ }
28
+
29
+ const integerWords = this.convertInteger(integerPartNum)
30
+
31
+ let decimalWords = ''
32
+ if (decimalPartStr && decimalPartStr.length > 0) {
33
+ for (let i = 0; i < decimalPartStr.length; i++) {
34
+ const digit = decimalPartStr[i]
35
+ if (digit === '0') {
36
+ decimalWords += MalagasyNumerals.GLUE_DECIMAL_ZERO
37
+ } else {
38
+ const remainingDecimal = decimalPartStr.substring(i)
39
+ decimalWords += this.convertInteger(parseInt(remainingDecimal, 10))
40
+ break
41
+ }
42
+ }
43
+ return integerWords + MalagasyNumerals.GLUE_FAINGO + decimalWords
44
+ } else {
45
+ return integerWords
46
+ }
47
+ }
48
+
49
+ private convertInteger(num: number): string {
50
+ if (num === 0) {
51
+ return MalagasyNumerals.ZERO
52
+ }
53
+
54
+ for (const unit of MalagasyNumerals.LARGE_NUMBER_UNITS) {
55
+ if (num >= unit.threshold) {
56
+ return this.formatLargeNumber(num, unit)
57
+ }
58
+ }
59
+
60
+ return this.convertBelowThousand(num)
61
+ }
62
+
63
+ private formatLargeNumber(num: number, unit: LargeNumberUnit): string {
64
+ const multiple = Math.floor(num / unit.threshold)
65
+ const remainder = num % unit.threshold
66
+
67
+ let prefix = ''
68
+ // Use prefix only if multiple is greater than 1 OR
69
+ // if multiple is 1 AND the unit is larger than 'arivo' (1000)
70
+ if (multiple > 1) {
71
+ prefix = this.convertInteger(multiple) + ' '
72
+ } else if (multiple === 1 && unit.threshold > 1000) {
73
+ prefix = MalagasyNumerals.DIGITS[1] + ' '
74
+ }
75
+ // If multiple is 1 and unit is 'arivo', no prefix is needed ("arivo", not "iray arivo")
76
+ const basePart = prefix + unit.name
77
+
78
+ if (remainder > 0) {
79
+ const remainderText = this.convertInteger(remainder)
80
+ return remainderText + MalagasyNumerals.GLUE_SY + basePart
81
+ } else {
82
+ return basePart
83
+ }
84
+ }
85
+
86
+ private convertBelowThousand(num: number): string {
87
+ // Assumes 0 < num < 1000
88
+ if (num >= 100) {
89
+ const hundredMultiple = Math.floor(num / 100)
90
+ const remainder = num % 100
91
+ const hundredWord = MalagasyNumerals.HUNDREDS[hundredMultiple]
92
+
93
+ if (remainder === 0) {
94
+ return hundredWord
95
+ } else {
96
+ const remainderWords = this.convertBelowHundred(remainder)
97
+ let glue = MalagasyNumerals.GLUE_AMBY as string
98
+ // Special rule: Use "sy" if hundred base >= 200 AND remainder >= 10
99
+ if (hundredMultiple >= 2 && remainder >= 10) {
100
+ glue = MalagasyNumerals.GLUE_SY
101
+ }
102
+ // Use "iraika" for remainder 1 when connecting
103
+ const finalRemainderWords =
104
+ remainder === 1 ? MalagasyNumerals.CUSTOM_ONE : remainderWords
105
+ return finalRemainderWords + glue + hundredWord
106
+ }
107
+ }
108
+ return this.convertBelowHundred(num)
109
+ }
110
+
111
+ private convertBelowHundred(num: number): string {
112
+ if (num >= 10) {
113
+ const tenMultiple = Math.floor(num / 10)
114
+ const remainder = num % 10
115
+ const tenWord = MalagasyNumerals.TENS[tenMultiple]
116
+
117
+ if (remainder === 0) {
118
+ return tenWord
119
+ } else {
120
+ // Always use "amby" for tens+digits
121
+ const digitWord =
122
+ remainder === 1
123
+ ? MalagasyNumerals.CUSTOM_ONE
124
+ : MalagasyNumerals.DIGITS[remainder]
125
+ return digitWord + MalagasyNumerals.GLUE_AMBY + tenWord
126
+ }
127
+ }
128
+ return MalagasyNumerals.DIGITS[num]
129
+ }
130
+ }