🌐用 JavaScript 實作多欄位排序:先依 SEQ_NO,再依 APS_PLAN_NO,最後依 TFT4(空值排最後)

摘要

教你用 JavaScript 為資料列實作「先依 SEQ_NO、再依 APS_PLAN_NO、最後依 TFT4(空值排最後)」的多欄位排序。包含完整程式碼、逐行說明、常見陷阱與可重用的排序工具。


為什麼需要多欄位排序?

實務上,我們常需要 先用主要欄位 排序(例如工單次序 SEQ_NO),若相同再用 次要欄位APS_PLAN_NO),最後再用 第三順位欄位TFT4)。更麻煩的是,TFT4 可能為 null、空字串或只有空白——我們希望這些 空值排在最後,避免干擾有效值的排序。

本篇提供一段 可直接套用 的 JavaScript 排序程式,並解釋每個細節與容易踩到的坑。


最終目標的排序規則

  1. 第一順位SEQ_NO

  2. 第二順位APS_PLAN_NO

  3. 第三順位TFT4空值排最後,其餘照字面自然排序)

「空值」包含 nullundefined、空字串 ''、或只有空白字元的字串。



直接可用的排序程式碼(新版)

// 需求:先依 SEQ_NO,再依 APS_PLAN_NO,最後依 TFT4(空值排最後) // 建立單一比較子:一般字串/數字混合 → 自然排序 const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) const norm = (v) => (v ?? '').toString().trim() // 建立「空值排最後」的欄位比較子(專供 TFT4 用) const cmpEmptyLast = (x, y) => { const ax = norm(x) const by = norm(y) const axBlank = ax === '' const byBlank = by === '' if (axBlank && byBlank) return 0 if (axBlank) return 1 // x 空 → 往後排 if (byBlank) return -1 // y 空 → 往後排 return collator.compare(ax, by) } // 以「比較子合成」的方式組裝多欄位排序規則 const compareBy = (...fns) => (a, b) => { for (const fn of fns) { const r = fn(a, b) if (r !== 0) return r } return 0 } // 三段式規則:SEQ_NO → APS_PLAN_NO → TFT4(空值最後) rows.sort( compareBy( (a, b) => collator.compare(norm(a.SEQ_NO), norm(b.SEQ_NO)), (a, b) => collator.compare(norm(a.APS_PLAN_NO),norm(b.APS_PLAN_NO)), (a, b) => cmpEmptyLast(a.TFT4, b.TFT4) ) )

為什麼這樣寫?

  • 使用 比較子合成(compareBy) 讓規則一目了然、易擴充。

  • Intl.Collator自然排序 處理「字串中的數字」(例如 "2" < "10")。

  • cmpEmptyLast 先判斷是否為空,再進行文字比較,可讀性高且容易單測。


範例資料與排序前後對照

const rows = [ { SEQ_NO: '2', APS_PLAN_NO: 'A100', TFT4: 'T2' }, { SEQ_NO: '2', APS_PLAN_NO: 'A100', TFT4: ' ' }, // 空白視為空值 { SEQ_NO: '1', APS_PLAN_NO: 'A200', TFT4: 'T1' }, { SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: 'T3' }, { SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: null }, // null 視為空值 ] // 套用上方的 sort 比較器後: // 1) 先比 SEQ_NO('1' 群組在 '2' 前) // 2) 在 SEQ_NO 相同時比 APS_PLAN_NO(A100 在 A200 前) // 3) 在同群組內比 TFT4,空值最後 rows.sort( compareBy( (a, b) => collator.compare(norm(a.SEQ_NO), norm(b.SEQ_NO)), (a, b) => collator.compare(norm(a.APS_PLAN_NO),norm(b.APS_PLAN_NO)), (a, b) => cmpEmptyLast(a.TFT4, b.TFT4) ) )

可重用的「多欄位排序工具」(DSL 風格)

// 以鏈式 DSL 建立多欄位比較器 function order() { const coll = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) const steps = [] const toS = (v) => (v ?? '').toString().trim() return { by(key) { steps.push((a, b) => coll.compare(toS(a[key]), toS(b[key]))) return this }, byEmptyLast(key) { steps.push((a, b) => { const ax = toS(a[key]); const bx = toS(b[key]) const ae = ax === ''; const be = bx === '' if (ae || be) return ae === be ? 0 : (ae ? 1 : -1) return coll.compare(ax, bx) }) return this }, build() { return (a, b) => { for (const s of steps) { const r = s(a, b) if (r) return r } return 0 } } } } // 使用方式 const cmp = order() .by('SEQ_NO') .by('APS_PLAN_NO') .byEmptyLast('TFT4') .build() rows.sort(cmp)

好處:

  • 規則宣告式、可讀性高。

  • 之後想改成 byDesc、或加入更多欄位,不必重寫整個比較器。


進階:使用 Intl.Collator 與「空值權重」優化

const coll = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) const T = (v) => (v ?? '').toString().trim() rows.sort((a, b) => { // 1) SEQ_NO let r = coll.compare(T(a.SEQ_NO), T(b.SEQ_NO)) if (r) return r // 2) APS_PLAN_NO r = coll.compare(T(a.APS_PLAN_NO), T(b.APS_PLAN_NO)) if (r) return r // 3) TFT4(先比空值權重,再比字面) const aw = T(a.TFT4) === '' ? 1 : 0 const bw = T(b.TFT4) === '' ? 1 : 0 if (aw !== bw) return aw - bw // 非空(0) 先於 空(1) return coll.compare(T(a.TFT4), T(b.TFT4)) })

此寫法把「空值最後」拆成 權重比較(數字)+ 字面比較(文字),在大資料量下仍具備良好可讀性與效能。


常見陷阱與最佳實務

  1. 字串 vs. 數字
    後端若傳回混合型態(有時數字、有時字串),比較前先 String() 正規化,並用 Intl.Collatornumeric: true 避免 "10" < "2" 的字典序陷阱。

  2. 空白字元
    trim() 可避免 " " 被誤判為有效值。

  3. 穩定排序(Stable Sort)
    現代主流 JS 引擎的 Array.prototype.sort 已採用穩定排序,但若需 極舊環境 仍應驗證或引入 polyfill。

  4. 僅對需要的欄位做空值處理
    本例僅對 TFT4 做「空值最後」,其它欄位維持一般排序,以符合業務邏輯。

  5. 效能

    • 重複排序時,重用同一個 Intl.Collator

    • 資料量極大(>10 萬)時,可考慮先分群再排序,降低比較次數。


單元測試建議(以 Node.js assert 範例)

import assert from 'node:assert/strict' const coll = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) const toS = (v) => (v ?? '').toString().trim() const cmpEmptyLast = (x, y) => { const ax = toS(x), by = toS(y) const ae = ax === '', be = by === '' if (ae || be) return ae === be ? 0 : (ae ? 1 : -1) return coll.compare(ax, by) } const compareBy = (...fns) => (a, b) => { for (const fn of fns) { const r = fn(a, b) if (r) return r } return 0 } const data = [ { SEQ_NO: '2', APS_PLAN_NO: 'A100', TFT4: '' }, { SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: 'T3' }, { SEQ_NO: '1', APS_PLAN_NO: 'A100', TFT4: null }, { SEQ_NO: '1', APS_PLAN_NO: 'A200', TFT4: 'T1' }, ] const sorted = [...data].sort( compareBy( (a, b) => coll.compare(toS(a.SEQ_NO), toS(b.SEQ_NO)), (a, b) => coll.compare(toS(a.APS_PLAN_NO), toS(b.APS_PLAN_NO)), (a, b) => cmpEmptyLast(a.TFT4, b.TFT4) ) ) // 驗證順序(以索引或欄位檢查) assert.equal(sorted[0].SEQ_NO, '1') assert.equal(sorted[1].SEQ_NO, '1') assert.equal(sorted[2].SEQ_NO, '1') assert.equal(sorted[3].SEQ_NO, '2') assert.equal(sorted[0].APS_PLAN_NO, 'A100') assert.equal(sorted[1].APS_PLAN_NO, 'A100') assert.equal(sorted[2].APS_PLAN_NO, 'A200') assert.equal(sorted[0].TFT4, 'T3') // 非空先於空 assert.equal(sorted[1].TFT4, null)

提示:也可用 Vitest/Jest 撰寫更語意化的斷言(例如 expect(sorted).toEqual([...]))。



結語

本文示範了如何在前端以 清楚、可維護、可測試 的方式,完成「先 SEQ_NO → 再 APS_PLAN_NO → 最後 TFT4(空值排最後)」的排序需求。你可以直接複製貼上使用,或採用通用比較器在專案中重複利用。

需要把它整合進 Vue 的表格或匯出 Excel 的流程嗎?把你的欄位定義與資料量告訴我,我可以幫你做最佳化與單元測試範本。

留言

這個網誌中的熱門文章

🔍Vue.js 專案錯誤排查:解決 numericFields is not defined 與合併儲存格邏輯最佳化

🔎EF Core 連 Oracle 出現 ORA-00600 [kpp_concatq:2] 的完整排錯指南(含 EF Core ToString/CultureInfo 錯誤)

🛠【ASP.NET Core + Oracle】解決 ORA-00904 "FALSE": 無效的 ID 錯誤與資料欄位動態插入顯示問題