Compare commits

1 Commits

Author SHA1 Message Date
zxq9 366c6157af WIP: still need to test against hz_format 2026-07-28 15:58:06 +09:00
@@ -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 >= '' && c <= '') {
sb.append((char) (c - '' + '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 >= '' && c <= '') ? (char) (c - '' + '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();
}
}