來源格式與預期記錄
這是原創假設轉換例子。輸入限下列欄位的普通、無循環 JSON 形記錄,統一記錄格式及拒絕代碼由本例訂明。若換來源,先修改格式約定。
來源事件有 open 加非空標籤、text 加文字,以及 close 關最近開啟的段落。根層也可有文字。按原次序組成 section、text 物件;沒有已開段落卻 close,或結尾仍未關齊便拒絕。已關閉的空段落照留。
| 個案 | 輸入記錄(JSON) | 預期輸出(JSON) |
|---|---|---|
| 建立巢狀段落 | {"events":[{"kind":"open","label":"Outer"},{"kind":"open","label":"Inner"},{"kind":"text","value":"Note"},{"kind":"close"},{"kind":"close"}]} | {"ok":true,"value":{"parts":[{"kind":"section","label":"Outer","parts":[{"kind":"section","label":"Inner","parts":[{"kind":"text","text":"Note"}]}]}]}} |
| 拒絕無對應結束 | {"events":[{"kind":"close"}]} | {"ok":false,"error":"SECTION_CLOSE"} |
| 保留空段落 | {"events":[{"kind":"open","label":"Empty"},{"kind":"close"}]} | {"ok":true,"value":{"parts":[{"kind":"section","label":"Empty","parts":[]}]}} |
在本機執行轉換
把完整例子以 JavaScript 模組在 Node.js 執行,內含純轉換函數、三個輸入及寫明的預期輸出,對不上便由斷言停止。拒絕個案回傳 ok=false 及代碼,不回傳半份統一記錄。
import assert from 'node:assert/strict';
function nestedSections(i){
const fail=error=>({ok:false,error});if(!i||!Array.isArray(i.events))return fail('SECTION_EVENTS');
const parts=[],stack=[];
for(const e of i.events){
if(!e)return fail('SECTION_KIND');const destination=stack.length?stack.at(-1).parts:parts;
if(e.kind==='open'){
if(typeof e.label!=='string'||!e.label)return fail('SECTION_LABEL');
const section={kind:'section',label:e.label,parts:[]};destination.push(section);stack.push(section);
}else if(e.kind==='text'){
if(typeof e.value!=='string')return fail('SECTION_TEXT');destination.push({kind:'text',text:e.value});
}else if(e.kind==='close'){
if(!stack.length)return fail('SECTION_CLOSE');stack.pop();
}else return fail('SECTION_KIND');
}
if(stack.length)return fail('SECTION_UNCLOSED');return {ok:true,value:{parts}};
}
// Original synthetic cases; expected outputs are literal records.
const cases = [
{
"label": "Build nested sections",
"input": {
"events": [
{
"kind": "open",
"label": "Outer"
},
{
"kind": "open",
"label": "Inner"
},
{
"kind": "text",
"value": "Note"
},
{
"kind": "close"
},
{
"kind": "close"
}
]
},
"expected": {
"ok": true,
"value": {
"parts": [
{
"kind": "section",
"label": "Outer",
"parts": [
{
"kind": "section",
"label": "Inner",
"parts": [
{
"kind": "text",
"text": "Note"
}
]
}
]
}
]
}
}
},
{
"label": "Reject unmatched close",
"input": {
"events": [
{
"kind": "close"
}
]
},
"expected": {
"ok": false,
"error": "SECTION_CLOSE"
}
},
{
"label": "Preserve empty section",
"input": {
"events": [
{
"kind": "open",
"label": "Empty"
},
{
"kind": "close"
}
]
},
"expected": {
"ok": true,
"value": {
"parts": [
{
"kind": "section",
"label": "Empty",
"parts": []
}
]
}
}
}
];
for (const sample of cases) {
const actual = nestedSections(sample.input);
assert.deepEqual(actual, sample.expected);
console.log(sample.label, JSON.stringify(actual));
}
判清邊界個案
先開 Outer,再開 Inner,Note 屬哪一層?
查看參考答案
Note 放在 Inner,Inner 再放在 Outer。每次 close 只退出一層。空段個案仍保留 Empty,其 parts=[],不刪除。