Procedurally generates a radio weather report
1import path from 'path';
2
3interface Voice {
4 directory: string;
5 extension: string;
6}
7
8type Voices = { [name: string]: Voice };
9
10const LINES = {
11 NEGATIVE: 'negative',
12 POINT: 'point',
13 ZERO: 'zero',
14 ONE: 'one',
15 TWO: 'two',
16 THREE: 'three',
17 FOUR: 'four',
18 FIVE: 'five',
19 SIX: 'six',
20 SEVEN: 'seven',
21 EIGHT: 'eight',
22 NINE: 'nine',
23 TEN: 'ten',
24 ELEVEN: 'eleven',
25 TWELVE: 'twelve',
26 THIRTEEN: 'thirteen',
27 FIFTEEN: 'fifteen',
28 TEEN: 'teen',
29 TWENTY: 'twenty',
30 THIRTY: 'thirty',
31 FORTY: 'forty',
32 FIFTY: 'fifty',
33 SIXTY: 'sixty',
34 SEVENTY: 'seventy',
35 EIGHTY: 'eighty',
36 NINETY: 'ninety',
37 HUNDRED: 'hundred',
38 THOUSAND: 'thousand',
39 MILLION: 'million',
40 BILLION: 'billion',
41 TRILLION: 'trillion'
42}
43
44function formatNumber(num: number): string {
45 const parts = num.toString().split(".");
46 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
47 return parts.join(".");
48}
49
50function digitByDigit(str: string): string[] {
51 const tokens: string[] = [];
52 const input = str.replaceAll(',', '');
53 const map = [
54 LINES.ZERO,
55 LINES.ONE,
56 LINES.TWO,
57 LINES.THREE,
58 LINES.FOUR,
59 LINES.FIVE,
60 LINES.SIX,
61 LINES.SEVEN,
62 LINES.EIGHT,
63 LINES.NINE
64 ]
65 for (let i = 0; i < input.length; i++) {
66 tokens.push(map[parseInt(input.charAt(i))]);
67 }
68 return tokens;
69}
70
71function tens(str: string): string[] {
72 if (str === '0') {
73 return [LINES.ZERO];
74 }
75 const tokens: string[] = [];
76 const num = parseInt(str);
77 const map = {
78 '2': LINES.TWENTY,
79 '3': LINES.THIRTY,
80 '4': LINES.FORTY,
81 '5': LINES.FIFTY,
82 '6': LINES.SIXTY,
83 '7': LINES.SEVENTY,
84 '8': LINES.EIGHTY,
85 '9': LINES.NINETY
86 };
87 const ones = str.charAt(str.length - 1);
88 if (num === 0) {
89 return [];
90 }
91 else if (num >= 20) {
92 tokens.push(map[str.charAt(0)]);
93 if (ones !== '0') {
94 tokens.push(...digitByDigit(ones));
95 }
96 }
97 else if (num < 10) {
98 tokens.push(...digitByDigit(ones));
99 }
100 else {
101 const weirdoNumberMap = [
102 [LINES.TEN],
103 [LINES.ELEVEN],
104 [LINES.TWELVE],
105 [LINES.THIRTEEN],
106 [LINES.FOUR, LINES.TEEN],
107 [LINES.FIFTEEN],
108 [LINES.SIX, LINES.TEEN],
109 [LINES.SEVEN, LINES.TEEN],
110 [LINES.EIGHT, LINES.TEEN],
111 [LINES.NINE, LINES.TEEN],
112 ]
113 tokens.push(...weirdoNumberMap[num - 10]);
114 }
115 return tokens;
116}
117
118function hundreds(str: string): string[] {
119 const tokens: string[] = [];
120 if (str.length === 3 && str.charAt(0) !== '0') {
121 tokens.push(...digitByDigit(str.charAt(0)));
122 tokens.push(LINES.HUNDRED);
123 str = str.substring(1);
124 }
125 tokens.push(...tens(str));
126 return tokens;
127}
128
129function integer(str: string): string[] {
130 const tokens: string[] = [];
131 const numGroups = str.split(',');
132 const seperators = [LINES.TRILLION, LINES.BILLION, LINES.MILLION, LINES.THOUSAND];
133 seperators.splice(0, seperators.length - numGroups.length + 1);
134 numGroups.forEach(g => {
135 if (g !== '000') {
136 tokens.push(...hundreds(g));
137 }
138 if (seperators.length === 0) {
139 return;
140 }
141 const sep = seperators.shift();
142 if (g !== '000') {
143 tokens.push(sep);
144 }
145 });
146 return tokens;
147}
148
149function voiceLines(voice: Voice, num: number): string[] {
150 if (Math.abs(num) > 999999999999999 || isNaN(num)) {
151 return [];
152 }
153 const tokens: string[] = [];
154 const str = formatNumber(num);
155 const parts = str.split('.');
156 if (parts[0].startsWith('-')) {
157 tokens.push(LINES.NEGATIVE);
158 parts[0] = parts[0].substring(1);
159 }
160 tokens.push(...integer(parts[0]));
161 if (parts.length > 1) {
162 tokens.push(LINES.POINT);
163 tokens.push(...digitByDigit(parts[1]));
164 }
165 return tokens.map(l => path.join(voice.directory, `${l}${voice.extension.length > 0 && voice.extension.charAt(0) !== '.' ? '.' : ''}${voice.extension}`));
166}
167
168export default voiceLines;
169export { voiceLines, LINES };
170export type { Voice, Voices };