diff --git "a/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.spec.ts" "b/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.spec.ts" index bb60af7..75c6fbf 100644 --- "a/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.spec.ts" +++ "b/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.spec.ts" @@ -60,8 +60,20 @@ test('測試「祇」字對應的音韻地位的各項音韻屬性', t => { test('音韻地位.調整', t => { const 地位 = 音韻地位.from描述('幫三元上'); t.is(地位.調整({ 聲: '平' }).描述, '幫三元平'); + t.is(地位.調整('平聲').描述, '幫三元平'); + t.is(地位.調整`平聲`.描述, '幫三元平'); t.throws(() => 地位.調整({ 母: '見' }), { message: /: need 呼/ }, '.調整() 會驗證新地位'); t.is(地位.調整({ 母: '見', 呼: '合' }).描述, '見合三元上'); + t.is(地位.調整`見母 合口`.描述, '見合三元上'); + t.is(地位.調整`${'見'}母 ${'合口'}`.描述, '見合三元上'); + t.is(地位.調整`仙韻 重紐A類`.描述, '幫三A仙上'); + t.is(地位.調整`仙韻 重紐${'A'}類`.描述, '幫三A仙上'); + t.throws(() => 地位.調整`壞耶`, { message: 'unrecognized expression: 壞耶' }); + t.throws(() => 地位.調整`見影母`, { message: 'unrecognized expression: 見影母' }); + t.throws(() => 地位.調整`${'見影'}母`, { message: 'unexpected 母: 見影' }); + t.throws(() => 地位.調整`見母 ${'影'}母`, { message: 'duplicated assignment of 母' }); + t.throws(() => 地位.調整`${'開合'}中立`, { message: 'unrecognized expression: 開合, 中立' }); + t.throws(() => 地位.調整`仙韻 重紐${'A類'}`, { message: 'unrecognized expression: 重紐, A類' }); t.is(地位.描述, '幫三元上', '.調整() 不修改原對象'); }); @@ -97,7 +109,7 @@ test('測試「法」字對應的音韻地位的屬於(複雜用法)及判 t.true(當前音韻地位.屬於`一四等 或 ${當前音韻地位.描述 === '幫三凡入'}`); t.true(當前音韻地位.屬於`${() => '三等'} 或 ${() => '短路〔或〕'}`); t.false(當前音韻地位.屬於`非 ${() => '三等'} 且 ${() => '短路〔且〕'}`); - t.throws(() => 當前音韻地位.屬於`${() => '三等'} 或 ${'立即求值'}`, { message: 'unreconized test condition: 立即求值' }); + t.throws(() => 當前音韻地位.屬於`${() => '三等'} 或 ${'立即求值'}`, { message: 'unrecognized test condition: 立即求值' }); t.is( 當前音韻地位.判斷( [ diff --git "a/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.ts" "b/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.ts" index 348d556..36d6383 100644 --- "a/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.ts" +++ "b/src/lib/\351\237\263\351\237\273\345\234\260\344\275\215.ts" @@ -38,6 +38,9 @@ function assert(value: unknown, error: string): asserts value { if (!value) throw new Error(error); } +// `Array.isArray`, but with more conservative type inference +const isArray: (x: unknown) => x is readonly unknown[] = Array.isArray; + type Falsy = '' | 0 | false | null | undefined; export type 規則 = [unknown, T | 規則][]; @@ -182,7 +185,7 @@ export class 音韻地位 { readonly 聲: string; /** - * 初始化音韻地位物件。(各項參數詳見 [`音韻地位`] 說明) + * 初始化音韻地位物件。(各項參數詳見 [[`音韻地位`]] 說明) * @param 母 聲母:幫, 滂, 並, 明, … * @param 呼 呼:`null`, 開, 合 * @param 等 等:一, 二, 三, 四 @@ -351,7 +354,7 @@ export class 音韻地位 { } /** - * 表達式,可用於 [[`屬於`]] 函數 + * 表達式,可用於 [[`.屬於`]] 函數 * @example * ```typescript * > 音韻地位 = Qieyun.音韻地位.from描述('幫三凡入'); @@ -394,6 +397,8 @@ export class 音韻地位 { /** * 調整該音韻地位的屬性,會驗證調整後地位的合法性,返回新的對象。 * + * 本方法可使用一般形式(`.調整({ ... })`)、字串形式(`.調整('...')`)或標籤模板語法(`` .調整`...` ``)。 + * * **注意**:原對象不會被修改。 * * @param 調整屬性 對象,其屬性可為六項基本屬性中的若干項,各屬性的值為欲修改成的值。 @@ -410,7 +415,97 @@ export class 音韻地位 { * '見合三元上' * ``` */ - 調整(調整屬性: 部分音韻屬性): 音韻地位 { + 調整(調整屬性: 部分音韻屬性): 音韻地位; + + /** + * @example + * ```typescript + * > 音韻地位 = Qieyun.音韻地位.from描述('幫三元上'); + * > 音韻地位.調整`平聲`.描述 // 標籤模板語法(表達式為字面值時推薦) + * '幫三元平' + * > 音韻地位.調整('平聲').描述 + * '幫三元平' + * > 音韻地位.調整`見母 合口`.描述 + * '見合三元上' + * ``` + */ + 調整(調整屬性: string): 音韻地位; + + /** + * @example + * ```typescript + * > 音韻地位 = Qieyun.音韻地位.from描述('幫三元上'); + * > 音韻地位.調整`${'見'}母 ${'合口'}`.描述 + * '見合三元上' + * ``` + */ + 調整(調整屬性: TemplateStringsArray, ...參數: string[]): 音韻地位; + + 調整(調整屬性: string | readonly string[] | 部分音韻屬性, ...參數: string[]): 音韻地位 { + if (typeof 調整屬性 === 'string') 調整屬性 = [調整屬性]; + + if (isArray(調整屬性)) { + const tokenGroups: string[][] = [[]]; + for (let i = 0; i < 調整屬性.length; i++) { + let fragment = 調整屬性[i]; + if (!i) { + fragment = fragment.trimStart(); + } + if (i === 調整屬性.length - 1) { + fragment = fragment.trimEnd(); + } + + const tokens = fragment.split(/\s+/); + for (let j = 0; j < tokens.length; j++) { + if (tokens[j]) { + tokenGroups[tokenGroups.length - 1].push(tokens[j]); + } + if (j < tokens.length - 1) { + tokenGroups.push([]); + } + } + if (i < 參數.length) { + tokenGroups[tokenGroups.length - 1].push(參數[i]); + } + } + + const 音韻屬性: 部分音韻屬性 = {}; + const tryAssign = (屬性: T, 值: 音韻地位[T]) => { + assert(!(屬性 in 音韻屬性), `duplicated assignment of ${屬性}`); + 音韻屬性[屬性] = 值; + }; + + for (let tokens of tokenGroups) { + assert(tokens.length, 'empty expression'); + let original: string | undefined; + if (tokens.length === 1) { + switch (tokens[0]) { + case '開合中立': + tryAssign('呼', null); + continue; + case '不分重紐': + tryAssign('重紐', null); + continue; + } + original = tokens[0]; + tokens = [...tokens[0]]; + } + let 屬性 = tokens[tokens.length - 1]; + const 值 = tokens[tokens.length - 2]; + assert( + 屬性 === '類' ? tokens.slice(0, -2).join('') === '重紐' : tokens.length === 2 && ['母', '等', '韻', '聲', '口'].includes(屬性), + `unrecognized expression: ${original ?? tokens.join(', ')}` + ); + if (屬性 === '口') 屬性 = '呼'; + if (屬性 === '類') 屬性 = '重紐'; + const check = 檢查[屬性 as keyof 部分音韻屬性]; + assert(check.includes(值), `unexpected ${屬性}: ${值}`); + tryAssign(屬性 as keyof 部分音韻屬性, 值); + } + + 調整屬性 = 音韻屬性; + } + const { 母 = this.母, 呼 = this.呼, 等 = this.等, 重紐 = this.重紐, 韻 = this.韻, 聲 = this.聲 } = 調整屬性; return new 音韻地位(母, 呼, 等, 重紐, 韻, 聲); } @@ -449,17 +544,15 @@ export class 音韻地位 { * * NOT 運算子:`非`, `not`, `~`, `!` * * 括號:`(……)`, `(……)` * - * 各表達式及運算子之間以空格隔開。 - * - * AND 運算子可省略。 + * 各表達式及運算子之間必須以空格隔開。 * - * 如 `(端精組 且 入聲) 或 (以母 且 四等 且 去聲)` 與 `端精組 入聲 或 以母 四等 去聲` 同義。 + * AND 運算子可省略,如 `(端精組 且 入聲) 或 (以母 且 四等 且 去聲)` 與 `端精組 入聲 或 以母 四等 去聲` 同義。 * @returns 若描述音韻地位的字串符合該音韻地位,回傳 `true`;否則回傳 `false`。 * @throws 若表達式為空、不合語法、或限定條件不合法,則拋出異常。 * @example * ```typescript * > 音韻地位 = Qieyun.音韻地位.from描述('幫三凡入'); - * > 音韻地位.屬於`章母`; // 標籤模板語法(表達式為字面值時推荐) + * > 音韻地位.屬於`章母`; // 標籤模板語法(表達式為字面值時推薦) * false * > 音韻地位.屬於('章母'); // 一般形式 * false @@ -476,8 +569,8 @@ export class 音韻地位 { * * 嵌入的參數可以是: * - * * 函數:會被執行;若其傳回值為字串,會遞迴套用至 [[`音韻地位.屬於`]] 函數,否則會檢測其真值 - * * 字串:會遞迴套用至 [[`音韻地位.屬於`]] 函數 + * * 函數:會被執行;若其傳回值為字串,會遞迴套用至 [[`.屬於`]] 函數,否則會檢測其真值 + * * 字串:會遞迴套用至 [[`.屬於`]] 函數 * * 其他:會檢測其真值 * * **注意**: @@ -496,7 +589,7 @@ export class 音韻地位 { * true * ``` */ - 屬於(表達式: readonly string[], ...參數: unknown[]): boolean; + 屬於(表達式: TemplateStringsArray, ...參數: unknown[]): boolean; 屬於(表達式: string | readonly string[], ...參數: unknown[]): boolean { if (typeof 表達式 === 'string') 表達式 = [表達式]; @@ -526,7 +619,7 @@ export class 音韻地位 { assert(!invalid, invalid + match[2] + '不存在'); return values.includes(this[match[2] as keyof typeof 檢查]); } - throw new Error(`unreconized test condition: ${token}`); + throw new Error(`unrecognized test condition: ${token}`); }; // 詞法分析,同時給普通運算元求值(惟函數型運算元留待後面惰性求值) @@ -684,8 +777,8 @@ export class 音韻地位 { * * 判斷式可以是: * - * *   函數:會被執行;若其傳回值為非空字串,會套用至 [[`音韻地位.屬於`]] 函數,若為布林值則直接決定是否跳過本規則,否則規則永遠不會被跳過 - * * 非空字串:描述音韻地位的表達式,會套用至 [[`音韻地位.屬於`]] 函數 + * *   函數:會被執行;若其傳回值為非空字串,會套用至 [[`.屬於`]] 函數,若為布林值則直接決定是否跳過本規則,否則規則永遠不會被跳過 + * * 非空字串:描述音韻地位的表達式,會套用至 [[`.屬於`]] 函數 * *  布林值:直接決定是否跳過本規則 * *   其他:規則永遠不會被跳過(可用作指定後備結果) *