From 366c6157af59ab8df3ad91999a228ec4a5ae99c3 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 28 Jul 2026 15:58:06 +0900 Subject: [PATCH] WIP: still need to test against hz_format --- .../gajumaru/core/formatting/GajuFormat.java | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java diff --git a/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java b/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java new file mode 100644 index 0000000..2917fe7 --- /dev/null +++ b/gajumaru-core/src/main/java/swiss/qpq/gajumaru/core/formatting/GajuFormat.java @@ -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 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(); + } +}