/**
* Portions Copyright 2001-2003 Sun Microsystems, Inc.
* Portions Copyright 1999-2001 Language Technologies Institute,
* Carnegie Mellon University.
* All Rights Reserved. Use is subject to license terms.
*
* See the file "license.terms" for information on usage and
* redistribution of this file, and for a DISCLAIMER OF ALL
* WARRANTIES.
*/
package com.sun.speech.freetts.en.us;
import com.sun.speech.freetts.FeatureSet;
import com.sun.speech.freetts.Item;
import com.sun.speech.freetts.util.Utilities;
/**
* Expands Strings containing digits characters into
* a list of words representing those digits.
*
* It translates the following code from flite:
* <code>lang/usEnglish/us_expand.c</code>
*/
public class NumberExpander {
private static final String[] digit2num = {
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine" };
private static final String[] digit2teen = {
"ten", /* shouldn't get called */
"eleven",
"twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
"eighteen",
"nineteen" };
private static final String[] digit2enty = {
"zero", /* shouldn't get called */
"ten",
"twenty",
"thirty",
"forty",
"fifty",
"sixty",
"seventy",
"eighty",
"ninety" };
private static final String[] ord2num = {
"zeroth",
"first",
"second",
"third",
"fourth",
"fifth",
"sixth",
"seventh",
"eighth",
"ninth" };
private static final String[] ord2teen = {
"tenth", /* shouldn't get called */
"eleventh",
"twelfth",
"thirteenth",
"fourteenth",
"fifteenth",
"sixteenth",
"seventeenth",
"eighteenth",
"nineteenth" };
private static final String[] ord2enty = {
"zeroth", /* shouldn't get called */
"tenth",
"twentieth",
"thirtieth",
"fortieth",
"fiftieth",
"sixtieth",
"seventieth",
"eightieth",
"ninetieth" };
/**
* Unconstructable
*/
private NumberExpander() {
}
/**
* Expands a digit string into a list of English words of those digits.
* For example, "1234" expands to "one two three four"
*
* @param numberString the digit string to expand.
* @param wordRelation words are added to this Relation
*/
public static void expandNumber(String numberString,
WordRelation wordRelation) {
int numDigits = numberString.length();
if (numDigits == 0) {
// wordRelation = null;
} else if (numDigits == 1) {
expandDigits(numberString, wordRelation);
} else if (numDigits == 2) {
expand2DigitNumber(numberString, wordRelation);
} else if (numDigits == 3) {
expand3DigitNumber(numberString, wordRelation);
} else if (numDigits < 7) {
expandBelow7DigitNumber(numberString, wordRelation);
} else if (numDigits < 10) {
expandBelow10DigitNumber(numberString, wordRelation);
} else if (numDigits < 13) {
expandBelow13DigitNumber(numberString, wordRelation);
} else {
expandDigits(numberString, wordRelation);
}
}
/**
* Expands a two-digit string into a list of English words.
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
private static void expand2DigitNumber(String numberString,
WordRelation wordRelation) {
if (numberString.charAt(0) == '0') {
// numberString is "0X"
if (numberString.charAt(1) == '0') {
// numberString is "00", do nothing
} else {
// numberString is "01", "02" ...
String number = digit2num[numberString.charAt(1)-'0'];
wordRelation.addWord(number);
}
} else if (numberString.charAt(1) == '0') {
// numberString is "10", "20", ...
String number = digit2enty[numberString.charAt(0)-'0'];
wordRelation.addWord(number);
} else if (numberString.charAt(0) == '1') {
// numberString is "11", "12", ..., "19"
String number = digit2teen[numberString.charAt(1)-'0'];
wordRelation.addWord(number);
} else {
// numberString is "2X", "3X", ...
String enty = digit2enty[numberString.charAt(0)-'0'];
wordRelation.addWord(enty);
expandDigits(numberString.substring(1,numberString.length()),
wordRelation);
}
}
/**
* Expands a three-digit string into a list of English words.
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
private static void expand3DigitNumber(String numberString,
WordRelation wordRelation) {
if (numberString.charAt(0) == '0') {
expandNumberAt(numberString, 1, wordRelation);
} else {
String hundredDigit = digit2num[numberString.charAt(0)-'0'];
wordRelation.addWord(hundredDigit);
wordRelation.addWord("hundred");
expandNumberAt(numberString, 1, wordRelation);
}
}
/**
* Expands a string that is a 4 to 6 digits number into a list
* of English words. For example, "333000" into "three hundred
* and thirty-three thousand".
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
private static void expandBelow7DigitNumber(String numberString,
WordRelation wordRelation) {
expandLargeNumber(numberString, "thousand", 3, wordRelation);
}
/**
* Expands a string that is a 7 to 9 digits number into a list
* of English words. For example, "19000000" into nineteen million.
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
private static void expandBelow10DigitNumber(String numberString,
WordRelation wordRelation) {
expandLargeNumber(numberString, "million", 6, wordRelation);
}
/**
* Expands a string that is a 10 to 12 digits number into a list
* of English words. For example, "27000000000" into twenty-seven
* billion.
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
private static void expandBelow13DigitNumber(String numberString,
WordRelation wordRelation) {
expandLargeNumber(numberString, "billion", 9, wordRelation);
}
/**
* Expands a string that is a number longer than 3 digits into a list
* of English words. For example, "1000" into one thousand.
*
* @param numberString the string which is the number to expand
* @param order either "thousand", "million", or "billion"
* @param numberZeroes the number of zeroes, depending on the order, so
* its either 3, 6, or 9
* @param wordRelation words are added to this Relation
*/
private static void expandLargeNumber(String numberString,
String order,
int numberZeroes,
WordRelation wordRelation) {
int numberDigits = numberString.length();
// parse out the prefix, e.g., "113" in "113,000"
int i = numberDigits - numberZeroes;
String part = numberString.substring(0, i);
// get how many thousands/millions/billions
Item oldTail = wordRelation.getTail();
expandNumber(part, wordRelation);
if (wordRelation.getTail() == oldTail) {
expandNumberAt(numberString, i, wordRelation);
} else {
wordRelation.addWord(order);
expandNumberAt(numberString, i, wordRelation);
}
}
/**
* Returns the number string list of the given string starting at
* the given index. E.g., expandNumberAt("1100", 1) gives "one hundred"
*
* @param numberString the string which is the number to expand
* @param startIndex the starting position
* @param wordRelation words are added to this Relation
*/
private static void expandNumberAt(String numberString,
int startIndex,
WordRelation wordRelation) {
expandNumber(numberString.substring(startIndex,numberString.length()),
wordRelation);
}
/**
* Expands given token to list of words pronouncing it as digits
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
public static void expandDigits(String numberString,
WordRelation wordRelation) {
int numberDigits = numberString.length();
for (int i = 0; i < numberDigits; i++) {
char digit = numberString.charAt(i);
if (isDigit(digit)) {
wordRelation.addWord(digit2num[numberString.charAt(i)-'0']);
} else {
wordRelation.addWord("umpty");
}
}
}
/**
* Expands the digit string of an ordinal number.
*
* @param rawNumberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
public static void expandOrdinal(String rawNumberString,
WordRelation wordRelation) {
// remove all ','s from the raw number string
String numberString = Utilities.deleteChar(rawNumberString, ',');
expandNumber(numberString, wordRelation);
// get the last in the list of number strings
Item lastItem = wordRelation.getTail();
if (lastItem != null) {
FeatureSet featureSet = lastItem.getFeatures();
String lastNumber = featureSet.getString("name");
String ordinal = findMatchInArray(lastNumber, digit2num, ord2num);
if (ordinal == null) {
ordinal = findMatchInArray(lastNumber, digit2teen, ord2teen);
}
if (ordinal == null) {
ordinal = findMatchInArray(lastNumber, digit2enty, ord2enty);
}
if (lastNumber.equals("hundred")) {
ordinal = "hundredth";
} else if (lastNumber.equals("thousand")) {
ordinal = "thousandth";
} else if (lastNumber.equals("billion")) {
ordinal = "billionth";
}
// if there was an ordinal, set the last element of the list
// to that ordinal; otherwise, don't do anything
if (ordinal != null) {
wordRelation.setLastWord(ordinal);
}
}
}
/**
* Finds a match of the given string in the given array,
* and returns the element at the same index in the returnInArray
*
* @param strToMatch the string to match
* @param matchInArray the source array
* @param returnInArray the return array
*
* @return an element in returnInArray, or <code>null</code>
* if a match is not found
*/
private static String findMatchInArray(String strToMatch,
String[] matchInArray,
String[] returnInArray) {
for (int i = 0; i < matchInArray.length; i++) {
if (strToMatch.equals(matchInArray[i])) {
if (i < returnInArray.length) {
return returnInArray[i];
} else {
return null;
}
}
}
return null;
}
/**
* Expands the given number string as pairs as in years or IDs
*
* @param numberString the string which is the number to expand
* @param wordRelation words are added to this Relation
*/
public static void expandID(String numberString, WordRelation wordRelation) {
int numberDigits = numberString.length();
if ((numberDigits == 4) &&
(numberString.charAt(2) == '0') &&
(numberString.charAt(3) == '0')) {
if (numberString.charAt(1) == '0') { // e.g. 2000, 3000
expandNumber(numberString, wordRelation);
} else {
expandNumber(numberString.substring(0,2), wordRelation);
wordRelation.addWord("hundred");
}
} else if ((numberDigits == 2) && (numberString.charAt(0) == '0')) {
wordRelation.addWord("oh");
expandDigits(numberString.substring(1,2), wordRelation);
} else if ((numberDigits == 4 &&
numberString.charAt(1) == '0') ||
numberDigits < 3) {
expandNumber(numberString, wordRelation);
} else if (numberDigits % 2 == 1) {
String firstDigit = digit2num[numberString.charAt(0)-'0'];
wordRelation.addWord(firstDigit);
expandID(numberString.substring(1,numberDigits), wordRelation);
} else {
expandNumber(numberString.substring(0,2), wordRelation);
expandID(numberString.substring(2,numberDigits), wordRelation);
}
}
/**
* Expands the given number string as a real number.
*
* @param numberString the string which is the real number to expand
* @param wordRelation words are added to this Relation
*/
public static void expandReal(String numberString, WordRelation wordRelation) {
int stringLength = numberString.length();
int position;
if (numberString.charAt(0) == '-') {
// negative real numbers
wordRelation.addWord("minus");
expandReal(numberString.substring(1, stringLength), wordRelation);
} else if (numberString.charAt(0) == '+') {
// prefixed with a '+'
wordRelation.addWord("plus");
expandReal(numberString.substring(1, stringLength), wordRelation);
} else if ((position = numberString.indexOf('e')) != -1 ||
(position = numberString.indexOf('E')) != -1) {
// numbers with 'E' or 'e'
expandReal(numberString.substring(0, position), wordRelation);
wordRelation.addWord("e");
expandReal(numberString.substring(position + 1), wordRelation);
} else if ((position = numberString.indexOf('.')) != -1) {
// numbers with '.'
String beforeDot = numberString.substring(0, position);
if (beforeDot.length() > 0) {
expandReal(beforeDot, wordRelation);
}
wordRelation.addWord("point");
String afterDot = numberString.substring(position + 1);
if (afterDot.length() > 0) {
expandDigits(afterDot, wordRelation);
}
} else {
// everything else
expandNumber(numberString, wordRelation);
}
}
/**
* Expands the given string of letters as a list of single char symbols.
*
* @param letters the string of letters to expand
* @param wordRelation words are added to this Relation
*/
public static void expandLetters(String letters,
WordRelation wordRelation) {
letters = letters.toLowerCase();
char c;
for (int i = 0; i < letters.length(); i++) {
// if this is a number
c = letters.charAt(i);
if (isDigit(c)) {
wordRelation.addWord(digit2num[c-'0']);
} else if (letters.equals("a")) {
wordRelation.addWord("_a");
} else {
wordRelation.addWord(String.valueOf(c));
}
}
}
/**
* Returns the integer value of the given string of Roman numerals.
*
* @param roman the string of Roman numbers
*
* @return the integer value
*/
public static int expandRoman(String roman) {
int value = 0;
for (int p = 0; p < roman.length(); p++) {
char c = roman.charAt(p);
if (c == 'X') {
value += 10;
} else if (c == 'V') {
value += 5;
} else if (c == 'I') {
if (p+1 < roman.length()) {
char p1 = roman.charAt(p+1);
if (p1 == 'V') {
value += 4;
p++;
} else if (p1 == 'X') {
value += 9;
p++;
} else {
value += 1;
}
} else {
value += 1;
}
}
}
return value;
}
/**
* Returns true if the given character is a digit (0-9 only).
*
* @param ch the character to test
*
* @return true or false
*/
public static boolean isDigit(char ch) {
return ('0' <= ch && ch <= '9');
}
}