WIP: still need to test against hz_format
This commit is contained in:
@@ -0,0 +1,242 @@
|
|||||||
|
package swiss.qpq.gajumaru.core.formatting;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class GajuFormat {
|
||||||
|
public enum Type { US, JP, METRIC, LEGACY }
|
||||||
|
public enum Unit { GAJU, PUCK }
|
||||||
|
public record FormatSpec(Type type, Unit unit, char separator, int span) {}
|
||||||
|
|
||||||
|
private GajuFormat() {}
|
||||||
|
|
||||||
|
private static final String GAJU_MARK = "木";
|
||||||
|
private static final String PUCK_MARK = "本";
|
||||||
|
private static final int FRACTION_LEN = 18;
|
||||||
|
|
||||||
|
private static final String[] JP_RANKS = {"", "万", "億", "兆", "京", "垓", "秭", "穣", "溝", "澗", "正", "載", "極"};
|
||||||
|
private static final String[] METRIC_RANKS = {"", "k", "m", "G", "T", "P", "E", "Z", "Y", "r", "Q"};
|
||||||
|
private static final String[] LEGACY_RANKS = {"", "k", "m", "b", "T", "q", "E", "Z", "Y", "r", "Q"};
|
||||||
|
|
||||||
|
public static String amount(FormatSpec spec, byte[] puckBytes) {
|
||||||
|
String base10 = GmMonetaryFormatter.bytesToBase10PuckString(puckBytes);
|
||||||
|
boolean isNegative = puckBytes.length > 0 && (puckBytes[0] & 0x80) != 0; // Handle sign tracking if applicable
|
||||||
|
|
||||||
|
return switch (spec.type()) {
|
||||||
|
case US -> formatWestern(spec, base10);
|
||||||
|
case JP -> formatMyriad(spec, base10, JP_RANKS, "-");
|
||||||
|
case METRIC -> formatBestern(spec, base10, METRIC_RANKS);
|
||||||
|
case LEGACY -> formatBestern(spec, base10, LEGACY_RANKS);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatWestern(FormatSpec spec, String base10) {
|
||||||
|
if (spec.unit() == Unit.PUCK) {
|
||||||
|
return PUCK_MARK + chunkString(base10, spec.separator(), spec.span(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split at the 18-digit Gaju <-> Puck boundary
|
||||||
|
String[] split = splitPucksAndGajus(base10);
|
||||||
|
String gajuStr = chunkString(split[0], spec.separator(), spec.span(), false);
|
||||||
|
String puckStr = cleanTrailingZeros(split[1]);
|
||||||
|
|
||||||
|
if (puckStr.isEmpty()) {
|
||||||
|
return GAJU_MARK + gajuStr;
|
||||||
|
}
|
||||||
|
return GAJU_MARK + gajuStr + "." + chunkString(puckStr, spec.separator(), spec.span(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatMyriad(FormatSpec spec, String base10, String[] ranks, String negSign) {
|
||||||
|
if (spec.unit() == Unit.PUCK) {
|
||||||
|
return processRanks(base10, ranks, 4, PUCK_MARK);
|
||||||
|
}
|
||||||
|
String[] split = splitPucksAndGajus(base10);
|
||||||
|
String gajuFormatted = processRanks(split[0], ranks, 4, GAJU_MARK);
|
||||||
|
String puckClean = cleanTrailingZeros(split[1]);
|
||||||
|
|
||||||
|
if (puckClean.isEmpty()) return gajuFormatted;
|
||||||
|
return gajuFormatted + " " + processRanks(puckClean, ranks, 4, PUCK_MARK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatBestern(FormatSpec spec, String base10, String[] ranks) {
|
||||||
|
if (spec.unit() == Unit.PUCK) {
|
||||||
|
return PUCK_MARK + processRanks(base10, ranks, 3, "P");
|
||||||
|
}
|
||||||
|
String[] split = splitPucksAndGajus(base10);
|
||||||
|
String gajuPart = GAJU_MARK + processRanks(split[0], ranks, 3, "G");
|
||||||
|
String puckClean = cleanTrailingZeros(split[1]);
|
||||||
|
|
||||||
|
if (puckClean.isEmpty()) return gajuPart;
|
||||||
|
return gajuPart + " " + processRanks(puckClean, ranks, 3, "P");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] splitPucksAndGajus(String base10) {
|
||||||
|
if (base10.length() <= FRACTION_LEN) {
|
||||||
|
char[] zeros = new char[FRACTION_LEN - base10.length()];
|
||||||
|
Arrays.fill(zeros, '0');
|
||||||
|
return new String[]{"0", new String(zeros) + base10};
|
||||||
|
}
|
||||||
|
int cut = base10.length() - FRACTION_LEN;
|
||||||
|
return new String[]{base10.substring(0, cut), base10.substring(cut)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cleanTrailingZeros(String str) {
|
||||||
|
int idx = str.length() - 1;
|
||||||
|
while (idx >= 0 && str.charAt(idx) == '0') idx--;
|
||||||
|
return idx < 0 ? "" : str.substring(0, idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String chunkString(String str, char sep, int span, boolean isFraction) {
|
||||||
|
if (str.isEmpty()) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int len = str.length();
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
if (i > 0 && (isFraction ? i % span == 0 : (len - i) % span == 0)) {
|
||||||
|
sb.append(sep);
|
||||||
|
}
|
||||||
|
sb.append(str.charAt(i));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String processRanks(String numericStr, String[] ranks, int span, String endingSymbol) {
|
||||||
|
if (numericStr.equals("0") || numericStr.isEmpty()) return "0 " + endingSymbol;
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
int len = numericStr.length();
|
||||||
|
int rankIndex = 0;
|
||||||
|
|
||||||
|
for (int i = len; i > 0; i -= span) {
|
||||||
|
int start = Math.max(0, i - span);
|
||||||
|
String chunk = numericStr.substring(start, i);
|
||||||
|
long val = Long.parseLong(chunk);
|
||||||
|
if (val > 0) {
|
||||||
|
result.insert(0, val + ranks[rankIndex] + " ");
|
||||||
|
}
|
||||||
|
rankIndex++;
|
||||||
|
}
|
||||||
|
return result.toString().trim() + endingSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long ONE_GAJU_FACTOR = 1_000_000_000_000_000_000L;
|
||||||
|
|
||||||
|
private static final Map<Character, Long> MULTIPLIERS = new HashMap<>();
|
||||||
|
static {
|
||||||
|
// Warhammer 40k heresy meets Base 10k primacy
|
||||||
|
MULTIPLIERS.put('万', 10_000L);
|
||||||
|
MULTIPLIERS.put('億', 100_000_000L);
|
||||||
|
MULTIPLIERS.put('兆', 1_000_000_000_000L);
|
||||||
|
MULTIPLIERS.put('京', 10_000_000_000_000_000L);
|
||||||
|
// NOTE: Higher ranges (垓, 秭) require BigInteger parsing if values exceed Long.MAX_VALUE.
|
||||||
|
// For standard UI entries, Long tracking handles up to 9 Quintillion Pucks
|
||||||
|
|
||||||
|
// SI / Heresy notations mapping rules
|
||||||
|
MULTIPLIERS.put('k', 1_000L);
|
||||||
|
MULTIPLIERS.put('m', 1_000_000L);
|
||||||
|
MULTIPLIERS.put('G', 1_000_000_000L);
|
||||||
|
MULTIPLIERS.put('g', 1_000_000_000L);
|
||||||
|
MULTIPLIERS.put('b', 1_000_000_000L); // Hertical billion flag
|
||||||
|
MULTIPLIERS.put('T', 1_000_000_000_000L);
|
||||||
|
MULTIPLIERS.put('t', 1_000_000_000_000L);
|
||||||
|
MULTIPLIERS.put('P', 1_000_000_000_000_000L);
|
||||||
|
MULTIPLIERS.put('p', 1_000_000_000_000_000L);
|
||||||
|
MULTIPLIERS.put('q', 1_000_000_000_000_000L); // Heretical quadrillion flag
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] read(String rawInput) {
|
||||||
|
if (rawInput == null || rawInput.isEmpty()) throw new IllegalArgumentException("Empty string context");
|
||||||
|
|
||||||
|
boolean isNegative = false;
|
||||||
|
String input = rawInput.trim();
|
||||||
|
if (input.startsWith("-") || input.startsWith("-") || input.startsWith("-")) {
|
||||||
|
isNegative = true;
|
||||||
|
input = input.substring(1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean forceGaju = input.startsWith("木");
|
||||||
|
boolean forcePuck = input.startsWith("本");
|
||||||
|
if (forceGaju || forcePuck) {
|
||||||
|
input = input.substring(1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
input = input.replace(",", "").replace("_", "").replace(" ", "");
|
||||||
|
|
||||||
|
if (input.contains(".")) {
|
||||||
|
int dotIdx = input.indexOf('.');
|
||||||
|
String gajuString = input.substring(0, dotIdx);
|
||||||
|
String puckString = input.substring(dotIdx + 1);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(puckString);
|
||||||
|
while (sb.length() < 18) sb.append('0');
|
||||||
|
if (sb.length() > 18) throw new IllegalArgumentException("Precision overflow error");
|
||||||
|
|
||||||
|
byte[] gajuBytes = GmMonetaryParser.parseBase10ToBytes(standardizeDigits(gajuString));
|
||||||
|
byte[] puckBytes = GmMonetaryParser.parseBase10ToBytes(standardizeDigits(sb.toString()));
|
||||||
|
|
||||||
|
return addAtomicPuckArrays(multiplyByGajuFactor(gajuBytes), puckBytes, isNegative);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseRankedString(input, forcePuck, isNegative);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String standardizeDigits(String raw) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < raw.length(); i++) {
|
||||||
|
char c = raw.charAt(i);
|
||||||
|
if (c >= '0' && c <= '9') {
|
||||||
|
sb.append((char) (c - '0' + '0')); // full-width -> half-width
|
||||||
|
} else {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] parseRankedString(String input, boolean forcePuck, boolean isNegative) {
|
||||||
|
byte[] totalPucks = new byte[]{0};
|
||||||
|
StringBuilder currentDigits = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < input.length(); i++) {
|
||||||
|
char c = input.charAt(i);
|
||||||
|
char standardC = (c >= '0' && c <= '9') ? (char) (c - '0' + '0') : c;
|
||||||
|
|
||||||
|
if (Character.isDigit(standardC)) {
|
||||||
|
currentDigits.append(standardC);
|
||||||
|
} else if (MULTIPLIERS.containsKey(standardC)) {
|
||||||
|
if (currentDigits.length() == 0) continue;
|
||||||
|
|
||||||
|
long multiplier = MULTIPLIERS.get(standardC);
|
||||||
|
long sliceVal = Long.parseLong(currentDigits.toString()) * multiplier;
|
||||||
|
byte[] blockBytes = GmMonetaryParser.parseBase10ToBytes(Long.toString(sliceVal));
|
||||||
|
|
||||||
|
// Unless explicitly marked as a standalone Puck entry, assume Gaju
|
||||||
|
if (!forcePuck && standardC != 'P' && standardC != 'p') {
|
||||||
|
blockBytes = multiplyByGajuFactor(blockBytes);
|
||||||
|
}
|
||||||
|
totalPucks = addAtomicPuckArrays(totalPucks, blockBytes, false);
|
||||||
|
currentDigits.setLength(0); // Flush buffers!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDigits.length() > 0) {
|
||||||
|
byte[] remainingBytes = GmMonetaryParser.parseBase10ToBytes(currentDigits.toString());
|
||||||
|
if (!forcePuck) remainingBytes = multiplyByGajuFactor(remainingBytes);
|
||||||
|
totalPucks = addAtomicPuckArrays(totalPucks, remainingBytes, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalPucks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] multiplyByGajuFactor(byte[] base) {
|
||||||
|
java.math.BigInteger b = new java.math.BigInteger(base);
|
||||||
|
return b.multiply(java.math.BigInteger.valueOf(ONE_GAJU_FACTOR)).toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] addAtomicPuckArrays(byte[] a, byte[] b, boolean makeNegative) {
|
||||||
|
java.math.BigInteger biA = new java.math.BigInteger(a);
|
||||||
|
java.math.BigInteger biB = new java.math.BigInteger(b);
|
||||||
|
java.math.BigInteger sum = biA.add(biB);
|
||||||
|
if (makeNegative) sum = sum.negate();
|
||||||
|
return sum.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user