iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
Software Development

AI寫得出程式,但它不知道你的店:一家租書店的30天技術選型實錄系列 第 5

Day 5:老闆問「用Excel不行嗎」,做完之後發現他是對的

  • 分享至 

  • xImage
  •  

電腦搬進店裡那天,老闆其實先問過一句:

用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="","未還","已還"))

INDEXMATCH就是FindBookFindMember的試算表寫法。

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流程,搬過去那些保護要重新建一次。

那為什麼還是選了Console

老闆那句「用Excel不行嗎」的答案是:現在行。

只有一台電腦、兩位店員輪班,五個櫃檯動作都做得到,月底統計還多做了一件Console版沒做的事。

它會在兩種情況下不行:店裡需要一個按下去就被擋住、不靠人眼看紅格子的東西;或者第二台電腦搬進來——那時要另外回答一件事,兩位店員同時看到同一本書還在架上,然後同時新增一列,會發生什麼。共同編輯能把儲存格同步給對方,不代表「檢查可借」和「新增借閱」會合成一個不能被切開的動作。

這兩件事今天都還沒發生。

筆者選C# Console,不是因為試算表此時不夠用,是因為這家店的系統要一路走到資料庫。第一步就把規則寫進有測試護著的程式裡,之後每一次換方向都只換儲存方式,規則和測試不動。

這個理由今天還無法驗證。它要到第十七天決定換掉文字檔、第二十天建索引的時候,才知道當初是不是自作聰明。

今天的試算表和匯出指令會放在day05這個tag:

git clone --branch day05 https://github.com/EugeneSu0515/highflyer-ironman.git

明天

老闆多買了一本《深夜的手記》,因為它借閱次數排第一,一本不夠借。

兩本書同一個ISBN。明天程式會分不清客人還的是哪一本。


這個系列是筆者正在寫的一套教學內容的濃縮版;完整版含每一次量測的原始輸出與更長的決策紀錄,三十天結束後會繼續往下走。

程式碼:https://github.com/EugeneSu0515/highflyer-ironman


上一篇
Day 4:十條測試,實作一行都沒改
下一篇
Day 6:老闆多買了一本,電腦說那本書借出中
系列文
AI寫得出程式,但它不知道你的店:一家租書店的30天技術選型實錄6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言