Skip to content

Commit 4514887

Browse files
authored
Add feature to convert numeric words to their number representation (#6195)
1 parent b44ecf7 commit 4514887

File tree

3 files changed

+459
-0
lines changed

3 files changed

+459
-0
lines changed

DIRECTORY.md

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
* [TurkishToLatinConversion](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/conversions/TurkishToLatinConversion.java)
119119
* [UnitConversions](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/conversions/UnitConversions.java)
120120
* [UnitsConverter](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/conversions/UnitsConverter.java)
121+
* [WordsToNumber](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/conversions/WordsToNumber.java)
121122
* datastructures
122123
* bags
123124
* [Bag](https://github.com/TheAlgorithms/Java/blob/master/src/main/java/com/thealgorithms/datastructures/bags/Bag.java)
@@ -840,6 +841,7 @@
840841
* [TurkishToLatinConversionTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/conversions/TurkishToLatinConversionTest.java)
841842
* [UnitConversionsTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/conversions/UnitConversionsTest.java)
842843
* [UnitsConverterTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/conversions/UnitsConverterTest.java)
844+
* [WordsToNumberTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/conversions/WordsToNumberTest.java)
843845
* datastructures
844846
* bag
845847
* [BagTest](https://github.com/TheAlgorithms/Java/blob/master/src/test/java/com/thealgorithms/datastructures/bag/BagTest.java)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
package com.thealgorithms.conversions;
2+
3+
import java.io.Serial;
4+
import java.math.BigDecimal;
5+
import java.util.ArrayDeque;
6+
import java.util.ArrayList;
7+
import java.util.Collection;
8+
import java.util.List;
9+
10+
/**
11+
A Java-based utility for converting English word representations of numbers
12+
into their numeric form. This utility supports whole numbers, decimals,
13+
large values up to trillions, and even scientific notation where applicable.
14+
It ensures accurate parsing while handling edge cases like negative numbers,
15+
improper word placements, and ambiguous inputs.
16+
*
17+
*/
18+
19+
public final class WordsToNumber {
20+
21+
private WordsToNumber() {
22+
}
23+
24+
private enum NumberWord {
25+
ZERO("zero", 0),
26+
ONE("one", 1),
27+
TWO("two", 2),
28+
THREE("three", 3),
29+
FOUR("four", 4),
30+
FIVE("five", 5),
31+
SIX("six", 6),
32+
SEVEN("seven", 7),
33+
EIGHT("eight", 8),
34+
NINE("nine", 9),
35+
TEN("ten", 10),
36+
ELEVEN("eleven", 11),
37+
TWELVE("twelve", 12),
38+
THIRTEEN("thirteen", 13),
39+
FOURTEEN("fourteen", 14),
40+
FIFTEEN("fifteen", 15),
41+
SIXTEEN("sixteen", 16),
42+
SEVENTEEN("seventeen", 17),
43+
EIGHTEEN("eighteen", 18),
44+
NINETEEN("nineteen", 19),
45+
TWENTY("twenty", 20),
46+
THIRTY("thirty", 30),
47+
FORTY("forty", 40),
48+
FIFTY("fifty", 50),
49+
SIXTY("sixty", 60),
50+
SEVENTY("seventy", 70),
51+
EIGHTY("eighty", 80),
52+
NINETY("ninety", 90);
53+
54+
private final String word;
55+
private final int value;
56+
57+
NumberWord(String word, int value) {
58+
this.word = word;
59+
this.value = value;
60+
}
61+
62+
public static Integer getValue(String word) {
63+
for (NumberWord num : values()) {
64+
if (word.equals(num.word)) {
65+
return num.value;
66+
}
67+
}
68+
return null;
69+
}
70+
}
71+
72+
private enum PowerOfTen {
73+
THOUSAND("thousand", new BigDecimal("1000")),
74+
MILLION("million", new BigDecimal("1000000")),
75+
BILLION("billion", new BigDecimal("1000000000")),
76+
TRILLION("trillion", new BigDecimal("1000000000000"));
77+
78+
private final String word;
79+
private final BigDecimal value;
80+
81+
PowerOfTen(String word, BigDecimal value) {
82+
this.word = word;
83+
this.value = value;
84+
}
85+
86+
public static BigDecimal getValue(String word) {
87+
for (PowerOfTen power : values()) {
88+
if (word.equals(power.word)) {
89+
return power.value;
90+
}
91+
}
92+
return null;
93+
}
94+
}
95+
96+
public static String convert(String numberInWords) {
97+
if (numberInWords == null) {
98+
throw new WordsToNumberException(WordsToNumberException.ErrorType.NULL_INPUT, "");
99+
}
100+
101+
ArrayDeque<String> wordDeque = preprocessWords(numberInWords);
102+
BigDecimal completeNumber = convertWordQueueToBigDecimal(wordDeque);
103+
104+
return completeNumber.toString();
105+
}
106+
107+
public static BigDecimal convertToBigDecimal(String numberInWords) {
108+
String conversionResult = convert(numberInWords);
109+
return new BigDecimal(conversionResult);
110+
}
111+
112+
private static ArrayDeque<String> preprocessWords(String numberInWords) {
113+
String[] wordSplitArray = numberInWords.trim().split("[ ,-]");
114+
ArrayDeque<String> wordDeque = new ArrayDeque<>();
115+
for (String word : wordSplitArray) {
116+
if (word.isEmpty()) {
117+
continue;
118+
}
119+
wordDeque.add(word.toLowerCase());
120+
}
121+
if (wordDeque.isEmpty()) {
122+
throw new WordsToNumberException(WordsToNumberException.ErrorType.NULL_INPUT, "");
123+
}
124+
return wordDeque;
125+
}
126+
127+
private static void handleConjunction(boolean prevNumWasHundred, boolean prevNumWasPowerOfTen, ArrayDeque<String> wordDeque) {
128+
if (wordDeque.isEmpty()) {
129+
throw new WordsToNumberException(WordsToNumberException.ErrorType.INVALID_CONJUNCTION, "");
130+
}
131+
132+
String nextWord = wordDeque.pollFirst();
133+
String afterNextWord = wordDeque.peekFirst();
134+
135+
wordDeque.addFirst(nextWord);
136+
137+
Integer number = NumberWord.getValue(nextWord);
138+
139+
boolean isPrevWordValid = prevNumWasHundred || prevNumWasPowerOfTen;
140+
boolean isNextWordValid = number != null && (number >= 10 || afterNextWord == null || "point".equals(afterNextWord));
141+
142+
if (!isPrevWordValid || !isNextWordValid) {
143+
throw new WordsToNumberException(WordsToNumberException.ErrorType.INVALID_CONJUNCTION, "");
144+
}
145+
}
146+
147+
private static BigDecimal handleHundred(BigDecimal currentChunk, String word, boolean prevNumWasPowerOfTen) {
148+
boolean currentChunkIsZero = currentChunk.compareTo(BigDecimal.ZERO) == 0;
149+
if (currentChunk.compareTo(BigDecimal.TEN) >= 0 || prevNumWasPowerOfTen) {
150+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNEXPECTED_WORD, word);
151+
}
152+
if (currentChunkIsZero) {
153+
currentChunk = currentChunk.add(BigDecimal.ONE);
154+
}
155+
return currentChunk.multiply(BigDecimal.valueOf(100));
156+
}
157+
158+
private static void handlePowerOfTen(List<BigDecimal> chunks, BigDecimal currentChunk, BigDecimal powerOfTen, String word, boolean prevNumWasPowerOfTen) {
159+
boolean currentChunkIsZero = currentChunk.compareTo(BigDecimal.ZERO) == 0;
160+
if (currentChunkIsZero || prevNumWasPowerOfTen) {
161+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNEXPECTED_WORD, word);
162+
}
163+
BigDecimal nextChunk = currentChunk.multiply(powerOfTen);
164+
165+
if (!(chunks.isEmpty() || isAdditionSafe(chunks.getLast(), nextChunk))) {
166+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNEXPECTED_WORD, word);
167+
}
168+
chunks.add(nextChunk);
169+
}
170+
171+
private static BigDecimal handleNumber(Collection<BigDecimal> chunks, BigDecimal currentChunk, String word, Integer number) {
172+
boolean currentChunkIsZero = currentChunk.compareTo(BigDecimal.ZERO) == 0;
173+
if (number == 0 && !(currentChunkIsZero && chunks.isEmpty())) {
174+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNEXPECTED_WORD, word);
175+
}
176+
BigDecimal bigDecimalNumber = BigDecimal.valueOf(number);
177+
178+
if (!currentChunkIsZero && !isAdditionSafe(currentChunk, bigDecimalNumber)) {
179+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNEXPECTED_WORD, word);
180+
}
181+
return currentChunk.add(bigDecimalNumber);
182+
}
183+
184+
private static void handlePoint(Collection<BigDecimal> chunks, BigDecimal currentChunk, ArrayDeque<String> wordDeque) {
185+
boolean currentChunkIsZero = currentChunk.compareTo(BigDecimal.ZERO) == 0;
186+
if (!currentChunkIsZero) {
187+
chunks.add(currentChunk);
188+
}
189+
190+
String decimalPart = convertDecimalPart(wordDeque);
191+
chunks.add(new BigDecimal(decimalPart));
192+
}
193+
194+
private static void handleNegative(boolean isNegative) {
195+
if (isNegative) {
196+
throw new WordsToNumberException(WordsToNumberException.ErrorType.MULTIPLE_NEGATIVES, "");
197+
}
198+
throw new WordsToNumberException(WordsToNumberException.ErrorType.INVALID_NEGATIVE, "");
199+
}
200+
201+
private static BigDecimal convertWordQueueToBigDecimal(ArrayDeque<String> wordDeque) {
202+
BigDecimal currentChunk = BigDecimal.ZERO;
203+
List<BigDecimal> chunks = new ArrayList<>();
204+
205+
boolean isNegative = "negative".equals(wordDeque.peek());
206+
if (isNegative) {
207+
wordDeque.poll();
208+
}
209+
210+
boolean prevNumWasHundred = false;
211+
boolean prevNumWasPowerOfTen = false;
212+
213+
while (!wordDeque.isEmpty()) {
214+
String word = wordDeque.poll();
215+
216+
switch (word) {
217+
case "and" -> {
218+
handleConjunction(prevNumWasHundred, prevNumWasPowerOfTen, wordDeque);
219+
continue;
220+
}
221+
case "hundred" -> {
222+
currentChunk = handleHundred(currentChunk, word, prevNumWasPowerOfTen);
223+
prevNumWasHundred = true;
224+
continue;
225+
}
226+
default -> {
227+
228+
}
229+
}
230+
prevNumWasHundred = false;
231+
232+
BigDecimal powerOfTen = PowerOfTen.getValue(word);
233+
if (powerOfTen != null) {
234+
handlePowerOfTen(chunks, currentChunk, powerOfTen, word, prevNumWasPowerOfTen);
235+
currentChunk = BigDecimal.ZERO;
236+
prevNumWasPowerOfTen = true;
237+
continue;
238+
}
239+
prevNumWasPowerOfTen = false;
240+
241+
Integer number = NumberWord.getValue(word);
242+
if (number != null) {
243+
currentChunk = handleNumber(chunks, currentChunk, word, number);
244+
continue;
245+
}
246+
247+
switch (word) {
248+
case "point" -> {
249+
handlePoint(chunks, currentChunk, wordDeque);
250+
currentChunk = BigDecimal.ZERO;
251+
continue;
252+
}
253+
case "negative" -> {
254+
handleNegative(isNegative);
255+
}
256+
default -> {
257+
258+
}
259+
}
260+
261+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNKNOWN_WORD, word);
262+
}
263+
264+
if (currentChunk.compareTo(BigDecimal.ZERO) != 0) {
265+
chunks.add(currentChunk);
266+
}
267+
268+
BigDecimal completeNumber = combineChunks(chunks);
269+
return isNegative ? completeNumber.multiply(BigDecimal.valueOf(-1))
270+
:
271+
completeNumber;
272+
}
273+
274+
private static boolean isAdditionSafe(BigDecimal currentChunk, BigDecimal number) {
275+
int chunkDigitCount = currentChunk.toString().length();
276+
int numberDigitCount = number.toString().length();
277+
return chunkDigitCount > numberDigitCount;
278+
}
279+
280+
private static String convertDecimalPart(ArrayDeque<String> wordDeque) {
281+
StringBuilder decimalPart = new StringBuilder(".");
282+
283+
while (!wordDeque.isEmpty()) {
284+
String word = wordDeque.poll();
285+
Integer number = NumberWord.getValue(word);
286+
if (number == null) {
287+
throw new WordsToNumberException(WordsToNumberException.ErrorType.UNEXPECTED_WORD_AFTER_POINT, word);
288+
}
289+
decimalPart.append(number);
290+
}
291+
292+
boolean missingNumbers = decimalPart.length() == 1;
293+
if (missingNumbers) {
294+
throw new WordsToNumberException(WordsToNumberException.ErrorType.MISSING_DECIMAL_NUMBERS, "");
295+
}
296+
return decimalPart.toString();
297+
}
298+
299+
private static BigDecimal combineChunks(List<BigDecimal> chunks) {
300+
BigDecimal completeNumber = BigDecimal.ZERO;
301+
for (BigDecimal chunk : chunks) {
302+
completeNumber = completeNumber.add(chunk);
303+
}
304+
return completeNumber;
305+
}
306+
}
307+
308+
class WordsToNumberException extends RuntimeException {
309+
310+
@Serial private static final long serialVersionUID = 1L;
311+
312+
enum ErrorType {
313+
NULL_INPUT("'null' or empty input provided"),
314+
UNKNOWN_WORD("Unknown Word: "),
315+
UNEXPECTED_WORD("Unexpected Word: "),
316+
UNEXPECTED_WORD_AFTER_POINT("Unexpected Word (after Point): "),
317+
MISSING_DECIMAL_NUMBERS("Decimal part is missing numbers."),
318+
MULTIPLE_NEGATIVES("Multiple 'Negative's detected."),
319+
INVALID_NEGATIVE("Incorrect 'negative' placement"),
320+
INVALID_CONJUNCTION("Incorrect 'and' placement");
321+
322+
private final String message;
323+
324+
ErrorType(String message) {
325+
this.message = message;
326+
}
327+
328+
public String formatMessage(String details) {
329+
return "Invalid Input. " + message + (details.isEmpty() ? "" : details);
330+
}
331+
}
332+
333+
public final ErrorType errorType;
334+
335+
WordsToNumberException(ErrorType errorType, String details) {
336+
super(errorType.formatMessage(details));
337+
this.errorType = errorType;
338+
}
339+
340+
public ErrorType getErrorType() {
341+
return errorType;
342+
}
343+
}

0 commit comments

Comments
 (0)