[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fdont-split-names":3},{"id":4,"title":5,"body":6,"category":567,"date":568,"description":569,"draft":570,"extension":571,"meta":572,"navigation":145,"path":573,"readingTime":142,"seo":574,"stem":575,"tags":576,"__hash__":580},"blog\u002Fblog\u002Fdont-split-names.md","我寫了姓名拆解，兩個月後把它從註冊流程刪掉",{"type":7,"value":8,"toc":560},"minimark",[9,26,29,33,36,52,55,310,313,316,319,407,410,414,417,420,498,507,510,513,516,530,533,536,539,556],[10,11,12,13,17,18,21,22,25],"p",{},"這次的需求是：註冊時的姓名要能支援原住民的名字。原住民的名字不一定能分成一個字的姓和後面的名，有些還會用間隔號 ",[14,15,16],"code",{},"·"," 連接，所以表單改成一個「姓名」欄位，但後端的資料還是要 ",[14,19,20],{},"first_name"," 和 ",[14,23,24],{},"last_name"," 兩個值。",[10,27,28],{},"我的第一個想法很直覺：寫一個函式，把使用者輸入的姓名拆開。兩個月後，我把註冊流程裡的這段邏輯刪了。",[30,31,32],"h2",{"id":32},"當時的拆解規則",[10,34,35],{},"我依照常見的姓名格式寫了幾條規則：",[37,38,39,46,49],"ol",{},[40,41,42,43,45],"li",{},"有間隔號 ",[14,44,16],{}," 或空白，就從第一個符號切開。",[40,47,48],{},"開頭是中文，第一個字當姓，其他當名。",[40,50,51],{},"其他情況，整串放進同一個欄位。",[10,53,54],{},"寫成程式大概是這樣：",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-js shiki shiki-themes github-dark","function splitName(name) {\n  const trimmed = name.trim()\n  const index = trimmed.search(\u002F[·\\s]\u002F)\n\n  if (index !== -1) {\n    return {\n      surname: trimmed.slice(0, index),\n      givenName: trimmed.slice(index + 1),\n    }\n  }\n\n  if (\u002F^[一-龥]\u002F.test(trimmed)) {\n    return { surname: trimmed[0], givenName: trimmed.slice(1) }\n  }\n\n  return { surname: trimmed, givenName: '' }\n}\n","js","",[14,63,64,88,110,140,147,167,176,193,213,219,225,230,257,279,284,289,304],{"__ignoreMap":61},[65,66,69,73,77,81,85],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"snl16","function",[65,74,76],{"class":75},"svObZ"," splitName",[65,78,80],{"class":79},"s95oV","(",[65,82,84],{"class":83},"s9osk","name",[65,86,87],{"class":79},") {\n",[65,89,91,94,98,101,104,107],{"class":67,"line":90},2,[65,92,93],{"class":71},"  const",[65,95,97],{"class":96},"sDLfK"," trimmed",[65,99,100],{"class":71}," =",[65,102,103],{"class":79}," name.",[65,105,106],{"class":75},"trim",[65,108,109],{"class":79},"()\n",[65,111,113,115,118,120,123,126,128,132,135,137],{"class":67,"line":112},3,[65,114,93],{"class":71},[65,116,117],{"class":96}," index",[65,119,100],{"class":71},[65,121,122],{"class":79}," trimmed.",[65,124,125],{"class":75},"search",[65,127,80],{"class":79},[65,129,131],{"class":130},"sU2Wk","\u002F",[65,133,134],{"class":96},"[·\\s]",[65,136,131],{"class":130},[65,138,139],{"class":79},")\n",[65,141,143],{"class":67,"line":142},4,[65,144,146],{"emptyLinePlaceholder":145},true,"\n",[65,148,150,153,156,159,162,165],{"class":67,"line":149},5,[65,151,152],{"class":71},"  if",[65,154,155],{"class":79}," (index ",[65,157,158],{"class":71},"!==",[65,160,161],{"class":71}," -",[65,163,164],{"class":96},"1",[65,166,87],{"class":79},[65,168,170,173],{"class":67,"line":169},6,[65,171,172],{"class":71},"    return",[65,174,175],{"class":79}," {\n",[65,177,179,182,185,187,190],{"class":67,"line":178},7,[65,180,181],{"class":79},"      surname: trimmed.",[65,183,184],{"class":75},"slice",[65,186,80],{"class":79},[65,188,189],{"class":96},"0",[65,191,192],{"class":79},", index),\n",[65,194,196,199,201,204,207,210],{"class":67,"line":195},8,[65,197,198],{"class":79},"      givenName: trimmed.",[65,200,184],{"class":75},[65,202,203],{"class":79},"(index ",[65,205,206],{"class":71},"+",[65,208,209],{"class":96}," 1",[65,211,212],{"class":79},"),\n",[65,214,216],{"class":67,"line":215},9,[65,217,218],{"class":79},"    }\n",[65,220,222],{"class":67,"line":221},10,[65,223,224],{"class":79},"  }\n",[65,226,228],{"class":67,"line":227},11,[65,229,146],{"emptyLinePlaceholder":145},[65,231,233,235,238,240,243,246,248,251,254],{"class":67,"line":232},12,[65,234,152],{"class":71},[65,236,237],{"class":79}," (",[65,239,131],{"class":130},[65,241,242],{"class":71},"^",[65,244,245],{"class":96},"[一-龥]",[65,247,131],{"class":130},[65,249,250],{"class":79},".",[65,252,253],{"class":75},"test",[65,255,256],{"class":79},"(trimmed)) {\n",[65,258,260,262,265,267,270,272,274,276],{"class":67,"line":259},13,[65,261,172],{"class":71},[65,263,264],{"class":79}," { surname: trimmed[",[65,266,189],{"class":96},[65,268,269],{"class":79},"], givenName: trimmed.",[65,271,184],{"class":75},[65,273,80],{"class":79},[65,275,164],{"class":96},[65,277,278],{"class":79},") }\n",[65,280,282],{"class":67,"line":281},14,[65,283,224],{"class":79},[65,285,287],{"class":67,"line":286},15,[65,288,146],{"emptyLinePlaceholder":145},[65,290,292,295,298,301],{"class":67,"line":291},16,[65,293,294],{"class":71},"  return",[65,296,297],{"class":79}," { surname: trimmed, givenName: ",[65,299,300],{"class":130},"''",[65,302,303],{"class":79}," }\n",[65,305,307],{"class":67,"line":306},17,[65,308,309],{"class":79},"}\n",[10,311,312],{},"測試時輸入「王小明」，結果完全正確，看起來沒有問題。",[30,314,315],{"id":315},"問題在於規則永遠寫不完",[10,317,318],{},"姓名不是一種能用幾條規則描述的格式：",[320,321,322,338],"table",{},[323,324,325],"thead",{},[326,327,328,332,335],"tr",{},[329,330,331],"th",{},"輸入",[329,333,334],{},"拆解結果",[329,336,337],{},"問題",[339,340,341,353,363,374,385,396],"tbody",{},[326,342,343,347,350],{},[344,345,346],"td",{},"歐陽小明",[344,348,349],{},"歐 \u002F 陽小明",[344,351,352],{},"複姓被切開",[326,354,355,358,361],{},[344,356,357],{},"范姜小明",[344,359,360],{},"范 \u002F 姜小明",[344,362,352],{},[326,364,365,368,371],{},[344,366,367],{},"張李小明",[344,369,370],{},"張 \u002F 李小明",[344,372,373],{},"無法判斷是否為雙姓",[326,375,376,379,382],{},[344,377,378],{},"瓦旦·達路",[344,380,381],{},"瓦旦 \u002F 達路",[344,383,384],{},"順序不一定是「姓在前」",[326,386,387,390,393],{},[344,388,389],{},"John Smith",[344,391,392],{},"John \u002F Smith",[344,394,395],{},"英文是名在前",[326,397,398,401,404],{},[344,399,400],{},"Mary Ann Smith",[344,402,403],{},"Mary \u002F Ann Smith",[344,405,406],{},"中間名怎麼算",[10,408,409],{},"當然可以繼續加規則，例如放一份複姓清單，但「張李」這種情況，光看字串根本無法判斷。程式只是在猜，而猜錯的是使用者的名字。",[30,411,413],{"id":412},"最後的修法團隊討論後決定不拆","最後的修法：團隊討論後決定不拆",[10,415,416],{},"原本的方向就是把姓名拆開存。後來團隊內部討論，對照資料庫的欄位後發現其實沒有必要拆分：後端收到姓和名之後，會直接接成一個完整姓名儲存。前端辛苦拆開，後端又接回去，拆分根本沒有帶來價值。",[10,418,419],{},"所以註冊時改成使用者打什麼，就存什麼：",[56,421,423],{"className":58,"code":422,"language":60,"meta":61,"style":61},"\u002F\u002F 之前\nconst { surname, givenName } = splitName(form.name)\n\n\u002F\u002F 之後\nconst payload = {\n  last_name: form.name,\n  first_name: '',\n}\n",[14,424,425,431,459,463,468,479,484,494],{"__ignoreMap":61},[65,426,427],{"class":67,"line":68},[65,428,430],{"class":429},"sAwPA","\u002F\u002F 之前\n",[65,432,433,436,439,442,445,448,451,454,456],{"class":67,"line":90},[65,434,435],{"class":71},"const",[65,437,438],{"class":79}," { ",[65,440,441],{"class":96},"surname",[65,443,444],{"class":79},", ",[65,446,447],{"class":96},"givenName",[65,449,450],{"class":79}," } ",[65,452,453],{"class":71},"=",[65,455,76],{"class":75},[65,457,458],{"class":79},"(form.name)\n",[65,460,461],{"class":67,"line":112},[65,462,146],{"emptyLinePlaceholder":145},[65,464,465],{"class":67,"line":142},[65,466,467],{"class":429},"\u002F\u002F 之後\n",[65,469,470,472,475,477],{"class":67,"line":149},[65,471,435],{"class":71},[65,473,474],{"class":96}," payload",[65,476,100],{"class":71},[65,478,175],{"class":79},[65,480,481],{"class":67,"line":169},[65,482,483],{"class":79},"  last_name: form.name,\n",[65,485,486,489,491],{"class":67,"line":178},[65,487,488],{"class":79},"  first_name: ",[65,490,300],{"class":130},[65,492,493],{"class":79},",\n",[65,495,496],{"class":67,"line":195},[65,497,309],{"class":79},[10,499,500,501,503,504,506],{},"這次改動刪掉了 40 多行，只留下兩行。資料庫的欄位沒有調整，所以完整姓名放在 ",[14,502,24],{},"，",[14,505,20],{}," 留空。",[10,508,509],{},"這不是最漂亮的資料結構，但系統不用再猜哪個部分是姓，原住民的名字也能完整保存。",[30,511,512],{"id":512},"什麼時候才需要拆",[10,514,515],{},"資料還是存完整姓名，真的需要分開時才拆。例如要顯示「王先生您好」，就在使用的當下，依照調整後的規則自動拆分：",[517,518,519,524,527],"ul",{},[40,520,42,521,523],{},[14,522,16],{}," 或空白時，從第一個符號切開。",[40,525,526],{},"沒有符號的中文姓名，以第一個字當姓。",[40,528,529],{},"判斷不了的情況，就顯示完整姓名。",[10,531,532],{},"和原本最大的差別是，拆分結果只用來顯示，不寫回資料庫。規則就算猜錯，完整姓名也還在，之後調整規則就能修正，不會留下拆錯的資料。",[10,534,535],{},"如果姓和名會影響重要用途，例如要和證件資料比對，就不適合靠規則判斷，應該讓使用者自己分開填寫。",[30,537,538],{"id":538},"這次學到的事",[517,540,541,544,547,550,553],{},[40,542,543],{},"測試資料全部通過，不代表規則是對的。我只想到了「王小明」。",[40,545,546],{},"碰到人名、地址這類真實世界的資料，要先懷疑自己的假設。",[40,548,549],{},"當規則需要一直補例外時，先問這段程式是不是根本不該存在。",[40,551,552],{},"動手前先和團隊確認資料實際怎麼使用，討論之後常常會發現不用做那麼多。",[40,554,555],{},"刪程式碼也是一種修法，而且常常是最好的那種。",[557,558,559],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":61,"searchDepth":90,"depth":90,"links":561},[562,563,564,565,566],{"id":32,"depth":90,"text":32},{"id":315,"depth":90,"text":315},{"id":412,"depth":90,"text":413},{"id":512,"depth":90,"text":512},{"id":538,"depth":90,"text":538},"case","2026-02-25","為了讓註冊姓名支援原住民的名字，我寫了一段拆解規則。記錄這些規則的問題，以及團隊討論後決定不拆、改存單一欄位的過程。",false,"md",{},"\u002Fblog\u002Fdont-split-names",{"title":5,"description":569},"blog\u002Fdont-split-names",[577,578,579],"Forms","Refactoring","Edge Cases","Lxt087WSWxMgDtfpoMaNGFDxWzOqKL58ylNWh4oQs-Y"]