MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

民國年條碼解析:當你的系統要處理非標準日期格式

做 ERP 系統久了,你會發現一件事:每間工廠的條碼格式都是一門獨立的學問。

最近接到一個需求,要解析一種 21 碼的條碼,裡面藏了料號、民國年日期、還有流水號。聽起來不難對吧?但魔鬼藏在「民國年」和「這個日期到底代表什麼」裡面。

條碼長什麼樣

先看結構。這是一個 21 個字元的條碼,三段拼在一起,沒有分隔符號:

1
[料號 11碼][日期 6碼][流水號 4碼]

舉個例子:

1
A12345678901502250001

拆開來看:

  • A1234567890 — 料號(11 碼,英數混合)
  • 150225 — 日期(YYMMDD,YY 是民國年經過轉換的兩碼)
  • 0001 — 流水號(4 碼)

好,第一個問題來了:為什麼是民國年?

民國年:台灣製造業的日常

如果你沒在台灣的製造業待過,可能不知道民國年有多普遍。政府公文用民國年、很多 ERP 系統用民國年、工廠的條碼和批號也用民國年。

民國年轉西元年的公式很簡單:

1
西元年 = 民國年 + 1911

所以 114 就是 2025 年,113 是 2024 年。

但條碼裡的日期只給了 6 碼 YYMMDD,年份只有兩位數。現在是民國 115 年(2026),三位數的年份要怎麼塞進兩碼?

民國年的 Y2K 問題

這其實是台灣版的千年蟲。民國 100 年(2011 年)的時候,很多系統就已經碰過一次了。

不同廠商的解法不一樣:

  • 方案 A:年份欄位從 2 碼擴到 3 碼 → 條碼變 22 碼,但掃碼設備可能不支援
  • 方案 B:用西元年後兩碼代替 → 25 代表 2025,但跟民國 25 年怎麼區分?
  • 方案 C:民國年減去某個 offset → 例如 YY = 民國年 - 100,所以 115 年 = 15

我碰到的這個案例用的是 方案 C 的變體:

1
YY + 2011 = 西元年

也就是說,條碼裡的 14 代表的是民國 114 年(2025 年),計算方式是 14 + 2011 = 2025。這其實等價於「民國年 - 100 之後取兩碼,再 + 1911 + 100」,只是合併成一步了。

所以 150225 正確的拆法是:

1
2
3
YY = 1515 + 2011 = 2026
MM = 022
DD = 2525

2026 年 2 月 25 日。合理了。

更刺激的:同一個日期欄位,兩種含義

拆完日期之後,下一個問題:這個日期是製造日期還是有效期限?

答案是:看情況。

規則是這樣的:

  • 如果日期 ≤ 今天 → 這是製造日期
  • 如果日期 > 今天 → 這是有效期限

對,同一個欄位,根據跟今天的比較結果,代表完全不同的意思。

你可能會想:「這也太扯了吧?」但在製造業的批號管理裡,這種設計其實有它的道理。條碼是在生產線上印的,不可能同時印兩個日期。所以它把兩個資訊壓縮到同一個欄位裡,用「時間先後」來隱含判斷。

寫成程式碼大概是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface BarcodeInfo {
materialCode: string;
date: Date;
dateType: 'manufacture' | 'expiry';
serialNumber: string;
}

function parseBarcode(barcode: string): BarcodeInfo {
if (barcode.length !== 21) {
throw new Error(`Invalid barcode length: ${barcode.length}`);
}

const materialCode = barcode.substring(0, 11);
const dateStr = barcode.substring(11, 17);
const serialNumber = barcode.substring(17, 21);

// 解析民國年日期
const yy = parseInt(dateStr.substring(0, 2), 10);
const mm = parseInt(dateStr.substring(2, 4), 10);
const dd = parseInt(dateStr.substring(4, 6), 10);
const year = yy + 2011;

const date = new Date(year, mm - 1, dd);
const today = new Date();
today.setHours(0, 0, 0, 0);

const dateType = date <= today ? 'manufacture' : 'expiry';

return { materialCode, date, dateType, serialNumber };
}

邊界案例:今天的日期算哪邊?

你可能注意到了,我用的是 date <= today,也就是今天算製造日期

這個「等於」要特別小心。如果今天就是條碼上的日期,它有可能是:

  • 今天生產的東西(製造日期 = 今天)→ 合理
  • 今天到期的東西(有效期限 = 今天)→ 也合理

在這個案例裡,業務端確認「今天」算製造日期。但不同客戶可能有不同定義,這種邊界一定要問清楚,不能自己猜。

另一個坑:月份和日期的合法性

YYMM DD 格式裡,你不能假設 MM 一定是 01-12、DD 一定是 01-31。條碼有可能是人工輸入的,也可能是機器印歪的。所以解析完之後要驗證:

1
2
3
4
5
6
7
8
function isValidDate(year: number, month: number, day: number): boolean {
const date = new Date(year, month - 1, day);
return (
date.getFullYear() === year &&
date.getMonth() === month - 1 &&
date.getDate() === day
);
}

這個技巧利用了 JavaScript Date 的自動修正特性。如果你傳 new Date(2025, 1, 30)(2 月 30 日),它會自動變成 3 月 2 日。透過比對 input 和 output 是否一致,就能判斷原始日期是否合法。

通用化的條碼解析思路

做了幾間工廠的條碼解析之後,我整理出一個通用的處理框架:

第一步:搞清楚格式規格

不要猜。跟客戶要正式的條碼規格文件。如果沒有文件(很常見),就拿實際的條碼樣本反推,然後跟客戶確認。

需要確認的事:

  • 總長度(固定長度 or 變動長度?)
  • 各段的位置和長度
  • 日期格式(民國年 or 西元年?YYMMDD or YYYYMMDD?)
  • 有沒有分隔符號
  • 有沒有 check digit

第二步:寫解析器,但保持彈性

不要把格式硬編碼在 parser 裡。用 config 或 schema 定義格式,讓同一個 parser 能處理不同客戶的條碼。

1
2
3
4
5
6
7
8
9
10
interface BarcodeSchema {
totalLength: number;
segments: {
name: string;
start: number;
length: number;
type: 'string' | 'date' | 'number';
dateFormat?: 'rocYYMMDD' | 'YYYYMMDD' | 'YYMMDD';
}[];
}

第三步:驗證,驗證,再驗證

掃碼的環境通常很惡劣——工廠灰塵多、條碼可能被油汙蓋住、印刷品質參差不齊。你的 parser 要能優雅地處理:

  • 長度不對
  • 日期不合法
  • 非預期的字元(比如空白或特殊符號)

錯誤訊息要具體。不要只說「條碼格式錯誤」,要說「條碼長度應為 21 碼,實際為 18 碼」或「日期 140230 不是合法日期」。操作人員在產線上沒時間猜你的錯誤訊息是什麼意思。

結語

條碼解析看起來是很小的功能,但它牽涉到的東西比想像中多:日期格式的歷史包袱、業務邏輯的隱含規則、產線環境的容錯需求。

每次碰到這種「聽起來很簡單」的需求,我都會提醒自己:簡單的需求不代表簡單的實作。特別是在製造業,那些看似不合理的設計背後,通常都有一段你不知道的歷史故事。

如果你也在做製造業相關的系統,希望這篇能幫你少踩一些坑。至少下次看到民國年條碼的時候,不會對著 6 碼日期發呆半天 😂