Skip to content

Commit a5b4072

Browse files
committed
Add feature to convert numeric words to their number representation
1 parent e6073f8 commit a5b4072

File tree

3 files changed

+273
-0
lines changed

3 files changed

+273
-0
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
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)
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.thealgorithms.conversions;
2+
3+
import java.math.BigDecimal;
4+
import java.util.*;
5+
6+
public final class WordsToNumber {
7+
private WordsToNumber() {
8+
}
9+
10+
private static final HashMap<String, Integer> NUMBER_MAP = new HashMap<>();
11+
private static final HashMap<String, BigDecimal> POWERS_OF_TEN = new HashMap<>();
12+
13+
static {
14+
NUMBER_MAP.put("zero", 0);
15+
NUMBER_MAP.put("one", 1);
16+
NUMBER_MAP.put("two", 2);
17+
NUMBER_MAP.put("three", 3);
18+
NUMBER_MAP.put("four", 4);
19+
NUMBER_MAP.put("five", 5);
20+
NUMBER_MAP.put("six", 6);
21+
NUMBER_MAP.put("seven", 7);
22+
NUMBER_MAP.put("eight", 8);
23+
NUMBER_MAP.put("nine", 9);
24+
NUMBER_MAP.put("ten", 10);
25+
NUMBER_MAP.put("eleven", 11);
26+
NUMBER_MAP.put("twelve", 12);
27+
NUMBER_MAP.put("thirteen", 13);
28+
NUMBER_MAP.put("fourteen", 14);
29+
NUMBER_MAP.put("fifteen", 15);
30+
NUMBER_MAP.put("sixteen", 16);
31+
NUMBER_MAP.put("seventeen", 17);
32+
NUMBER_MAP.put("eighteen", 18);
33+
NUMBER_MAP.put("nineteen", 19);
34+
NUMBER_MAP.put("twenty", 20);
35+
NUMBER_MAP.put("thirty", 30);
36+
NUMBER_MAP.put("forty", 40);
37+
NUMBER_MAP.put("fifty", 50);
38+
NUMBER_MAP.put("sixty", 60);
39+
NUMBER_MAP.put("seventy", 70);
40+
NUMBER_MAP.put("eighty", 80);
41+
NUMBER_MAP.put("ninety", 90);
42+
43+
POWERS_OF_TEN.put("thousand", new BigDecimal("1000"));
44+
POWERS_OF_TEN.put("million", new BigDecimal("1000000"));
45+
POWERS_OF_TEN.put("billion", new BigDecimal("1000000000"));
46+
POWERS_OF_TEN.put("trillion", new BigDecimal("1000000000000"));
47+
}
48+
49+
public static String convert(String numberInWords) {
50+
if (numberInWords == null) return "Null Input";
51+
52+
String[] wordSplitArray = numberInWords.trim().split("[ ,-]");
53+
ArrayDeque<String> wordDeque = new ArrayDeque<>();
54+
for (String word : wordSplitArray) {
55+
if (word.isEmpty()) continue;
56+
wordDeque.add(word.toLowerCase());
57+
}
58+
59+
List<BigDecimal> chunks = new ArrayList<>();
60+
BigDecimal currentChunk = BigDecimal.ZERO;
61+
62+
boolean isNegative = false;
63+
boolean prevNumWasHundred = false;
64+
boolean prevNumWasPowerOfTen = false;
65+
66+
while (!wordDeque.isEmpty()) {
67+
String word = wordDeque.poll();
68+
boolean currentChunkIsZero = currentChunk.equals(BigDecimal.ZERO);
69+
70+
boolean isConjunction = word.equals("and");
71+
if (isConjunction && isValidConjunction(prevNumWasHundred, prevNumWasPowerOfTen, wordDeque)) continue;
72+
73+
boolean isHundred = word.equals("hundred");
74+
if (isHundred) {
75+
if (currentChunk.compareTo(BigDecimal.TEN) >= 0 || prevNumWasPowerOfTen) return "Invalid Input. Unexpected Word: " + word;
76+
if (currentChunkIsZero) currentChunk = currentChunk.add(BigDecimal.ONE);
77+
currentChunk = currentChunk.multiply(BigDecimal.valueOf(100));
78+
prevNumWasHundred = true;
79+
continue;
80+
}
81+
prevNumWasHundred = false;
82+
83+
BigDecimal powerOfTen = POWERS_OF_TEN.getOrDefault(word, null);
84+
if (powerOfTen != null) {
85+
if (currentChunkIsZero || prevNumWasPowerOfTen) return "Invalid Input. Unexpected Word: " + word;
86+
BigDecimal nextChunk = currentChunk.multiply(powerOfTen);
87+
88+
if (chunks.isEmpty() || isAdditionSafe(chunks.getLast(), nextChunk))
89+
chunks.add(nextChunk);
90+
else
91+
return "Invalid Input. Unexpected Word: " + word;
92+
currentChunk = BigDecimal.ZERO;
93+
prevNumWasPowerOfTen = true;
94+
continue;
95+
}
96+
prevNumWasPowerOfTen = false;
97+
98+
Integer number = NUMBER_MAP.getOrDefault(word, null);
99+
if (number != null) {
100+
if (number == 0 && !(currentChunkIsZero && chunks.isEmpty())) return "Invalid Input. Unexpected word: " + word;
101+
BigDecimal bigDecimalNumber = BigDecimal.valueOf(number);
102+
103+
if (currentChunkIsZero || isAdditionSafe(currentChunk, bigDecimalNumber))
104+
currentChunk = currentChunk.add(bigDecimalNumber);
105+
else
106+
return "Invalid Input. Unexpected word: " + word;
107+
continue;
108+
}
109+
110+
if (word.equals("point")) {
111+
if (!currentChunkIsZero) chunks.add(currentChunk);
112+
currentChunk = BigDecimal.ZERO;
113+
114+
String decimalPart = convertDecimalPart(wordDeque);
115+
if (!decimalPart.startsWith("I"))
116+
chunks.add(new BigDecimal(decimalPart));
117+
else
118+
return decimalPart;
119+
break;
120+
}
121+
122+
if (word.equals("negative")) {
123+
if (isNegative) return "Invalid Input. Multiple 'Negative's detected.";
124+
isNegative = chunks.isEmpty() && currentChunkIsZero;
125+
continue;
126+
}
127+
128+
return "Invalid Input. " + (isConjunction ? "Unexpected 'and' placement" : "Unknown Word: " + word);
129+
}
130+
131+
if (!currentChunk.equals(BigDecimal.ZERO)) chunks.add(currentChunk);
132+
BigDecimal completeNumber = combineChunks(chunks);
133+
134+
return isNegative ? completeNumber.multiply(BigDecimal.valueOf(-1)).toString() : completeNumber.toString();
135+
}
136+
137+
private static boolean isValidConjunction(boolean prevNumWasHundred, boolean prevNumWasPowerOfTen, ArrayDeque<String> wordDeque) {
138+
if (wordDeque.isEmpty()) return false;
139+
140+
String nextWord = wordDeque.pollFirst();
141+
String afterNextWord = wordDeque.peekFirst();
142+
143+
wordDeque.addFirst(nextWord);
144+
145+
Integer number = NUMBER_MAP.getOrDefault(nextWord, null);
146+
147+
boolean isPrevWordValid = prevNumWasHundred || prevNumWasPowerOfTen;
148+
boolean isNextWordValid = number != null && (number >= 10 || afterNextWord == null || afterNextWord.equals("point"));
149+
150+
return isPrevWordValid && isNextWordValid;
151+
}
152+
153+
private static boolean isAdditionSafe(BigDecimal currentChunk, BigDecimal number) {
154+
int chunkDigitCount = currentChunk.toString().length();
155+
int numberDigitCount = number.toString().length();
156+
return chunkDigitCount > numberDigitCount;
157+
}
158+
159+
private static String convertDecimalPart(Queue<String> wordDeque) {
160+
StringBuilder decimalPart = new StringBuilder(".");
161+
while (!wordDeque.isEmpty()) {
162+
String word = wordDeque.poll();
163+
Integer number = NUMBER_MAP.getOrDefault(word, null);
164+
if (number != null)
165+
decimalPart.append(number);
166+
else
167+
return "Invalid Input. Unexpected Word (after Point): " + word;
168+
}
169+
170+
if (decimalPart.length() == 1) return "Invalid Input. Decimal Part is missing Numbers.";
171+
return decimalPart.toString();
172+
}
173+
174+
private static BigDecimal combineChunks(List<BigDecimal> chunks) {
175+
BigDecimal completeNumber = BigDecimal.ZERO;
176+
for (BigDecimal chunk : chunks) completeNumber = completeNumber.add(chunk);
177+
return completeNumber;
178+
}
179+
180+
public static BigDecimal convertToBigDecimal(String numberInWords) {
181+
String conversionResult = convert(numberInWords);
182+
if (conversionResult.startsWith("I")) return null;
183+
return new BigDecimal(conversionResult);
184+
}
185+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.thealgorithms.conversions;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.math.BigDecimal;
6+
import org.junit.jupiter.api.Test;
7+
8+
public class WordsToNumberTest {
9+
10+
@Test
11+
void testNullInput() {
12+
assertEquals("Null Input", WordsToNumber.convert(null), "Null input should return 'Null Input'");
13+
}
14+
15+
@Test
16+
void testStandardCases() {
17+
assertEquals("0", WordsToNumber.convert("zero"), "'zero' should convert to '0'");
18+
assertEquals("5", WordsToNumber.convert("five"), "'five' should convert to '5'");
19+
assertEquals("21", WordsToNumber.convert("twenty one"), "'twenty one' should convert to '21'");
20+
assertEquals("101", WordsToNumber.convert("one hundred one"), "'one hundred' should convert to '101'");
21+
assertEquals("342", WordsToNumber.convert("three hundred and forty two"), "'three hundred and forty two' should convert to '342'");
22+
}
23+
24+
@Test
25+
void testLargeNumbers() {
26+
assertEquals("1000000", WordsToNumber.convert("one million"), "'one million' should convert to '1000000'");
27+
assertEquals("1000000000", WordsToNumber.convert("one billion"), "'one billion' should convert to '1000000000'");
28+
assertEquals("1000000000000", WordsToNumber.convert("one trillion"), "'one trillion' should convert to '1000000000000'");
29+
assertEquals("999000000900999", WordsToNumber.convert("nine hundred ninety nine trillion nine hundred thousand nine hundred and ninety nine"), "'nine hundred ninety nine trillion nine hundred thousand nine hundred and ninety nine' should convert to '999000000900999'");
30+
}
31+
32+
@Test
33+
void testNegativeNumbers() {
34+
assertEquals("-5", WordsToNumber.convert("negative five"), "'negative five' should convert to '-5'");
35+
assertEquals("-120", WordsToNumber.convert("negative one hundred and twenty"), "'negative one hundred and twenty' should convert correctly");
36+
}
37+
38+
@Test
39+
void testNegativeLargeNumbers() {
40+
assertEquals("-1000000000000", WordsToNumber.convert("negative one trillion"), "'negative one trillion' should convert to '-1000000000000'");
41+
assertEquals("-9876543210987", WordsToNumber.convert("Negative Nine Trillion Eight Hundred Seventy Six Billion Five Hundred Forty Three Million Two Hundred Ten Thousand Nine Hundred Eighty Seven"), "");
42+
}
43+
44+
@Test
45+
void testDecimalNumbers() {
46+
assertEquals("3.1415", WordsToNumber.convert("three point one four one five"), "'three point one four one five' should convert to '3.1415'");
47+
assertEquals("-2.718", WordsToNumber.convert("negative two point seven one eight"), "'negative two point seven one eight' should convert to '-2.718'");
48+
assertEquals("-1E-7", WordsToNumber.convert("negative zero point zero zero zero zero zero zero one"), "'negative zero point zero zero zero zero zero zero one' should convert to '-1E-7'");
49+
}
50+
51+
@Test
52+
void testLargeDecimalNumbers() {
53+
assertEquals("1000000000.000000001", WordsToNumber.convert("one billion point zero zero zero zero zero zero zero zero zero one"), "Tests a large whole number with a tiny fractional part");
54+
assertEquals("999999999999999.999999999999999",
55+
WordsToNumber.convert("nine hundred ninety nine trillion nine hundred ninety nine billion nine hundred ninety nine million nine hundred ninety nine thousand nine hundred ninety nine point nine nine nine nine nine nine nine nine nine nine nine nine nine"),
56+
"Tests maximum scale handling for large decimal numbers");
57+
assertEquals("0.505", WordsToNumber.convert("zero point five zero five"), "Tests a decimal with an internal zero, ensuring correct parsing");
58+
assertEquals("42.00000000000001", WordsToNumber.convert("forty two point zero zero zero zero zero zero zero zero zero zero zero zero zero one"), "Tests a decimal with leading zeros before a significant figure");
59+
assertEquals("7.89E-7", WordsToNumber.convert("zero point zero zero zero zero zero zero seven eight nine"), "Tests scientific notation for a small decimal with multiple digits");
60+
assertEquals("0.999999", WordsToNumber.convert("zero point nine nine nine nine nine nine"), "Tests a decimal close to one with multiple repeated digits");
61+
}
62+
63+
@Test
64+
void testCaseInsensitivity() {
65+
assertEquals("21", WordsToNumber.convert("TWENTY-ONE"), "Uppercase should still convert correctly");
66+
assertEquals("-100.0001", WordsToNumber.convert("negAtiVe OnE HuNdReD, point ZeRO Zero zERo ONE"), "Mixed case should still convert correctly");
67+
assertEquals("-225647.00019", WordsToNumber.convert("nEgative twO HundRed, and twenty-Five thOusaNd, six huNdred Forty-Seven, Point zero zero zero One nInE"));
68+
}
69+
70+
@Test
71+
void testInvalidInputs() {
72+
assertEquals("Invalid Input. Unknown Word: alpha", WordsToNumber.convert("negative one hundred AlPha"), "'AlPha' is not a valid word");
73+
assertEquals("Invalid Input. Unexpected Word: thirteen", WordsToNumber.convert("twenty thirteen"), "'twenty thirteen' is not a valid format");
74+
assertEquals("Invalid Input. Multiple 'Negative's detected.", WordsToNumber.convert("negative negative ten"), "Should detect multiple negatives");
75+
assertEquals("Invalid Input. Unexpected Word: hundred", WordsToNumber.convert("one hundred hundred"), "Direct repetition of 'hundred' is invalid");
76+
assertEquals("Invalid Input. Unexpected 'and' placement", WordsToNumber.convert("one thousand and hundred"), "Detects invalid 'and' placement");
77+
assertEquals("Invalid Input. Unexpected Word: hundred", WordsToNumber.convert("one thousand hundred"), "Detects incorrect placement of powers of ten");
78+
assertEquals("Invalid Input. Unexpected 'and' placement", WordsToNumber.convert("nine hundred and nine hundred"), "Detects invalid 'and' placement");
79+
}
80+
81+
@Test
82+
void testConvertToBigDecimal() {
83+
assertEquals(new BigDecimal("-100000000000000.056"), WordsToNumber.convertToBigDecimal("negative one hundred trillion point zero five six"), "should convert to appropriate BigDecimal value");
84+
assertNull(WordsToNumber.convertToBigDecimal("invalid input"), "Invalid input should return null");
85+
}
86+
}

0 commit comments

Comments
 (0)