5
5
import java .util .ArrayList ;
6
6
import java .util .HashMap ;
7
7
import java .util .List ;
8
+ import java .util .Map ;
8
9
9
10
/**
10
11
A Java-based utility for converting English word representations of numbers
16
17
*/
17
18
18
19
public final class WordsToNumber {
20
+
19
21
private WordsToNumber () {
20
22
}
21
23
22
- private static final HashMap <String , Integer > NUMBER_MAP = new HashMap <>();
23
- private static final HashMap <String , BigDecimal > POWERS_OF_TEN = new HashMap <>();
24
+ private static final Map <String , Integer > NUMBER_MAP = new HashMap <>();
25
+ private static final Map <String , BigDecimal > POWERS_OF_TEN = new HashMap <>();
24
26
25
27
static {
26
28
NUMBER_MAP .put ("zero" , 0 );
@@ -60,9 +62,21 @@ private WordsToNumber() {
60
62
61
63
public static String convert (String numberInWords ) {
62
64
if (numberInWords == null ) {
63
- return "Null Input" ;
65
+ throw new WordsToNumberException ( WordsToNumberException . ErrorType . NULL_INPUT , "" ) ;
64
66
}
65
67
68
+ ArrayDeque <String > wordDeque = preprocessWords (numberInWords );
69
+ BigDecimal completeNumber = convertWordQueueToBigDecimal (wordDeque );
70
+
71
+ return completeNumber .toString ();
72
+ }
73
+
74
+ public static BigDecimal convertToBigDecimal (String numberInWords ) {
75
+ String conversionResult = convert (numberInWords );
76
+ return new BigDecimal (conversionResult );
77
+ }
78
+
79
+ private static ArrayDeque <String > preprocessWords (String numberInWords ) {
66
80
String [] wordSplitArray = numberInWords .trim ().split ("[ ,-]" );
67
81
ArrayDeque <String > wordDeque = new ArrayDeque <>();
68
82
for (String word : wordSplitArray ) {
@@ -71,52 +85,115 @@ public static String convert(String numberInWords) {
71
85
}
72
86
wordDeque .add (word .toLowerCase ());
73
87
}
88
+ if (wordDeque .isEmpty ()) {
89
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .NULL_INPUT , "" );
90
+ }
91
+ return wordDeque ;
92
+ }
74
93
75
- List <BigDecimal > chunks = new ArrayList <>();
94
+ private static void handleConjunction (boolean prevNumWasHundred , boolean prevNumWasPowerOfTen , ArrayDeque <String > wordDeque ) {
95
+ if (wordDeque .isEmpty ()) {
96
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .INVALID_CONJUNCTION , "" );
97
+ }
98
+
99
+ String nextWord = wordDeque .pollFirst ();
100
+ String afterNextWord = wordDeque .peekFirst ();
101
+
102
+ wordDeque .addFirst (nextWord );
103
+
104
+ Integer number = NUMBER_MAP .getOrDefault (nextWord , null );
105
+
106
+ boolean isPrevWordValid = prevNumWasHundred || prevNumWasPowerOfTen ;
107
+ boolean isNextWordValid = number != null && (number >= 10 || afterNextWord == null || "point" .equals (afterNextWord ));
108
+
109
+ if (!isPrevWordValid || !isNextWordValid ) {
110
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .INVALID_CONJUNCTION , "" );
111
+ }
112
+ }
113
+
114
+ private static BigDecimal handleHundred (BigDecimal currentChunk , String word , boolean prevNumWasPowerOfTen ) {
115
+ boolean currentChunkIsZero = currentChunk .compareTo (BigDecimal .ZERO ) == 0 ;
116
+ if (currentChunk .compareTo (BigDecimal .TEN ) >= 0 || prevNumWasPowerOfTen ) {
117
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .UNEXPECTED_WORD , word );
118
+ }
119
+ if (currentChunkIsZero ) {
120
+ currentChunk = currentChunk .add (BigDecimal .ONE );
121
+ }
122
+ return currentChunk .multiply (BigDecimal .valueOf (100 ));
123
+ }
124
+
125
+ private static void handlePowerOfTen (List <BigDecimal > chunks , BigDecimal currentChunk , BigDecimal powerOfTen , String word , boolean prevNumWasPowerOfTen ) {
126
+ boolean currentChunkIsZero = currentChunk .compareTo (BigDecimal .ZERO ) == 0 ;
127
+ if (currentChunkIsZero || prevNumWasPowerOfTen ) {
128
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .UNEXPECTED_WORD , word );
129
+ }
130
+ BigDecimal nextChunk = currentChunk .multiply (powerOfTen );
131
+
132
+ if (!(chunks .isEmpty () || isAdditionSafe (chunks .getLast (), nextChunk ))) {
133
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .UNEXPECTED_WORD , word );
134
+ }
135
+ chunks .add (nextChunk );
136
+ }
137
+
138
+ private static BigDecimal handleNumber (List <BigDecimal > chunks , BigDecimal currentChunk , String word , Integer number ) {
139
+ boolean currentChunkIsZero = currentChunk .compareTo (BigDecimal .ZERO ) == 0 ;
140
+ if (number == 0 && !(currentChunkIsZero && chunks .isEmpty ())) {
141
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .UNEXPECTED_WORD , word );
142
+ }
143
+ BigDecimal bigDecimalNumber = BigDecimal .valueOf (number );
144
+
145
+ if (!currentChunkIsZero && !isAdditionSafe (currentChunk , bigDecimalNumber )) {
146
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .UNEXPECTED_WORD , word );
147
+ }
148
+ return currentChunk .add (bigDecimalNumber );
149
+ }
150
+
151
+ private static void handlePoint (List <BigDecimal > chunks , BigDecimal currentChunk , ArrayDeque <String > wordDeque ) {
152
+ boolean currentChunkIsZero = currentChunk .compareTo (BigDecimal .ZERO ) == 0 ;
153
+ if (!currentChunkIsZero ) {
154
+ chunks .add (currentChunk );
155
+ }
156
+
157
+ String decimalPart = convertDecimalPart (wordDeque );
158
+ chunks .add (new BigDecimal (decimalPart ));
159
+ }
160
+
161
+ private static void handleNegative (boolean isNegative ) {
162
+ if (isNegative ) {
163
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .MULTIPLE_NEGATIVES , "" );
164
+ }
165
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .INVALID_NEGATIVE , "" );
166
+ }
167
+
168
+ private static BigDecimal convertWordQueueToBigDecimal (ArrayDeque <String > wordDeque ) {
76
169
BigDecimal currentChunk = BigDecimal .ZERO ;
170
+ List <BigDecimal > chunks = new ArrayList <>();
171
+
172
+ boolean isNegative = "negative" .equals (wordDeque .peek ());
173
+ if (isNegative ) wordDeque .poll ();
77
174
78
- boolean isNegative = false ;
79
175
boolean prevNumWasHundred = false ;
80
176
boolean prevNumWasPowerOfTen = false ;
81
177
82
- String errorMessage = null ;
83
-
84
- while (!wordDeque .isEmpty () && errorMessage == null ) {
178
+ while (!wordDeque .isEmpty ()) {
85
179
String word = wordDeque .poll ();
86
- boolean currentChunkIsZero = currentChunk .compareTo (BigDecimal .ZERO ) == 0 ;
87
180
88
- boolean isConjunction = word .equals ("and" );
89
- if (isConjunction && isValidConjunction (prevNumWasHundred , prevNumWasPowerOfTen , wordDeque )) {
90
- continue ;
91
- }
92
-
93
- if (word .equals ("hundred" )) {
94
- if (currentChunk .compareTo (BigDecimal .TEN ) >= 0 || prevNumWasPowerOfTen ) {
95
- errorMessage = "Invalid Input. Unexpected Word: " + word ;
181
+ switch (word ) {
182
+ case "and" -> {
183
+ handleConjunction (prevNumWasHundred , prevNumWasPowerOfTen , wordDeque );
96
184
continue ;
97
185
}
98
- if (currentChunkIsZero ) {
99
- currentChunk = currentChunk .add (BigDecimal .ONE );
186
+ case "hundred" -> {
187
+ currentChunk = handleHundred (currentChunk , word , prevNumWasPowerOfTen );
188
+ prevNumWasHundred = true ;
189
+ continue ;
100
190
}
101
- currentChunk = currentChunk .multiply (BigDecimal .valueOf (100 ));
102
- prevNumWasHundred = true ;
103
- continue ;
104
191
}
105
192
prevNumWasHundred = false ;
106
193
107
194
BigDecimal powerOfTen = POWERS_OF_TEN .getOrDefault (word , null );
108
195
if (powerOfTen != null ) {
109
- if (currentChunkIsZero || prevNumWasPowerOfTen ) {
110
- errorMessage = "Invalid Input. Unexpected Word: " + word ;
111
- continue ;
112
- }
113
- BigDecimal nextChunk = currentChunk .multiply (powerOfTen );
114
-
115
- if (!(chunks .isEmpty () || isAdditionSafe (chunks .getLast (), nextChunk ))) {
116
- errorMessage = "Invalid Input. Unexpected Word: " + word ;
117
- continue ;
118
- }
119
- chunks .add (nextChunk );
196
+ handlePowerOfTen (chunks , currentChunk , powerOfTen , word , prevNumWasPowerOfTen );
120
197
currentChunk = BigDecimal .ZERO ;
121
198
prevNumWasPowerOfTen = true ;
122
199
continue ;
@@ -125,120 +202,96 @@ public static String convert(String numberInWords) {
125
202
126
203
Integer number = NUMBER_MAP .getOrDefault (word , null );
127
204
if (number != null ) {
128
- if (number == 0 && !(currentChunkIsZero && chunks .isEmpty ())) {
129
- errorMessage = "Invalid Input. Unexpected Word: " + word ;
130
- continue ;
131
- }
132
- BigDecimal bigDecimalNumber = BigDecimal .valueOf (number );
133
-
134
- if (currentChunkIsZero || isAdditionSafe (currentChunk , bigDecimalNumber )) {
135
- currentChunk = currentChunk .add (bigDecimalNumber );
136
- } else {
137
- errorMessage = "Invalid Input. Unexpected Word: " + word ;
138
- }
205
+ currentChunk = handleNumber (chunks , currentChunk , word , number );
139
206
continue ;
140
207
}
141
208
142
- if (word .equals ("point" )) {
143
- if (!currentChunkIsZero ) {
144
- chunks .add (currentChunk );
145
- }
146
- currentChunk = BigDecimal .ZERO ;
147
-
148
- String decimalPart = convertDecimalPart (wordDeque );
149
- if (!decimalPart .startsWith ("I" )) {
150
- chunks .add (new BigDecimal (decimalPart ));
151
- } else {
152
- errorMessage = decimalPart ;
153
- }
154
- continue ;
155
- }
156
-
157
- if (word .equals ("negative" )) {
158
- if (isNegative ) {
159
- errorMessage = "Invalid Input. Multiple 'Negative's detected." ;
160
- } else {
161
- isNegative = chunks .isEmpty () && currentChunkIsZero ;
209
+ switch (word ) {
210
+ case "point" -> {
211
+ handlePoint (chunks , currentChunk , wordDeque );
212
+ currentChunk = BigDecimal .ZERO ;
213
+ continue ;
162
214
}
163
- continue ;
215
+ case "negative" -> handleNegative ( isNegative ) ;
164
216
}
165
217
166
- errorMessage = "Invalid Input. " + ( isConjunction ? "Unexpected 'and' placement" : "Unknown Word: " + word );
218
+ throw new WordsToNumberException ( WordsToNumberException . ErrorType . UNKNOWN_WORD , word );
167
219
}
168
220
169
- if (errorMessage != null ) {
170
- return errorMessage ;
171
- }
172
-
173
- if (!(currentChunk .compareTo (BigDecimal .ZERO ) == 0 )) {
221
+ if (currentChunk .compareTo (BigDecimal .ZERO ) != 0 ) {
174
222
chunks .add (currentChunk );
175
223
}
176
- BigDecimal completeNumber = combineChunks (chunks );
177
224
178
- return isNegative ? completeNumber .multiply (BigDecimal .valueOf (-1 )).toString () : completeNumber .toString ();
179
- }
180
-
181
- private static boolean isValidConjunction (boolean prevNumWasHundred , boolean prevNumWasPowerOfTen , ArrayDeque <String > wordDeque ) {
182
- if (wordDeque .isEmpty ()) {
183
- return false ;
184
- }
225
+ BigDecimal completeNumber = combineChunks (chunks );
226
+ return isNegative ? completeNumber .multiply (BigDecimal .valueOf (-1 )) :
227
+ completeNumber ;
228
+ }
185
229
186
- String nextWord = wordDeque .pollFirst ();
187
- String afterNextWord = wordDeque .peekFirst ();
230
+ private static boolean isAdditionSafe (BigDecimal currentChunk , BigDecimal number ) {
231
+ int chunkDigitCount = currentChunk .toString ().length ();
232
+ int numberDigitCount = number .toString ().length ();
233
+ return chunkDigitCount > numberDigitCount ;
234
+ }
188
235
189
- wordDeque .addFirst (nextWord );
236
+ private static String convertDecimalPart (ArrayDeque <String > wordDeque ) {
237
+ StringBuilder decimalPart = new StringBuilder ("." );
238
+
239
+ while (!wordDeque .isEmpty ()) {
240
+ String word = wordDeque .poll ();
241
+ Integer number = NUMBER_MAP .getOrDefault (word , null );
242
+ if (number == null ) {
243
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .UNEXPECTED_WORD_AFTER_POINT , word );
244
+ }
245
+ decimalPart .append (number );
246
+ }
247
+
248
+ boolean missingNumbers = decimalPart .length () == 1 ;
249
+ if (missingNumbers ) {
250
+ throw new WordsToNumberException (WordsToNumberException .ErrorType .MISSING_DECIMAL_NUMBERS , "" );
251
+ }
252
+ return decimalPart .toString ();
253
+ }
190
254
191
- Integer number = NUMBER_MAP .getOrDefault (nextWord , null );
255
+ private static BigDecimal combineChunks (List <BigDecimal > chunks ) {
256
+ BigDecimal completeNumber = BigDecimal .ZERO ;
257
+ for (BigDecimal chunk : chunks ) {
258
+ completeNumber = completeNumber .add (chunk );
259
+ }
260
+ return completeNumber ;
261
+ }
262
+ }
192
263
193
- boolean isPrevWordValid = prevNumWasHundred || prevNumWasPowerOfTen ;
194
- boolean isNextWordValid = number != null && (number >= 10 || afterNextWord == null || afterNextWord .equals ("point" ));
264
+ class WordsToNumberException extends RuntimeException {
195
265
196
- return isPrevWordValid && isNextWordValid ;
197
- }
266
+ enum ErrorType {
267
+ NULL_INPUT ("'null' or empty input provided" ),
268
+ UNKNOWN_WORD ("Unknown Word: " ),
269
+ UNEXPECTED_WORD ("Unexpected Word: " ),
270
+ UNEXPECTED_WORD_AFTER_POINT ("Unexpected Word (after Point): " ),
271
+ MISSING_DECIMAL_NUMBERS ("Decimal part is missing numbers." ),
272
+ MULTIPLE_NEGATIVES ("Multiple 'Negative's detected." ),
273
+ INVALID_NEGATIVE ("Incorrect 'negative' placement" ),
274
+ INVALID_CONJUNCTION ("Incorrect 'and' placement" );
198
275
199
- private static boolean isAdditionSafe (BigDecimal currentChunk , BigDecimal number ) {
200
- int chunkDigitCount = currentChunk .toString ().length ();
201
- int numberDigitCount = number .toString ().length ();
202
- return chunkDigitCount > numberDigitCount ;
203
- }
276
+ private final String message ;
204
277
205
- private static String convertDecimalPart ( ArrayDeque < String > wordDeque ) {
206
- StringBuilder decimalPart = new StringBuilder ( "." ) ;
207
- String errorMessage = null ;
278
+ ErrorType ( String message ) {
279
+ this . message = message ;
280
+ }
208
281
209
- while (!wordDeque .isEmpty ()) {
210
- String word = wordDeque .poll ();
211
- Integer number = NUMBER_MAP .getOrDefault (word , null );
212
- if (number == null ) {
213
- errorMessage = "Invalid Input. Unexpected Word (after Point): " + word ;
214
- break ;
282
+ public String formatMessage (String details ) {
283
+ return "Invalid Input. " + message + (details .isEmpty () ? "" : details );
284
+ }
215
285
}
216
- decimalPart .append (number );
217
- }
218
286
219
- if (errorMessage != null ) {
220
- return errorMessage ;
221
- }
287
+ public final ErrorType errorType ;
222
288
223
- if (decimalPart .length () == 1 ) {
224
- return "Invalid Input. Decimal Part is missing Numbers." ;
225
- }
226
- return decimalPart .toString ();
227
- }
228
-
229
- private static BigDecimal combineChunks (List <BigDecimal > chunks ) {
230
- BigDecimal completeNumber = BigDecimal .ZERO ;
231
- for (BigDecimal chunk : chunks ) {
232
- completeNumber = completeNumber .add (chunk );
233
- }
234
- return completeNumber ;
235
- }
289
+ WordsToNumberException (ErrorType errorType , String details ) {
290
+ super (errorType .formatMessage (details ));
291
+ this .errorType = errorType ;
292
+ }
236
293
237
- public static BigDecimal convertToBigDecimal (String numberInWords ) {
238
- String conversionResult = convert (numberInWords );
239
- if (conversionResult .startsWith ("I" )) {
240
- return null ;
294
+ public ErrorType getErrorType () {
295
+ return errorType ;
296
+ }
241
297
}
242
- return new BigDecimal (conversionResult );
243
- }
244
- }
0 commit comments