627 lines
18 KiB
Java
627 lines
18 KiB
Java
package com.amarsoft.aims.tools;
|
||
|
||
import java.util.HashMap;
|
||
import java.util.Map;
|
||
|
||
public abstract class NumberText {
|
||
|
||
/*----------------------------------------------------------------------------
|
||
* not designed to be inherented outside this file
|
||
* no public constructors provided, -- use factory method
|
||
---------------------------------------------------------------------------*/
|
||
private NumberText() {}
|
||
|
||
/**
|
||
* Exports a {@code NumberText} implementation instance, based on a natural
|
||
* language argument. {@see Lang}
|
||
*
|
||
* @param lang
|
||
* @return a NumberText instance.
|
||
*/
|
||
public static NumberText getInstance(Lang lang) {
|
||
|
||
return lang.instance();
|
||
}
|
||
|
||
/**
|
||
* Transfers an integer number into a String, specifically in which language
|
||
* depends on the implementation.
|
||
* <p />
|
||
* e.g. in EnglishWithDash,
|
||
* <p />
|
||
* 100 -> one hundred
|
||
* <br />
|
||
* -976083854 -> minus nine hundred and seventy-six million and eighty-three
|
||
* thousand eight hundred and fifty-four
|
||
*
|
||
* @param number the integer number to be transfered
|
||
* @return the result String
|
||
*/
|
||
public final String getText(long number) {
|
||
|
||
return getText(Long.toString(number));
|
||
}
|
||
|
||
/**
|
||
* Transfers an integer number into a String, specifically in which language
|
||
* depends on the implementation.
|
||
* <p />
|
||
* e.g. in EnglishWithDash,
|
||
* <p />
|
||
* 100 -> one hundred
|
||
* <br />
|
||
* -976083854 -> minus nine hundred and seventy-six million and eighty-three
|
||
* thousand eight hundred and fifty-four
|
||
*
|
||
* @param number the integer number to be transfered
|
||
* @return the result String
|
||
*/
|
||
public abstract String getText(String number);
|
||
|
||
/**
|
||
* Transfers an integer number into a String of its ordinal representation,
|
||
* specifically in which language depends on the implementation.
|
||
* <p />
|
||
* e.g. in EnglishWithDash,
|
||
* <p />
|
||
* 100 -> one hundredth
|
||
* <br />
|
||
* 8331125 -> eight million three hundred and thirty-one thousand one
|
||
* hundred and twenty-fifth
|
||
*
|
||
* @param number the integer number to be transfered
|
||
* @return the result String
|
||
*/
|
||
public final String getOrdinalText(long number) {
|
||
|
||
return getOrdinalText(Long.toString(number));
|
||
}
|
||
|
||
/**
|
||
* <p>
|
||
* Transfers an integer number into a String of its ordinal representation,
|
||
* specifically in which language depends on the implementation.
|
||
* </p>
|
||
*
|
||
* <p>
|
||
* e.g. in EnglishWithDash,
|
||
* <br /><br />
|
||
* 100 -> one hundredth
|
||
* <br />
|
||
* 8331125 -> eight million three hundred and thirty-one thousand one
|
||
* hundred and twenty-fifth
|
||
* </p>
|
||
*
|
||
* @param number the integer number to be transfered
|
||
* @return the result String
|
||
*/
|
||
public abstract String getOrdinalText(String number);
|
||
|
||
/**
|
||
* This enumeration type is typically named under a natural language
|
||
* name, and is to mark a specific implementation name; it is used as an
|
||
* argument to call the factory method
|
||
* {@link NumberText#getInstance(NumberText.Lang)}.
|
||
*/
|
||
public static enum Lang {
|
||
|
||
English (NumberTextEnglishCleanSpaceOnly.INSTANCE),
|
||
EnglishWithDash (NumberTextEnglish.INSTANCE),
|
||
ChineseSimplified (NumberTextChinese.SIMPLIFIED),
|
||
ChineseTraditional (NumberTextChinese.TRADITIONAL),
|
||
;
|
||
|
||
private final NumberText instance;
|
||
private Lang(NumberText instance) {
|
||
|
||
this.instance = instance;
|
||
}
|
||
|
||
private NumberText instance() {
|
||
|
||
if( instance == null )
|
||
throw new UnsupportedOperationException(
|
||
"Language not supported yet : " + this);
|
||
|
||
return instance;
|
||
}
|
||
}
|
||
|
||
abstract int limit();
|
||
|
||
void checkNumber(String number) {
|
||
|
||
if( !number.matches("-?\\d+") )
|
||
throw new NumberFormatException();
|
||
|
||
int length = number.length();
|
||
if( number.startsWith("-") )
|
||
length --;
|
||
|
||
if( length > limit() )
|
||
throw new UnsupportedOperationException(
|
||
"The current " + NumberText.class.getSimpleName() +
|
||
"can only handle numbers up to (+/-)10^" + limit() + ".");
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------
|
||
* EnglishWithDash Implementation
|
||
---------------------------------------------------------------------------*/
|
||
private static class NumberTextEnglish extends NumberText {
|
||
|
||
private static final NumberText INSTANCE = new NumberTextEnglish();
|
||
|
||
static enum Connect {
|
||
|
||
Minus ("minus"),
|
||
Hundred ("hundred"),
|
||
And ("and"),
|
||
AfterMinus (" "),
|
||
AfterNumber (" "),
|
||
AfterPower (" "),
|
||
AfterHundred (" "),
|
||
AfterAnd (" "),
|
||
AfterTen ("-"),
|
||
;
|
||
|
||
final String display;
|
||
Connect(String display) { this.display = display; }
|
||
|
||
private static boolean isConnect(char c) {
|
||
return c == ' ' || c == '-';
|
||
}
|
||
}
|
||
|
||
static enum Power {
|
||
|
||
Thousand ("thousand"), // 10 ^ 3
|
||
Million ("million"), // 10 ^ 6
|
||
Billion ("billion"), // 10 ^ 9
|
||
Trillion ("trillion"), // 10 ^ 12
|
||
Quadrillion ("quadrillion"), // 10 ^ 15
|
||
Quintillion ("quintillion"), // 10 ^ 18 (enough for Long.MAX_VALUE)
|
||
Sextillion ("sextillion"), // 10 ^ 21
|
||
Septillion ("septillion"), // 10 ^ 24
|
||
Octillion ("octillion"), // 10 ^ 27
|
||
Nonillion ("nonillion"), // 10 ^ 30
|
||
Decillion ("decillion"), // 10 ^ 33
|
||
Undecillion ("undecillion"), // 10 ^ 36
|
||
Duodecillion ("duodecillion"), // 10 ^ 39
|
||
Tredecillion ("tredecillion"), // 10 ^ 42
|
||
Quattuordecillion ("quattuordecillion"), // 10 ^ 45
|
||
Quindecillion ("quindecillion"), // 10 ^ 48
|
||
Sexdecillion ("sexdecillion"), // 10 ^ 51
|
||
Septendecillion ("septendecillion"), // 10 ^ 54
|
||
Octodecillion ("octodecillion"), // 10 ^ 57
|
||
Novemdecillion ("novemdecillion"), // 10 ^ 60
|
||
Vigintillion ("vigintillion"), // 10 ^ 63
|
||
;
|
||
|
||
final String display;
|
||
Power(String display) { this.display = display; }
|
||
}
|
||
|
||
static enum Digit {
|
||
|
||
Zero ("zero", "zeroth", "ten", ""),
|
||
One ("one", "first", "eleven", "ten"),
|
||
Two ("two", "second", "twelve", "twenty"),
|
||
Three ("three", "third", "thirteen", "thirty"),
|
||
Four ("four", "fourth", "fourteen", "fourty"),
|
||
Five ("five", "fifth", "fifteen", "fifty"),
|
||
Six ("six", "sixth", "sixteen", "sixty"),
|
||
Seven ("seven", "seventh", "seventeen", "seventy"),
|
||
Eight ("eight", "eighth", "eighteen", "eighty"),
|
||
Nine ("nine", "nineth", "nineteen", "ninety"),
|
||
;
|
||
|
||
final String display, displayOrdinal, plusTen, multiTen;
|
||
Digit(String display, String displayOrdinal,
|
||
String plusTen, String multiTen) {
|
||
this.display = display;
|
||
this.displayOrdinal = displayOrdinal;
|
||
this.plusTen = plusTen;
|
||
this.multiTen = multiTen;
|
||
}
|
||
}
|
||
|
||
private static final Map<String, String> _Ordinals;
|
||
static {
|
||
_Ordinals = new HashMap<String, String>();
|
||
for(Digit d : Digit.values())
|
||
_Ordinals.put(d.display, d.displayOrdinal);
|
||
}
|
||
|
||
@Override
|
||
int limit() {
|
||
|
||
return 63;
|
||
}
|
||
|
||
@Override
|
||
public String getText(String number) {
|
||
|
||
checkNumber(number);
|
||
|
||
StringBuilder builder = new StringBuilder();
|
||
buildText(builder, number);
|
||
return builder.toString();
|
||
}
|
||
|
||
@Override
|
||
public String getOrdinalText(String number) {
|
||
|
||
checkNumber(number);
|
||
|
||
StringBuilder builder = new StringBuilder();
|
||
buildText(builder, number);
|
||
replaceLastTokenWithOrdinal(builder);
|
||
return builder.toString();
|
||
}
|
||
|
||
private void buildText(StringBuilder builder, String number) {
|
||
|
||
assert builder != null;
|
||
|
||
if( number.startsWith("-") ) {
|
||
builder.append(getConnectDisplay(Connect.Minus))
|
||
.append(getConnectDisplay(Connect.AfterMinus));
|
||
number = number.substring(1);
|
||
}
|
||
|
||
int power = 0;
|
||
while(number.length() > (power + 1) * 3)
|
||
power++;
|
||
|
||
while(power > 0) {
|
||
boolean modified = extendToken(builder, number, power * 3);
|
||
if( modified )
|
||
builder.append(getConnectDisplay(Connect.AfterNumber))
|
||
.append(getPowerDisplay(Power.values()[power-1]));
|
||
power--;
|
||
}
|
||
extendToken(builder, number, 0);
|
||
}
|
||
|
||
private boolean extendToken(StringBuilder builder,
|
||
String number,
|
||
int suffix) {
|
||
|
||
assert builder != null && suffix < number.length();
|
||
|
||
int len = number.length() - suffix;
|
||
int hundreds = len > 2 ? (int)(number.charAt(len-3)-'0') : -1;
|
||
int tens = len > 1 ? (int)(number.charAt(len-2)-'0') : -1;
|
||
int inds = (int)(number.charAt(len-1)-'0');
|
||
|
||
if( hundreds <= 0 && tens <= 0 && inds <= 0 && suffix > 0 )
|
||
return false;
|
||
else if( len > 3 )
|
||
builder.append(getConnectDisplay(Connect.AfterPower));
|
||
|
||
if( hundreds == 0 ) {
|
||
if( len > 3 && (tens > 0 || inds > 0) )
|
||
builder.append(getConnectDisplay(Connect.And))
|
||
.append(getConnectDisplay(Connect.AfterAnd));
|
||
}
|
||
else if( hundreds > 0 ) {
|
||
builder.append(getDigitName(Digit.values()[hundreds]))
|
||
.append(getConnectDisplay(Connect.AfterNumber))
|
||
.append(getConnectDisplay(Connect.Hundred));
|
||
if( tens > 0 || inds > 0 )
|
||
builder.append(getConnectDisplay(Connect.AfterHundred))
|
||
.append(getConnectDisplay(Connect.And))
|
||
.append(getConnectDisplay(Connect.AfterAnd));
|
||
}
|
||
|
||
if( tens > 1 ) {
|
||
builder.append(getDigitMultiTen(Digit.values()[tens]));
|
||
if( inds > 0 )
|
||
builder.append(getConnectDisplay(Connect.AfterTen));
|
||
}
|
||
|
||
if( tens == 1 )
|
||
builder.append(getDigitPlusTen(Digit.values()[inds]));
|
||
else if( inds > 0 || number.length() == 1 )
|
||
builder.append(getDigitName(Digit.values()[inds]));
|
||
|
||
return true;
|
||
}
|
||
|
||
private void replaceLastTokenWithOrdinal(StringBuilder builder) {
|
||
|
||
assert builder != null && builder.length() > 0;
|
||
|
||
int suffix = builder.length()-1;
|
||
while( suffix >= 0 && !isConnect(builder.charAt(suffix)) )
|
||
suffix--;
|
||
String lastToken = builder.substring(suffix+1);
|
||
builder.delete(suffix+1, builder.length()).append(toOrdinal(lastToken));
|
||
}
|
||
|
||
String getPowerDisplay(Power power) {
|
||
|
||
assert power != null;
|
||
|
||
return power.display;
|
||
}
|
||
|
||
String getConnectDisplay(Connect connect) {
|
||
|
||
assert connect != null;
|
||
|
||
return connect.display;
|
||
}
|
||
|
||
String getDigitName(Digit digit) {
|
||
|
||
assert digit != null;
|
||
|
||
return digit.display;
|
||
}
|
||
|
||
String toOrdinal(String name) {
|
||
|
||
assert name != null && !name.isEmpty();
|
||
|
||
String result = _Ordinals.get(name);
|
||
if( result == null ) {
|
||
if( name.charAt(name.length()-1) == 'y' )
|
||
result = name.substring(0, name.length()-1) + "ieth";
|
||
else
|
||
result = name + "th";
|
||
}
|
||
return result;
|
||
}
|
||
|
||
String getDigitPlusTen(Digit digit) {
|
||
|
||
assert digit != null;
|
||
|
||
return digit.plusTen;
|
||
}
|
||
|
||
String getDigitMultiTen(Digit digit) {
|
||
|
||
assert digit != null;
|
||
|
||
return digit.multiTen;
|
||
}
|
||
|
||
boolean isConnect(char c) {
|
||
return Connect.isConnect(c);
|
||
}
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------
|
||
* EnglishWithDash with only Clean Space Connectors
|
||
---------------------------------------------------------------------------*/
|
||
private static class NumberTextEnglishCleanSpaceOnly
|
||
extends NumberTextEnglish {
|
||
|
||
private static final NumberText INSTANCE =
|
||
new NumberTextEnglishCleanSpaceOnly();
|
||
|
||
@Override
|
||
String getConnectDisplay(Connect connect) {
|
||
|
||
return connect == Connect.AfterTen ?
|
||
" " :
|
||
super.getConnectDisplay(connect);
|
||
}
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------
|
||
* ChineseSimplified Implementation
|
||
---------------------------------------------------------------------------*/
|
||
private static class NumberTextChinese extends NumberText {
|
||
|
||
private static final NumberText SIMPLIFIED =
|
||
new NumberTextChinese(Type.Simplified);
|
||
private static final NumberText TRADITIONAL =
|
||
new NumberTextChinese(Type.Traditional);
|
||
|
||
static enum Type { Simplified, Traditional; }
|
||
|
||
static enum Connect {
|
||
Di ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Fu ("<EFBFBD><EFBFBD>", "ؓ"),
|
||
Ling ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Shi ("ʮ", "ʰ"),
|
||
Bai ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Qian ("ǧ", "Ǫ"),
|
||
;
|
||
|
||
final String display, displayTraditional;
|
||
Connect(String display, String displayTraditional) {
|
||
this.display = display;
|
||
this.displayTraditional = displayTraditional;
|
||
}
|
||
}
|
||
|
||
static enum Power {
|
||
|
||
Wan ("<EFBFBD><EFBFBD>", "<EFBFBD>f"), // 10^4
|
||
Yi ("<EFBFBD><EFBFBD>", "<EFBFBD>|"), // 10^8
|
||
Zhao ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^12
|
||
Jing ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^16 (enough for Long.MAX_VALUE)
|
||
Gai ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^20
|
||
Zi ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^24
|
||
Rang ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^28
|
||
Gou ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^32
|
||
Jian ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^36
|
||
Zheng ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // 10^40
|
||
Zai ("<EFBFBD><EFBFBD>", "<EFBFBD>d"), // 10^44
|
||
;
|
||
|
||
final String display, displayTraditional;
|
||
Power(String display, String displayTraditional) {
|
||
this.display = display;
|
||
this.displayTraditional = displayTraditional;
|
||
}
|
||
}
|
||
|
||
static enum Digit {
|
||
|
||
Ling ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"), // just to occupy this position
|
||
Yi ("һ", "Ҽ"),
|
||
Er ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
San ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Si ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Wu ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Liu ("<EFBFBD><EFBFBD>", "½"),
|
||
Qi ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Ba ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
Jiu ("<EFBFBD><EFBFBD>", "<EFBFBD><EFBFBD>"),
|
||
;
|
||
|
||
final String display, displayTraditional;
|
||
Digit(String display, String displayTraditional) {
|
||
this.display = display;
|
||
this.displayTraditional = displayTraditional;
|
||
}
|
||
}
|
||
|
||
private final Type type;
|
||
private NumberTextChinese(Type type) {
|
||
assert type != null;
|
||
|
||
this.type = type;
|
||
}
|
||
|
||
@Override
|
||
int limit() {
|
||
|
||
return 44;
|
||
}
|
||
|
||
@Override
|
||
public String getText(String number) {
|
||
|
||
checkNumber(number);
|
||
|
||
StringBuilder builder = new StringBuilder();
|
||
buildText(builder, number);
|
||
return builder.toString();
|
||
}
|
||
|
||
@Override
|
||
public String getOrdinalText(String number) {
|
||
|
||
checkNumber(number);
|
||
|
||
StringBuilder builder = new StringBuilder().append(Connect.Di);
|
||
buildText(builder, number);
|
||
return builder.toString();
|
||
}
|
||
|
||
private void buildText(StringBuilder builder, String number) {
|
||
|
||
assert builder != null;
|
||
|
||
if( number.startsWith("-") ) {
|
||
builder.append(getConnectDisplay(Connect.Fu));
|
||
number = number.substring(1);
|
||
}
|
||
|
||
int power = 0;
|
||
while(number.length() > (power + 1) * 4)
|
||
power++;
|
||
|
||
while(power > 0) {
|
||
if( extendToken(builder, number, power * 4) )
|
||
builder.append(getPowerDisplay(Power.values()[power-1]));
|
||
power--;
|
||
}
|
||
extendToken(builder, number, 0);
|
||
}
|
||
|
||
private boolean extendToken(StringBuilder builder,
|
||
String number,
|
||
int suffix) {
|
||
|
||
assert builder != null && number.length() > suffix;
|
||
|
||
int len = number.length() - suffix;
|
||
int qian = len > 3 ? (int)(number.charAt(len-4)-'0') : -1;
|
||
int bai = len > 2 ? (int)(number.charAt(len-3)-'0') : -1;
|
||
int shi = len > 1 ? (int)(number.charAt(len-2)-'0') : -1;
|
||
int ind = (int)(number.charAt(len-1)-'0');
|
||
|
||
boolean nonZero = false; // true if any of the digits is not zero
|
||
if( qian == 0 ) {
|
||
if( bai > 0 || shi > 0 || ind > 0 )
|
||
builder.append(getConnectDisplay(Connect.Ling));
|
||
}
|
||
else if( qian > 0 ){
|
||
builder.append(getDigitDisplay(Digit.values()[qian]))
|
||
.append(getConnectDisplay(Connect.Qian));
|
||
nonZero = true;
|
||
}
|
||
|
||
if( bai == 0 ) {
|
||
if( qian > 0 && (shi > 0 || ind > 0) )
|
||
builder.append(getConnectDisplay(Connect.Ling));
|
||
}
|
||
else if( bai > 0 ){
|
||
builder.append(getDigitDisplay(Digit.values()[bai]))
|
||
.append(getConnectDisplay(Connect.Bai));
|
||
nonZero = true;
|
||
}
|
||
|
||
if( shi == 0 ) {
|
||
if( bai > 0 && ind > 0 )
|
||
builder.append(getConnectDisplay(Connect.Ling));
|
||
}
|
||
else if( shi > 0 ){
|
||
if( number.length() > 2 || shi != 1 )
|
||
builder.append(getDigitDisplay(Digit.values()[shi]));
|
||
builder.append(getConnectDisplay(Connect.Shi));
|
||
nonZero = true;
|
||
}
|
||
|
||
if( ind == 0 ){
|
||
boolean addZero = len == 1;
|
||
for(int i=1; addZero && i<=suffix; i++) {
|
||
if( number.charAt(i) != '0' )
|
||
addZero = false;
|
||
}
|
||
if( addZero ) builder.append(getConnectDisplay(Connect.Ling));
|
||
}
|
||
else {
|
||
builder.append(getDigitDisplay(Digit.values()[ind]));
|
||
nonZero = true;
|
||
}
|
||
return nonZero;
|
||
}
|
||
|
||
String getConnectDisplay(Connect connect) {
|
||
|
||
assert connect != null;
|
||
|
||
return type == Type.Simplified ?
|
||
connect.display :
|
||
connect.displayTraditional;
|
||
}
|
||
|
||
String getPowerDisplay(Power power) {
|
||
|
||
assert power != null;
|
||
|
||
return type == Type.Simplified ?
|
||
power.display :
|
||
power.displayTraditional;
|
||
}
|
||
|
||
String getDigitDisplay(Digit digit) {
|
||
|
||
assert digit != null;
|
||
|
||
return type == Type.Simplified ?
|
||
digit.display :
|
||
digit.displayTraditional;
|
||
}
|
||
}
|
||
}
|
||
|