電腦搬進店裡那天,老闆其實先問過一句:
用Excel不行嗎?我以前記帳都用那個。
第三天筆者選了C# Console,理由寫得很漂亮。但老闆那句話不能用一張評估表打發掉——他問的不是「哪個比較專業」,是「這件事需要寫程式嗎」。
所以今天真的做一份試算表出來,用同一組種子資料,把第三天那五個櫃檯動作全部做一遍,再做一次第四天測試裡最重要的那件事:借出中的書再借一次。
結論先說:現在行。 而且月底統計比Console版還好用。
試算表要有資料才試得出來。今天開一個小工具專案:
dotnet new console -n Ironman.SeedData.Cli -o tools/Ironman.SeedData.Cli
dotnet sln add tools/Ironman.SeedData.Cli
dotnet add tools/Ironman.SeedData.Cli reference src/Ironman.SeedData
核心是一個switch,一次印一張表:
static bool PrintCsv(SeedDataSet data, string table)
{
switch (table.ToLowerInvariant())
{
case "loans":
Console.WriteLine("會員編號,ISBN,借出日期,歸還日期");
foreach (var l in data.Loans)
{
Console.WriteLine($"{l.MemberId},{l.Isbn},{l.LoanDate:yyyy-MM-dd},{(l.IsReturned ? l.ReturnDate!.Value.ToString("yyyy-MM-dd") : "")}");
}
return true;
// books、members 同樣的形狀,不重貼
default:
return false;
}
}
三張表分三次印:
dotnet run --project tools/Ironman.SeedData.Cli -- csv S books > books.csv
dotnet run --project tools/Ironman.SeedData.Cli -- csv S members > members.csv
dotnet run --project tools/Ironman.SeedData.Cli -- csv S loans > loans.csv
這個工具只寫到標準輸出,存檔交給shell的重導向。不是偷懶——檔案I/O是第八天的主題,那天要從「關機就忘了」開始談,現在先不碰。
欄位裡沒有逗號、引號或換行(書名和姓名都來自固定詞庫),所以不需要跳脫。日期是上面那個yyyy-MM-dd,試算表認得。
老闆以前記帳是一張表。搬到租書上就是一列一本書,旁邊寫現在誰借走。
這樣「查書可不可借」很快。但「查會員未歸還」要整張表掃一遍,而且客人還書之後把名字擦掉,這本書上個月借給誰就沒人知道了,月底統計無從算起。
這就是第二天那個卡片盒原封不動搬進電腦,只是卡片變成了列。
所以照第二天抄下來的三個名詞開三張工作表:書籍、會員是名冊,借閱是流水帳,一次借還一列,歸還日期空白就是還在外面。另外兩張是說明和統計,等一下會用到。
這個分法和RentalDesk裡的三個List一樣,差別只在規則放哪裡。
借閱表左邊四欄手填,右邊三欄用公式把名字帶出來,店員不用記ISBN對應哪本書:
| 欄 | 內容 | 公式(第2列) |
|---|---|---|
| A | 會員編號 | 手填 |
| B | ISBN | 手填 |
| C | 借出日期 | 手填 |
| D | 歸還日期 | 手填,空白=未還 |
| E | 書名 | =IF($B2="","",IFERROR(INDEX(書籍!$B$2:$B$2000,MATCH($B2,書籍!$A$2:$A$2000,0)),"(沒建檔)")) |
| F | 姓名 | =IF($A2="","",IFERROR(INDEX(會員!$B$2:$B$2000,MATCH($A2,會員!$A$2:$A$2000,0)),"(沒建檔)")) |
| G | 狀態 | =IF($B2="","",IF($D2="","未還","已還")) |
INDEX/MATCH就是FindBook和FindMember的試算表寫法。
IFERROR接住找不到的情況。等一下會看到,會員編號和ISBN兩欄設了資料驗證,正常輸入根本走不到「(沒建檔)」那個分支——所以IFERROR守的是另一件事:匯入舊資料、或有人直接貼上一整列的時候。試算表在這件事上比昨天那支程式多一層。
書籍表再加四欄:
| 欄 | 公式(第2列) |
|---|---|
| D 未歸還筆數 | =COUNTIFS(借閱!$B$2:$B$2000,$A2,借閱!$D$2:$D$2000,"") |
| E 狀態 | =IF(D2>0,"借出中","在店") |
| F 借閱次數 | =COUNTIF(借閱!$B$2:$B$2000,$A2) |
| G 排序鍵 | =F2+ROW()/100000 |
D欄那個COUNTIFS就是RentalDesk.IsAvailable。把第一個條件換成會員編號,就是OutstandingLoansOf(memberId).Count。
同一個判斷,一邊寫成C#,一邊寫成公式。
G欄那個排序鍵和店裡沒有任何關係,它是為了排行榜才存在的。為什麼需要它,下一節會知道。
第一天那張路線圖的第一格寫著:老闆晚上數哪些書還沒回來。
Console版現在要知道這件事,得一個會員一個會員查過去。試算表的書籍表把狀態欄篩選成「借出中」,整份打烊清單一眼看完。
借閱總筆數 500
未歸還筆數 37
借出中的書(本) 37
重複借出的書(本) 0
37和第三天啟動畫面上那個「未歸還 37 筆」對得上,表示匯入沒有漏。
「未歸還筆數」和「借出中的書」都是37不是巧合:每本書同時最多一筆未歸還,資料正常時這兩個數字必然相等。等一下這件事會派上用場。
老闆看到借閱次數那一欄,順口問了一句哪幾本最常借。
LARGE取第k大、MATCH找回是哪本書,十列公式就有:
名次 ISBN 書名 借閱次數
1 9783516770131 深夜的手記 9
2 9787524816980 深夜的島 8
3 9781679983634 最後一個森林 8
4 9789955491217 轉角鋼琴師 7
5 9784071906799 深夜的河 7
6 9788215834962 地下室鋼琴師 7
7 9788754426529 看不見的花園 7
8 9782919809752 圖書館地圖 7
9 9787563547494 沉默的鋼琴師 7
10 9789046821947 地下室夏天 7
看起來很正常。但這五百筆借閱裡,借9次的只有1本,借8次的2本,借7次的有10本。
也就是說,第4名到第13名是十本並列。這張榜上的第4到第10名,和沒上榜的第9列「十二個河」、第15列「海邊的貓」、第44列「藍色約定」,借的次數一模一樣。
MATCH碰到相同的值永遠只回傳第一個找到的,所以平手的書根本排不出來。前一節那個排序鍵就是為了這個加的:借閱次數 + ROW()/100000,讓每本書的鍵都不一樣——實際上是讓列號大的贏。
於是被擠掉的三本,輸的不是借閱次數,是它們在書籍表的列號比較小。
老闆照這張榜進書,會漏掉三本一樣熱門的書。
COUNTIF算得出次數,這沒問題。問題是「前十名」在有平手的時候本來就沒有唯一答案,而試算表的LARGE需要一個不重複的鍵才排得出來——為了讓公式跑得出結果,資料裡多了一個跟店裡無關的欄位。
這種欄位之後還會再出現。
| 第三天的選單 | 試算表怎麼做 | 比較差嗎 |
|---|---|---|
| 4 查書可不可借 | 書籍表篩選ISBN,看狀態欄 |
差不多 |
| 5 搜尋書名 | 書籍表篩選「書名 包含」 |
試算表較好,還能順便看作者和借閱次數 |
| 3 查會員未歸還 | 借閱表篩選會員編號+歸還日期空白 |
差不多,多兩下滑鼠 |
| 1 借出 | 借閱表最下面新增一列 |
見下一段 |
| 2 歸還 | 篩選出那本書未還的那一列,填歸還日期 | 差不多 |
會員編號與ISBN兩欄設了資料驗證,只能選名冊裡有的值,打錯會被拒絕。這就是昨天Lend前兩道檢查的對應物:會員沒建檔、書沒建檔。
到這裡為止,試算表沒有輸。
現在做第四天那十條測試裡最重要的那一件事。
第501筆:楊芷萱借《兩個人的偵探》,借出日2026-09-05——這本書在種子資料裡是李子涵2025-12-28借走、還沒還的那一本,和第三天那個場景同一本書。
要說清楚這一列是怎麼進去的:是用程式直接寫進檔案的,不是在試算表裡一格一格打進去。 也就是說資料驗證那一關是繞過去的,這個實驗要看的是重算之後會發生什麼事,不是輸入當下會不會跳警告。
重算之後:
借閱總筆數 501
未歸還筆數 38
借出中的書(本) 37
重複借出的書(本) 1
書籍表第72列:9785567735947 兩個人的偵探 未歸還筆數 2 借出中
會員表:M00001 楊芷萱 未歸還數 3
M00009 李子涵 未歸還數 2
剛才那兩個必然相等的數字,現在差了一。同一本書在兩個人手上。
試算表做到的是事先設好的條件式格式設定:未歸還筆數大於1那一格變紅,統計表的「重複借出」從0變1。
說明表上筆者自己寫的那一行,現在應驗了:
同一本書借出中再借一次。試算表只會在「書籍」把未歸還筆數標紅,不會阻止你新增那一列。
如果店員剛好切到書籍表、剛好往下捲到第72列、剛好注意到那一格是紅的,就會發現錯了。
昨天那十條測試裡的第四條,是在按下Enter那一刻拒絕,紀錄根本不會進去。店員什麼都不用注意。
這裡要修正一個很容易講錯的理由。
不是「試算表看不到別列」。Excel和Google試算表的自訂公式驗證都可以引用其他列,=COUNTIFS($B$2:$B$2000,B2,$D$2:$D$2000,"")<=1 是合法的驗證公式,這份檔案只是沒有建這條規則。
真正的限制在別的地方:資料驗證只在有人一格一格打字的那一刻算一次。
貼上一整列、往下填滿、匯入CSV、程式寫入——都不會觸發它。而且它算完就結束了,別的列後來改成什麼樣子,它不會回頭重算。
剛才那第501筆正是這樣進去的。
要的是一個每次都重算的守門員,試算表給的是一次性的輸入檢查。再往下補巨集或Apps Script,就是把規則搬到另一個程式環境——而這個專案已經替C#那邊建好了測試和Git流程,搬過去那些保護要重新建一次。
老闆那句「用Excel不行嗎」的答案是:現在行。
只有一台電腦、兩位店員輪班,五個櫃檯動作都做得到,月底統計還多做了一件Console版沒做的事。
它會在兩種情況下不行:店裡需要一個按下去就被擋住、不靠人眼看紅格子的東西;或者第二台電腦搬進來——那時要另外回答一件事,兩位店員同時看到同一本書還在架上,然後同時新增一列,會發生什麼。共同編輯能把儲存格同步給對方,不代表「檢查可借」和「新增借閱」會合成一個不能被切開的動作。
這兩件事今天都還沒發生。
筆者選C# Console,不是因為試算表此時不夠用,是因為這家店的系統要一路走到資料庫。第一步就把規則寫進有測試護著的程式裡,之後每一次換方向都只換儲存方式,規則和測試不動。
這個理由今天還無法驗證。它要到第十七天決定換掉文字檔、第二十天建索引的時候,才知道當初是不是自作聰明。
今天的試算表和匯出指令會放在day05這個tag:
git clone --branch day05 https://github.com/EugeneSu0515/highflyer-ironman.git
老闆多買了一本《深夜的手記》,因為它借閱次數排第一,一本不夠借。
兩本書同一個ISBN。明天程式會分不清客人還的是哪一本。
這個系列是筆者正在寫的一套教學內容的濃縮版;完整版含每一次量測的原始輸出與更長的決策紀錄,三十天結束後會繼續往下走。
程式碼:https://github.com/EugeneSu0515/highflyer-ironman