55import java .util .ArrayList ;
66import java .util .HashMap ;
77import java .util .List ;
8+ import java .util .Map ;
89
910/**
1011 A Java-based utility for converting English word representations of numbers
1617 */
1718
1819public final class WordsToNumber {
20+
1921 private WordsToNumber () {
2022 }
2123
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 <>();
2426
2527 static {
2628 NUMBER_MAP .put ("zero" , 0 );
@@ -60,9 +62,21 @@ private WordsToNumber() {
6062
6163 public static String convert (String numberInWords ) {
6264 if (numberInWords == null ) {
63- return "Null Input" ;
65+ throw new WordsToNumberException ( WordsToNumberException . ErrorType . NULL_INPUT , "" ) ;
6466 }
6567
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 ) {
6680 String [] wordSplitArray = numberInWords .trim ().split ("[ ,-]" );
6781 ArrayDeque <String > wordDeque = new ArrayDeque <>();
6882 for (String word : wordSplitArray ) {
@@ -71,52 +85,115 @@ public static String convert(String numberInWords) {
7185 }
7286 wordDeque .add (word .toLowerCase ());
7387 }
88+ if (wordDeque .isEmpty ()) {
89+ throw new WordsToNumberException (WordsToNumberException .ErrorType .NULL_INPUT , "" );
90+ }
91+ return wordDeque ;
92+ }
7493
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 ) {
76169 BigDecimal currentChunk = BigDecimal .ZERO ;
170+ List <BigDecimal > chunks = new ArrayList <>();
171+
172+ boolean isNegative = "negative" .equals (wordDeque .peek ());
173+ if (isNegative ) wordDeque .poll ();
77174
78- boolean isNegative = false ;
79175 boolean prevNumWasHundred = false ;
80176 boolean prevNumWasPowerOfTen = false ;
81177
82- String errorMessage = null ;
83-
84- while (!wordDeque .isEmpty () && errorMessage == null ) {
178+ while (!wordDeque .isEmpty ()) {
85179 String word = wordDeque .poll ();
86- boolean currentChunkIsZero = currentChunk .compareTo (BigDecimal .ZERO ) == 0 ;
87180
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 );
96184 continue ;
97185 }
98- if (currentChunkIsZero ) {
99- currentChunk = currentChunk .add (BigDecimal .ONE );
186+ case "hundred" -> {
187+ currentChunk = handleHundred (currentChunk , word , prevNumWasPowerOfTen );
188+ prevNumWasHundred = true ;
189+ continue ;
100190 }
101- currentChunk = currentChunk .multiply (BigDecimal .valueOf (100 ));
102- prevNumWasHundred = true ;
103- continue ;
104191 }
105192 prevNumWasHundred = false ;
106193
107194 BigDecimal powerOfTen = POWERS_OF_TEN .getOrDefault (word , null );
108195 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 );
120197 currentChunk = BigDecimal .ZERO ;
121198 prevNumWasPowerOfTen = true ;
122199 continue ;
@@ -125,120 +202,96 @@ public static String convert(String numberInWords) {
125202
126203 Integer number = NUMBER_MAP .getOrDefault (word , null );
127204 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 );
139206 continue ;
140207 }
141208
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 ;
162214 }
163- continue ;
215+ case "negative" -> handleNegative ( isNegative ) ;
164216 }
165217
166- errorMessage = "Invalid Input. " + ( isConjunction ? "Unexpected 'and' placement" : "Unknown Word: " + word );
218+ throw new WordsToNumberException ( WordsToNumberException . ErrorType . UNKNOWN_WORD , word );
167219 }
168220
169- if (errorMessage != null ) {
170- return errorMessage ;
171- }
172-
173- if (!(currentChunk .compareTo (BigDecimal .ZERO ) == 0 )) {
221+ if (currentChunk .compareTo (BigDecimal .ZERO ) != 0 ) {
174222 chunks .add (currentChunk );
175223 }
176- BigDecimal completeNumber = combineChunks (chunks );
177224
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+ }
185229
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+ }
188235
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+ }
190254
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+ }
192263
193- boolean isPrevWordValid = prevNumWasHundred || prevNumWasPowerOfTen ;
194- boolean isNextWordValid = number != null && (number >= 10 || afterNextWord == null || afterNextWord .equals ("point" ));
264+ class WordsToNumberException extends RuntimeException {
195265
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" );
198275
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 ;
204277
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+ }
208281
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+ }
215285 }
216- decimalPart .append (number );
217- }
218286
219- if (errorMessage != null ) {
220- return errorMessage ;
221- }
287+ public final ErrorType errorType ;
222288
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+ }
236293
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+ }
241297 }
242- return new BigDecimal (conversionResult );
243- }
244- }
0 commit comments