企業級系統常面臨 C# 服務使用 protobuf-net(含 bcl.proto 標準類型)與 Java 服務使用官方 protobuf-java 互通的場景。核心難點在於 bcl.Decimal 與 bcl.DateTime 這兩個 .NET 特有型別的精確映射。
bcl.Decimal 以 128 位元表示十進制數值:
hi (int32):高 32 位元lo (int64):低 64 位元signScale (int32):bit 0 為符號(0=正,1=負),bit 1-16 為小數位數組合形成 96 位元無符號整數 + 符號 + 縮放因子,對應 Java BigDecimal。
public static BigDecimal bclDecimalToBigDecimal(Bcl.Decimal bclDecimal) {
if (bclDecimal == null) return BigDecimal.ZERO;
// 1-2. 高低位轉大端位元組陣列
byte[] deBytes = new byte[12];
int2byte(deBytes, 0, bclDecimal.getHi()); // hi → 位元組 0-3
long2byte(deBytes, 4, bclDecimal.getLo()); // lo → 位元組 4-11
// 3. 解析 signScale
int signScale = bclDecimal.getSignScale();
int sign = (signScale & 1) == 1 ? -1 : 1; // bit 0
int scale = (signScale & 0xFFFE) >> 1; // bit 1-16
// 4-5. 建構 BigInteger → BigDecimal
BigInteger unscaled = new BigInteger(sign, deBytes);
return new BigDecimal(unscaled, scale);
}
public static Bcl.Decimal bigDecimalToBclDecimal(BigDecimal bigDecimal) {
if (bigDecimal == null) return null;
Bcl.Decimal.Builder builder = Bcl.Decimal.newBuilder();
BigInteger unscaled = bigDecimal.unscaledValue(); // 必須用未縮放值
int scale = bigDecimal.scale();
// 1. 整數轉 12 位元組大端陣列(左側補零)
byte[] deBytes = new byte[12];
byte[] src = unscaled.toByteArray();
System.arraycopy(src, 0, deBytes, 12 - src.length, src.length);
// 2. 切分 hi/lo
builder.setHi(bytes2int(deBytes, 0));
builder.setLo(bytes2long(deBytes, 4));
// 3. 編碼 signScale
int signScale = (unscaled.signum() < 0 ? 1 : 0) | (scale << 1);
builder.setSignScale(signScale);
return builder.build();
}
public final class BclUtil {
private static final long TICKS_PER_MILLISECOND = 10000L;
private static final int SIGN_SCALE_MASK = 0xFFFE; // bit 1-16
private BclUtil() {}
// --- 位元組轉換輔助(大端序)---
public static void int2byte(byte[] dst, int pos, int val) {
dst[pos] = (byte) (val >>> 24);
dst[pos + 1] = (byte) (val >>> 16);
dst[pos + 2] = (byte) (val >>> 8);
dst[pos + 3] = (byte) val;
}
public static void long2byte(byte[] dst, int pos, long val) {
int2byte(dst, pos, (int) (val >>> 32));
int2byte(dst, pos + 4, (int) val);
}
public static int bytes2int(byte[] src, int pos) {
return ((src[pos] & 0xFF) << 24) |
((src[pos+1] & 0xFF) << 16) |
((src[pos+2] & 0xFF) << 8) |
(src[pos+3] & 0xFF);
}
public static long bytes2long(byte[] src, int pos) {
return (((long) bytes2int(src, pos)) << 32) |
(bytes2int(src, pos + 4) & 0xFFFFFFFFL);
}
// --- 核心轉換 API ---
public static BigDecimal toBigDecimal(Bcl.Decimal src) { ... }
public static Bcl.Decimal toBclDecimal(BigDecimal src) { ... }
public static Date toDate(Bcl.DateTime src) { ... }
public static Bcl.DateTime toBclDateTime(Date src) { ... }
// --- DateTime 轉換(Ticks ↔ Millis)---
// .NET Ticks: 100ns 間隔從 0001-01-01 起算
// Java Date: 毫秒從 1970-01-01 起算
private static final long EPOCH_DIFF_TICKS = 621355968000000000L;
public static Date toDate(Bcl.DateTime dt) {
long ticks = (dt.getHigh() << 32) | (dt.getLow() & 0xFFFFFFFFL);
long millis = (ticks - EPOCH_DIFF_TICKS) / TICKS_PER_MILLISECOND;
return new Date(millis);
}
public static Bcl.DateTime toBclDateTime(Date date) {
long ticks = date.getTime() * TICKS_PER_MILLISECOND + EPOCH_DIFF_TICKS;
return Bcl.DateTime.newBuilder()
.setHigh((int) (ticks >>> 32))
.setLow((int) ticks)
.build();
}
}
| 測試案例 | 輸入 | 預期輸出 |
|---|---|---|
| 正數帶小數 | 123.456 | scale=3, unscaled=123456 |
| 負數 | -0.001 | sign=-1, scale=3 |
| 大數值 | 12345678901234567890.12345 | 96位整數無溢位 |
| DateTime 邊界 | 0001-01-01 / 9999-12-31 | Ticks 正確映射 |
unscaledValue() 而非 toBigInteger(),後者會套用 scalesign = (v & 1)、scale = (v >>> 1)