net.time4j.format.NumberSystem Maven / Gradle / Ivy
/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild,
* -----------------------------------------------------------------------
* This file (NumberSystem.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see .
* -----------------------------------------------------------------------
*/
package net.time4j.format;
import net.time4j.base.MathUtils;
import java.util.Locale;
/**
* Defines the number system.
*
* Attention: This enum can only handle non-negative integers.
*
* @author Meno Hochschild
* @since 3.11/4.8
*/
/*[deutsch]
* Definiert ein Zahlsystem.
*
* Achtung: Dieses Enum kann nur nicht-negative Ganzzahlen verarbeiten.
*
* @author Meno Hochschild
* @since 3.11/4.8
*/
public enum NumberSystem {
//~ Statische Felder/Initialisierungen --------------------------------
/**
* Arabic numbers with the decimal digits 0-9 (default setting).
*
* This number system is used worldwide. Direct conversion of negative integers is not supported.
*/
/*[deutsch]
* Arabische Zahlen mit den Dezimalziffern 0-9 (Standardeinstellung).
*
* Dieses Zahlsystem wird weltweit verwendet. Die direkte Konversion von negativen Ganzzahlen
* wird jedoch nicht unterstützt.
*/
ARABIC() {
@Override
public String toNumeral(int number) {
if (number < 0) {
throw new IllegalArgumentException("Cannot convert: " + number);
}
return Integer.toString(number);
}
@Override
public int toInteger(String numeral, Leniency leniency) {
int result = Integer.parseInt(numeral);
if (result < 0) {
throw new NumberFormatException("Cannot convert negative number: " + numeral);
}
return result;
}
@Override
public boolean contains(char digit) {
return ((digit >= '0') && (digit <= '9'));
}
@Override
public String getDigits() {
return "0123456789";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* Arabic-Indic numbers (used in many Arabic countries).
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Arabisch-indische Zahlen (in vielen arabischen Ländern verwendet).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
ARABIC_INDIC() {
@Override
public String getDigits() {
return "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* Extended Arabic-Indic numbers (used for example in Iran).
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Erweiterte arabisch-indische Zahlen (zum Beispiel im Iran).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
ARABIC_INDIC_EXT() {
@Override
public String getDigits() {
return "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* The Bengali digits used in parts of India.
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Die Bengalii-Ziffern (in Teilen von Indien verwendet).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
BENGALI() {
@Override
public String getDigits() {
return "\u09E6\u09E7\u09E8\u09E9\u09EA\u09EB\u09EC\u09ED\u09EE\u09EF";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* The Devanagari digits used in parts of India.
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Die Devanagari-Ziffern (in Teilen von Indien verwendet).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
DEVANAGARI() {
@Override
public String getDigits() {
return "\u0966\u0967\u0968\u0969\u096A\u096B\u096C\u096D\u096E\u096F";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* Ethiopic numerals (always positive).
*
* See also A Look at Ethiopic Numerals.
* Attention: This enum is not a decimal system.
*/
/*[deutsch]
* Äthiopische Numerale (immer positiv).
*
* Siehe auch A Look at Ethiopic Numerals.
* Achtung: Dieses Enum ist kein Dezimalsystem.
*/
ETHIOPIC() {
@Override
public String toNumeral(int number) {
if (number < 1) {
throw new IllegalArgumentException("Can only convert positive numbers: " + number);
}
String value = String.valueOf(number);
int n = value.length() - 1;
if ((n % 2) == 0) {
value = "0" + value;
n++;
}
StringBuilder numeral = new StringBuilder();
char asciiOne, asciiTen, ethioOne, ethioTen;
for (int place = n; place >= 0; place--) {
ethioOne = ethioTen = 0x0;
asciiTen = value.charAt(n - place);
place--;
asciiOne = value.charAt(n - place);
if (asciiOne != '0') {
ethioOne = (char) ((int) asciiOne + (ETHIOPIC_ONE - '1'));
}
if (asciiTen != '0') {
ethioTen = (char) ((int) asciiTen + (ETHIOPIC_TEN - '1'));
}
int pos = (place % 4) / 2;
char sep = 0x0;
if (place != 0) {
sep = (
(pos != 0)
? (((ethioOne != 0x0) || (ethioTen != 0x0)) ? ETHIOPIC_HUNDRED : 0x0)
: ETHIOPIC_TEN_THOUSAND);
}
if ((ethioOne == ETHIOPIC_ONE) && (ethioTen == 0x0) && (n > 1)) {
if ((sep == ETHIOPIC_HUNDRED) || ((place + 1) == n)) {
ethioOne = 0x0;
}
}
if (ethioTen != 0x0) {
numeral.append(ethioTen);
}
if (ethioOne != 0x0) {
numeral.append(ethioOne);
}
if (sep != 0x0) {
numeral.append(sep);
}
}
return numeral.toString();
}
@Override
public int toInteger(String numeral, Leniency leniency) {
int total = 0;
int sum = 0;
int factor = 1;
boolean hundred = false;
boolean thousand = false;
int n = numeral.length() - 1;
for (int place = n; place >= 0; place--) {
char digit = numeral.charAt(place);
if ((digit >= ETHIOPIC_ONE) && (digit < ETHIOPIC_TEN)) { // 1-9
sum += (1 + digit - ETHIOPIC_ONE);
} else if ((digit >= ETHIOPIC_TEN) && (digit < ETHIOPIC_HUNDRED)) { // 10-90
sum += ((1 + digit - ETHIOPIC_TEN) * 10);
} else if (digit == ETHIOPIC_TEN_THOUSAND) {
if (hundred && (sum == 0)) {
sum = 1;
}
total = addEthiopic(total, sum, factor);
if (hundred) {
factor *= 100;
} else {
factor *= 10000;
}
sum = 0;
hundred = false;
thousand = true;
} else if (digit == ETHIOPIC_HUNDRED) {
total = addEthiopic(total, sum, factor);
factor *= 100;
sum = 0;
hundred = true;
thousand = false;
}
}
if ((hundred || thousand) && (sum == 0)) {
sum = 1;
}
total = addEthiopic(total, sum, factor);
return total;
}
@Override
public boolean contains(char digit) {
return ((digit >= ETHIOPIC_ONE) && (digit <= ETHIOPIC_TEN_THOUSAND));
}
@Override
public String getDigits() {
return
"\u1369\u136A\u136B\u136C\u136D\u136E\u136F\u1370\u1371"
+ "\u1372\u1373\u1374\u1375\u1376\u1377\u1378\u1379\u137A"
+ "\u137B\u137C";
}
@Override
public boolean isDecimal() {
return false;
}
},
/**
* The Gujarati digits used in parts of India.
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Die Gujarati-Ziffern (in Teilen von Indien verwendet).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
GUJARATI() {
@Override
public String getDigits() {
return "\u0AE6\u0AE7\u0AE8\u0AE9\u0AEA\u0AEB\u0AEC\u0AED\u0AEE\u0AEF";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* Traditional number system used by Khmer people in Cambodia.
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Traditionelles Zahlsystem vom Khmer-Volk in Kambodscha verwendet.
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
KHMER() {
@Override
public String getDigits() {
return "\u17E0\u17E1\u17E2\u17E3\u17E4\u17E5\u17E6\u17E7\u17E8\u17E9";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* The number system used in Myanmar (Burma).
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Das traditionelle Zahlsystem von Myanmar (Burma).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
MYANMAR() {
@Override
public String getDigits() {
return "\u1040\u1041\u1042\u1043\u1044\u1045\u1046\u1047\u1048\u1049";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* Roman numerals in range 1-3999.
*
* If the leniency is strict then parsing of Roman numerals will only follow modern usage.
* The parsing is always case-insensitive. See also
* Roman Numerals.
*/
/*[deutsch]
* Römische Numerale im Wertbereich 1-3999.
*
* Wenn die Nachsichtigkeit strikt ist, wird das Interpretieren von römischen Numeralen
* nur dem modernen Gebrauch folgen. Die Groß- und Kleinschreibung spielt keine Rolle. Siehe
* auch Roman Numerals.
*/
ROMAN() {
@Override
public String toNumeral(int number) {
if ((number < 1) || (number > 3999)) {
throw new IllegalArgumentException("Out of range (1-3999): " + number);
}
int n = number;
StringBuilder roman = new StringBuilder();
for (int i = 0; i < NUMBERS.length; i++) {
while (n >= NUMBERS[i]) {
roman.append(LETTERS[i]);
n -= NUMBERS[i];
}
}
return roman.toString();
}
@Override
public int toInteger(String numeral, Leniency leniency) {
if (numeral.isEmpty()) {
throw new NumberFormatException("Empty Roman numeral.");
}
String ucase = numeral.toUpperCase(Locale.US); // use ASCII-base
boolean strict = leniency.isStrict();
int len = numeral.length();
int i = 0;
int total = 0;
while (i < len) {
char roman = ucase.charAt(i);
int value = getValue(roman);
int j = i + 1;
int count = 1;
if (j == len) {
total += value;
} else {
while (j < len) {
char test = ucase.charAt(j);
j++;
if (test == roman) {
count++;
if ((count >= 4) && strict) {
throw new NumberFormatException(
"Roman numeral contains more than 3 equal letters in sequence: " + numeral);
}
if (j == len) {
total += (value * count);
}
} else {
int next = getValue(test);
if (next < value) {
total += (value * count);
j--;
} else { // next > value
if (strict) {
if ((count > 1) || !isValidRomanCombination(roman, test)) {
throw new NumberFormatException("Not conform with modern usage: " + numeral);
}
}
total = total + next - (value * count);
}
break;
}
}
}
i = j;
}
if (total > 3999) {
throw new NumberFormatException("Roman numbers bigger than 3999 not supported.");
} else if (strict) {
if (total >= 900 && ucase.contains("DCD")) {
throw new NumberFormatException("Roman number contains invalid sequence DCD.");
}
if (total >= 90 && ucase.contains("LXL")) {
throw new NumberFormatException("Roman number contains invalid sequence LXL.");
}
if (total >= 9 && ucase.contains("VIV")) {
throw new NumberFormatException("Roman number contains invalid sequence VIV.");
}
}
return total;
}
@Override
public boolean contains(char digit) {
char c = Character.toUpperCase(digit);
return ((c == 'I') || (c == 'V') || (c == 'X') || (c == 'L') || (c == 'C') || (c == 'D') || (c == 'M'));
}
@Override
public String getDigits() {
return "IVXLCDM";
}
@Override
public boolean isDecimal() {
return false;
}
},
/**
* The Telugu digits used in parts of India.
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Die Telugu-Ziffern (in Teilen von Indien verwendet).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
TELUGU() {
@Override
public String getDigits() {
return "\u0C66\u0C67\u0C68\u0C69\u0C6A\u0C6B\u0C6C\u0C6D\u0C6E\u0C6F";
}
@Override
public boolean isDecimal() {
return true;
}
},
/**
* The Thai digits used in Thailand (Siam).
*
* Note: Must not be negative.
*
* @since 3.23/4.19
*/
/*[deutsch]
* Die Thai-Ziffern (in Thailand verwendet).
*
* Hinweis: Darf nicht negativ sein.
*
* @since 3.23/4.19
*/
THAI() {
@Override
public String getDigits() {
return "\u0E50\u0E51\u0E52\u0E53\u0E54\u0E55\u0E56\u0E57\u0E58\u0E59";
}
@Override
public boolean isDecimal() {
return true;
}
};
private static final char ETHIOPIC_ONE = 0x1369; // 1, 2, ..., 8, 9
private static final char ETHIOPIC_TEN = 0x1372; // 10, 20, ..., 80, 90
private static final char ETHIOPIC_HUNDRED = 0x137B;
private static final char ETHIOPIC_TEN_THOUSAND = 0x137C;
private static final int[] NUMBERS = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
private static final String[] LETTERS = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
//~ Methoden ----------------------------------------------------------
/**
* Converts given integer to a text numeral.
*
* @param number number to be displayed as text
* @return text numeral
* @throws IllegalArgumentException if the conversion is not supported for given number
* @since 3.11/4.8
*/
/*[deutsch]
* Konvertiert die angegebene Zahl zu einem Textnumeral.
*
* @param number number to be displayed as text
* @return text numeral
* @throws IllegalArgumentException if the conversion is not supported for given number
* @since 3.11/4.8
*/
public String toNumeral(int number) {
if (this.isDecimal() && (number >= 0)) {
int delta = this.getDigits().charAt(0) - '0';
String standard = Integer.toString(number);
StringBuilder numeral = new StringBuilder();
for (int i = 0, n = standard.length(); i < n; i++) {
int codepoint = standard.charAt(i) + delta;
numeral.append((char) codepoint);
}
return numeral.toString();
} else {
throw new IllegalArgumentException("Cannot convert: " + number);
}
}
/**
* Converts given text numeral to an integer in smart mode.
*
* @param numeral text numeral to be evaluated as number
* @return integer
* @throws IllegalArgumentException if given number has wrong format
* @throws ArithmeticException if int-range overflows
* @since 3.11/4.8
*/
/*[deutsch]
* Konvertiert das angegebene Numeral zu einer Ganzzahl im SMART-Modus.
*
* @param numeral text numeral to be evaluated as number
* @return integer
* @throws IllegalArgumentException if given number has wrong format
* @throws ArithmeticException if int-range overflows
* @since 3.11/4.8
*/
public final int toInteger(String numeral) {
return this.toInteger(numeral, Leniency.SMART);
}
/**
* Converts given text numeral to an integer.
*
* In most cases, the leniency will not be taken into account, but parsing of some odd roman numerals
* can be enabled in non-strict mode (for example: IIXX instead of XVIII).
*
* @param numeral text numeral to be evaluated as number
* @param leniency determines how lenient the parsing of given numeral should be
* @return integer
* @throws IllegalArgumentException if given number has wrong format
* @throws ArithmeticException if int-range overflows
* @since 3.15/4.12
*/
/*[deutsch]
* Konvertiert das angegebene Numeral zu einer Ganzzahl.
*
* In den meisten Fällen wird das Nachsichtigkeitsargument nicht in Betracht gezogen. Aber die
* Interpretation von nicht dem modernen Gebrauch entsprechenden römischen Numeralen kann im
* nicht-strikten Modus erfolgen (zum Beispiel IIXX statt XVIII).
*
* @param numeral text numeral to be evaluated as number
* @param leniency determines how lenient the parsing of given numeral should be
* @return integer
* @throws IllegalArgumentException if given number has wrong format
* @throws ArithmeticException if int-range overflows
* @since 3.15/4.12
*/
public int toInteger(
String numeral,
Leniency leniency
) {
if (this.isDecimal()) {
int delta = this.getDigits().charAt(0) - '0';
StringBuilder standard = new StringBuilder();
for (int i = 0, n = numeral.length(); i < n; i++) {
int codepoint = numeral.charAt(i) - delta;
standard.append((char) codepoint);
}
int result = Integer.parseInt(standard.toString());
if (result < 0) {
throw new NumberFormatException("Cannot convert negative number: " + numeral);
}
return result;
} else {
throw new NumberFormatException("Cannot convert: " + numeral);
}
}
/**
* Does this number system contains given digit char?
*
* @param digit numerical char to be checked
* @return boolean
* @since 3.11/4.8
*/
/*[deutsch]
* Enthält dieses Zahlensystem die angegebene Ziffer?
*
* @param digit numerical char to be checked
* @return boolean
* @since 3.11/4.8
*/
public boolean contains(char digit) {
String digits = this.getDigits();
for (int i = 0, n = digits.length(); i < n; i++) {
if (digits.charAt(i) == digit) {
return true;
}
}
return false;
}
/**
* Defines all digit characters from the smallest to the largest one.
*
* Note: If letters are used as digits then the upper case will be used.
*
* @return String containing all valid digit characters in ascending order
* @since 3.23/4.19
*/
/*[deutsch]
* Definiert alle gültigen Ziffernsymbole von der kleinsten bis zur
* größten Ziffer.
*
* Hinweis: Wenn Buchstaben als Ziffern verwendet werden, dann wird
* die Großschreibung angewandt.
*
* @return String containing all valid digit characters in ascending order
* @since 3.23/4.19
*/
public String getDigits() {
throw new AbstractMethodError();
}
/**
* Does this number system describe a decimal system where the digits can be mapped to the range 0-9?
*
* @return boolean
* @since 3.23/4.19
*/
/*[deutsch]
* Beschreibt dieses Zahlensystem ein Dezimalsystem, dessen Ziffern sich auf den Bereich 0-9 abbilden
* lassen?
*
* @return boolean
* @since 3.23/4.19
*/
public boolean isDecimal() {
throw new AbstractMethodError();
}
private static int addEthiopic(
int total,
int sum,
int factor
) {
return MathUtils.safeAdd(total, MathUtils.safeMultiply(sum, factor));
}
private static int getValue(char roman) {
switch (roman) {
case 'I':
return 1;
case 'V':
return 5;
case 'X':
return 10;
case 'L':
return 50;
case 'C':
return 100;
case 'D':
return 500;
case 'M':
return 1000;
default:
throw new NumberFormatException("Invalid Roman digit: " + roman);
}
}
private static boolean isValidRomanCombination(
char previous,
char next
) {
switch (previous) {
case 'C':
return ((next == 'M') || (next == 'D'));
case 'X':
return ((next == 'C') || (next == 'L'));
case 'I':
return ((next == 'X') || (next == 'V'));
default:
return false;
}
}
}