diff --git a/.gitignore b/.gitignore index ce80f0c..0a4df01 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ yarn.lock /prepare/data.csv /prepare/v2.csv -/src/data/資料.ts +/src/data/廣韻.ts diff --git a/prepare/main.py b/prepare/main.py index 593c7af..abe4481 100644 --- a/prepare/main.py +++ b/prepare/main.py @@ -90,7 +90,7 @@ def main(): d.setdefault(編碼 + 反切 + 韻目原貌, []).append((字頭, 字頭又作, 釋義)) os.makedirs('src/data', exist_ok=True) - with open('src/data/資料.ts', 'w', newline='') as fout: + with open('src/data/廣韻.ts', 'w', newline='') as fout: print('export default `\\', file=fout) for key, 各條目 in d.items(): print( diff --git a/src/Qieyun.ts b/src/Qieyun.ts index d3cf69a..30d7fa6 100644 --- a/src/Qieyun.ts +++ b/src/Qieyun.ts @@ -1,7 +1,7 @@ export { 音韻地位 } from './lib/音韻地位'; export type { 部分音韻屬性, 判斷規則列表, 邊緣地位種類指定 } from './lib/音韻地位'; -export * as 資料 from './lib/解析資料'; +export * as 資料 from './lib/資料'; export * as 表達式 from './lib/常用表達式'; diff --git "a/src/lib/\345\243\223\347\270\256\350\241\250\347\244\272.spec.ts" "b/src/lib/\345\243\223\347\270\256\350\241\250\347\244\272.spec.ts" index da4fdc2..f0f6506 100644 --- "a/src/lib/\345\243\223\347\270\256\350\241\250\347\244\272.spec.ts" +++ "b/src/lib/\345\243\223\347\270\256\350\241\250\347\244\272.spec.ts" @@ -1,7 +1,7 @@ import test from 'ava'; import { decode音韻編碼, encode音韻編碼 } from './壓縮表示'; -import { iter音韻地位 } from './解析資料'; +import { iter音韻地位 } from './資料'; import { 音韻地位 } from './音韻地位'; test('測試音韻編碼', t => { diff --git "a/src/lib/\350\247\243\346\236\220\350\263\207\346\226\231.ts" "b/src/lib/\350\247\243\346\236\220\350\263\207\346\226\231.ts" deleted file mode 100644 index 6cfdb54..0000000 --- "a/src/lib/\350\247\243\346\236\220\350\263\207\346\226\231.ts" +++ /dev/null @@ -1,100 +0,0 @@ -import 資料 from '../data/資料'; - -import { decode音韻編碼, encode音韻編碼 } from './壓縮表示'; -import { 音韻地位 } from './音韻地位'; - -type 內部檢索結果 = Readonly<{ 字頭: string; 編碼: string; 反切: string | null; 釋義: string; 韻目原貌: string }>; -export interface 檢索結果 { - 字頭: string; - 音韻地位: 音韻地位; - 反切: string | null; - 釋義: string; - 韻目原貌: string; -} - -const m字頭檢索 = new Map(); -const m音韻編碼檢索 = new Map(); - -function insertInto(map: Map, key: K, value: V) { - if (!map.has(key)) { - map.set(key, [value]); - } else { - map.get(key)!.push(value); - } -} - -(function 解析資料() { - const patternOuter = /([\w$]{3})(..)(.)(.*?\n)/gu; - for (const [, 編碼, maybe反切, 韻目原貌, 各條目] of 資料.matchAll(patternOuter)) { - // '@@' is a placeholder in the original data to indicate that there is no 反切 - const 反切 = maybe反切 === '@@' ? null : maybe反切; - - const patternInner = /(.)((?:\+.)*)(.*?)[|\n]/gu; - for (const [, 字頭, 字頭又作, 釋義] of 各條目.matchAll(patternInner)) { - const record = { 字頭, 編碼, 韻目原貌, 反切, 釋義 }; - - insertInto(m字頭檢索, 字頭, record); - for (const [, 別體] of 字頭又作.matchAll(/\+(.)/g)) { - insertInto(m字頭檢索, 別體, record); - } - - insertInto(m音韻編碼檢索, 編碼, record); - } - } -})(); - -function 結果from內部結果(內部結果: 內部檢索結果): 檢索結果 { - const { 字頭, 編碼, ...rest } = 內部結果; - return { 字頭, 音韻地位: decode音韻編碼(編碼), ...rest }; -} - -/** - * 遍歷內置資料中全部有字之音韻地位。 - * @returns 迭代器,所有至少對應一個字頭的音韻地位 - */ -export function* iter音韻地位(): IterableIterator<音韻地位> { - for (const 音韻編碼 of m音韻編碼檢索.keys()) { - yield decode音韻編碼(音韻編碼); - } -} - -/** - * 由字頭查出相應的音韻地位、反切、解釋。 - * @param 字頭 待查找的漢字 - * @returns 陣列,每一項包含音韻地位和解釋 - * - * 若查不到該字,則回傳空陣列。 - * @example - * ```typescript - * > Qieyun.資料.query字頭('結'); - * [ { 字頭: '結', 音韻地位: 音韻地位 { '見開四先入' }, 韻目原貌: '屑', 反切: '古屑', 釋義: '締也古屑切十五' } ] - * > Qieyun.資料.query字頭('冷'); - * [ - * { 字頭: '冷', 音韻地位: 音韻地位 { '來開四青平' }, 韻目原貌: '青', 反切: '郎丁', 釋義: '冷凙吳人云冰凌又力頂切' }, - * { 字頭: '冷', 音韻地位: 音韻地位 { '來開二庚上' }, 韻目原貌: '梗', 反切: '魯打', 釋義: '寒也魯打切又魯頂切一' }, - * { 字頭: '冷', 音韻地位: 音韻地位 { '來開四青上' }, 韻目原貌: '迥', 反切: '力鼎', 釋義: '寒也又姓前趙錄有徐州刺史冷道字安義又盧打切' }, - * ] - * ``` - */ -export function query字頭(字頭: string): 檢索結果[] { - return m字頭檢索.get(字頭)?.map(結果from內部結果) ?? []; -} - -/** - * 查詢音韻地位對應的字頭、反切、解釋。 - * - * @param 地位 待查詢的音韻地位 - * - * @returns 陣列,每一項包含音韻地位和解釋 - * - * 若音韻地位有音無字,則值為空陣列。 - * @example - * ```typescript - * > 地位 = Qieyun.音韻地位.from描述('影開二銜去'); - * > Qieyun.資料.query音韻地位(地位); - * [ { 字頭: '𪒠', 音韻地位: 音韻地位 { ''影開二銜去' }, 韻部原貌: '鑑', 反切: null, 解釋: '叫呼仿佛𪒠然自得音黯去聲一' } ] - * ``` - */ -export function query音韻地位(地位: 音韻地位): 檢索結果[] { - return m音韻編碼檢索.get(encode音韻編碼(地位))?.map(結果from內部結果) ?? []; -} diff --git "a/src/lib/\350\263\207\346\226\231\346\237\245\350\251\242.spec.ts" "b/src/lib/\350\263\207\346\226\231.spec.ts" similarity index 74% rename from "src/lib/\350\263\207\346\226\231\346\237\245\350\251\242.spec.ts" rename to "src/lib/\350\263\207\346\226\231.spec.ts" index 6ff58b5..273d178 100644 --- "a/src/lib/\350\263\207\346\226\231\346\237\245\350\251\242.spec.ts" +++ "b/src/lib/\350\263\207\346\226\231.spec.ts" @@ -2,7 +2,7 @@ import { readFileSync } from 'fs'; import test from 'ava'; -import { query字頭, query音韻地位 } from './解析資料'; +import { query字頭, query音韻地位 } from './資料'; import { 音韻地位 } from './音韻地位'; test('查「東」字的反切', t => { @@ -59,14 +59,20 @@ test('查詢「韓」字。「韓」是《廣韻》「亦作」字頭', t => { t.is(res[0].字頭, '𩏑'); }); -test('查詢韻目原貌', t => { - t.is(query字頭('劒')[0]?.韻目原貌, '梵'); - t.is(query字頭('茝').find(({ 音韻地位 }) => 音韻地位.屬於('廢韻'))?.韻目原貌, '海'); +test('查詢來源', t => { + t.like( + query字頭('茝').find(({ 音韻地位 }) => 音韻地位.屬於('廢韻')), + { 來源: { 文獻: '廣韻', 韻目: '海' } }, + ); + t.like( + query字頭('韻').find(({ 音韻地位 }) => 音韻地位.屬於('B類')), + { 來源: { 文獻: '王三', 韻目: '震' } }, + ); }); test('根據原資料檔查詢所有字頭', t => { for (const line of readFileSync('prepare/data.csv', { encoding: 'utf8' }).split('\n').slice(1, -1)) { - const [, , 韻目原貌1, 地位描述1, 原反切1, 字頭1, 字頭又作1, 原釋義1, 釋義補充1] = line.split(','); + const [, , 韻目原貌, 地位描述1, 原反切1, 字頭1, 字頭又作1, 原釋義1, 釋義補充1] = line.split(','); if (!地位描述1) { continue; } @@ -75,8 +81,15 @@ test('根據原資料檔查詢所有字頭', t => { const 音韻地位1 = 音韻地位.from描述(地位描述1); const query = (查詢字頭: string) => - query字頭(查詢字頭).some(({ 字頭: 字頭2, 音韻地位: 音韻地位2, 韻目原貌: 韻目原貌2, 反切: 反切2, 釋義: 釋義2 }) => { - return 字頭1 === 字頭2 && 音韻地位1.等於(音韻地位2) && 韻目原貌1 == 韻目原貌2 && 反切1 === 反切2 && 釋義1 === 釋義2; + query字頭(查詢字頭).some(({ 字頭: 字頭2, 音韻地位: 音韻地位2, 反切: 反切2, 釋義: 釋義2, 來源 }) => { + return ( + 字頭1 === 字頭2 && + 音韻地位1.等於(音韻地位2) && + 反切1 === 反切2 && + 釋義1 === 釋義2 && + 來源?.文獻 === '廣韻' && + 來源.韻目 === 韻目原貌 + ); }); t.true(query(字頭1), line); diff --git "a/src/lib/\350\263\207\346\226\231.ts" "b/src/lib/\350\263\207\346\226\231.ts" new file mode 100644 index 0000000..d65b555 --- /dev/null +++ "b/src/lib/\350\263\207\346\226\231.ts" @@ -0,0 +1,170 @@ +import 資料 from '../data/廣韻'; + +import { decode音韻編碼, encode音韻編碼 } from './壓縮表示'; +import { 音韻地位 } from './音韻地位'; + +type 內部檢索結果 = Readonly<{ 字頭: string; 編碼: string; 反切: string | null; 釋義: string; 來源: 來源類型 | null }>; + +export interface 檢索結果 { + 字頭: string; + 音韻地位: 音韻地位; + /** 反切,若未用反切注音(如「音某字某聲」)則為 `null` */ + 反切: string | null; + 釋義: string; + 來源: 來源類型 | null; +} +export type 來源類型 = 廣韻來源 | 王三來源; +export interface 廣韻來源 { + 文獻: '廣韻'; + 韻目: string; + // TODO 小韻號等 +} +export interface 王三來源 { + 文獻: '王三'; + 韻目: string; + // TODO 小韻號等 +} + +const m字頭檢索 = new Map(); +const m音韻編碼檢索 = new Map(); + +// NOTE This is for ensuring *invariance*(-ish) on the type of the map of `insertInto`. +// This way, the type of `map` (`T`) is inferred first, then the other two arguments will be checked against it, rather than the types of +// `key` and `value` dictating what the map should be like (because TypeScript sees `map` as *covariant* by default, which is not suitable +// for mutable operations like insertion). +type KeyOfMap = T extends Map ? K : never; +type ValueOfMap = T extends Map ? V : never; +type ArrayElement = T extends (infer U)[] ? U : never; + +function insertInto = Map>(map: T, key: KeyOfMap, value: ArrayElement>) { + if (!map.has(key)) { + map.set(key, [value]); + } else { + map.get(key)!.push(value); + } +} + +(function 早期廣韻外字() { + const 字頭 = '韻'; + const 編碼 = encode音韻編碼(音韻地位.from描述('云合三B真去')); + const record = { + 字頭, + 編碼, + 反切: '爲捃', + 釋義: '為捃反音和一', + 來源: { 文獻: '王三' as const, 韻目: '震' }, + }; + insertInto(m字頭檢索, 字頭, record); + insertInto(m音韻編碼檢索, 編碼, record); +})(); + +(function 解析廣韻資料() { + const patternOuter = /([\w$]{3})(..)(.)(.*?\n)/gu; + for (const [, 編碼, maybe反切, 韻目原貌, 各條目] of 資料.matchAll(patternOuter)) { + // '@@' is a placeholder in the original data to indicate that there is no 反切 + const 反切 = maybe反切 === '@@' ? null : maybe反切; + + const patternInner = /(.)((?:\+.)*)(.*?)[|\n]/gu; + for (const [, 字頭, 字頭又作, 釋義] of 各條目.matchAll(patternInner)) { + const record = { 字頭, 編碼, 反切, 釋義, 來源: { 文獻: '廣韻' as const, 韻目: 韻目原貌 } }; + + insertInto(m字頭檢索, 字頭, record); + for (const [, 別體] of 字頭又作.matchAll(/\+(.)/g)) { + insertInto(m字頭檢索, 別體, record); + } + + insertInto(m音韻編碼檢索, 編碼, record); + } + } +})(); + +function 結果from內部結果(內部結果: 內部檢索結果): 檢索結果 { + const { 字頭, 編碼, 來源, ...rest } = 內部結果; + return { + 字頭, + 音韻地位: decode音韻編碼(編碼), + ...rest, + 來源: 來源 ? { ...來源 } : null, + }; +} + +/** + * 遍歷內置資料中全部有字之音韻地位。 + * @returns 迭代器,所有至少對應一個字頭的音韻地位 + */ +export function* iter音韻地位(): IterableIterator<音韻地位> { + for (const 音韻編碼 of m音韻編碼檢索.keys()) { + yield decode音韻編碼(音韻編碼); + } +} + +/** + * 由字頭查出相應的音韻地位、反切、解釋。 + * @param 字頭 待查找的漢字 + * @returns 陣列,每一項包含音韻地位和解釋 + * + * 若查不到該字,則回傳空陣列。 + * @example + * ```typescript + * > Qieyun.資料.query字頭('結'); + * [ { + * 字頭: '結', + * 音韻地位: 音韻地位 { '見開四先入' }, + * 反切: '古屑', + * 釋義: '締也古屑切十五', + * 來源: { 文獻: '廣韻', 韻目: '屑' }, + * } ] + * > Qieyun.資料.query字頭('冷'); + * [ + * { + * 字頭: '冷', + * 音韻地位: 音韻地位 { '來開四青平' }, + * 反切: '郎丁', + * 釋義: '冷凙吳人云冰凌又力頂切', + * 來源: { 文獻: '廣韻', 韻目: '青' }, + * }, + * { + * 字頭: '冷', + * 音韻地位: 音韻地位 { '來開二庚上' }, + * 反切: '魯打', + * 釋義: '寒也魯打切又魯頂切一', + * 來源: { 文獻: '廣韻', 韻目: '梗' }, + * }, + * { + * 字頭: '冷', + * 音韻地位: 音韻地位 { '來開四青上' }, + * 反切: '力鼎', + * 釋義: '寒也又姓前趙錄有徐州刺史冷道字安義又盧打切', + * 來源: { 文獻: '廣韻', 韻目: '迥' }, + * }, + * ] + * ``` + */ +export function query字頭(字頭: string): 檢索結果[] { + return m字頭檢索.get(字頭)?.map(結果from內部結果) ?? []; +} + +/** + * 查詢音韻地位對應的字頭、反切、解釋。 + * + * @param 地位 待查詢的音韻地位 + * + * @returns 陣列,每一項包含音韻地位和解釋 + * + * 若音韻地位有音無字,則值為空陣列。 + * @example + * ```typescript + * > 地位 = Qieyun.音韻地位.from描述('影開二銜去'); + * > Qieyun.資料.query音韻地位(地位); + * [ { + * 字頭: '𪒠', + * 音韻地位: 音韻地位 { ''影開二銜去' }, + * 反切: null, + * 解釋: '叫呼仿佛𪒠然自得音黯去聲一', + * 來源: { 文獻: '廣韻', 韻目: '鑑' }, + * } ] + * ``` + */ +export function query音韻地位(地位: 音韻地位): 檢索結果[] { + return m音韻編碼檢索.get(encode音韻編碼(地位))?.map(結果from內部結果) ?? []; +} 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 ec82364..39b388f 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" @@ -1,6 +1,6 @@ import test from 'ava'; -import { iter音韻地位 } from './解析資料'; +import { iter音韻地位 } from './資料'; import { 判斷規則列表, 邊緣地位種類指定, 音韻地位 } from './音韻地位'; // 由音韻地位得出各項音韻屬性