Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 366c6157af |
@@ -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