拼音标注规则
整理一下拼音标注规则。以下是拼音标注的主要规则,按照优先级排序:
-
元音优先级 主要元音的优先级顺序为:a > o > e > i > u > ü 在多元音组合中,声调标在优先级最高的元音上。
-
单元音 直接在唯一的元音上标注声调。例如:mā, lé, nǐ, bò, chū
-
多元音组合 a. 有 'a' 或 'o' 时,标在 'a' 或 'o' 上。例如:
- huái (怀), kuài (快), biāo (标), guó (国)
b. 有 'e' 时,标在 'e' 上。例如:
- piē (撇), tiē (贴)
c. 'i', 'u', 'ü' 组合时,标在最后一个元音上。例如:
- liú (流), guī (归), lǜ (绿)
-
特殊情况 a. 'iu' 组合:声调标在 'u' 上。例如:
- liú (流), qiú (球)
b. 'ui' 组合:声调标在 'i' 上。例如:
- guì (贵), kuí (亏)
-
'ü' 的处理
- 在 'ju', 'qu', 'xu', 'yu' 中,'u' 读作 'ü',但写作 'u'。例如:
- jú (橘), qú (区), xú (需), yú (鱼)
- 其他情况下直接使用 'ü'。例如:
- lǜ (绿), nǚ (女)
- 在 'ju', 'qu', 'xu', 'yu' 中,'u' 读作 'ü',但写作 'u'。例如:
-
轻声(第五声) 不标注声调符号。例如:ma (吗)
-
'i' 和 'u' 作为介音 声调不标在介音上,而是按照上述规则标在主要元音上。例如:
- jiā (家), kuài (快)
-
多音节词 每个音节独立按照上述规则标注。例如:
- Běijīng (北京), Zhōngguó (中国)
-
大小写 声调符号应该保持原有的大小写。例如:
- Āiyō (哎哟), WǓHÀN (武汉)
-
'er' 的处理 当 'er' 作为整体发音时(儿化音),声调标在 'e' 上。例如:
- ér (儿), nǎr (哪儿)
这些规则涵盖了大多数拼音标注的情况。在实际应用中,可能还会遇到一些罕见或特殊的情况,但这些基本规则应该能够处理绝大多数的拼音标注需求。
实现方法
/**
* 将带有数字的拼音字符串转换为带有声调标记的拼音字符串
* @param {string} pinyinWithNumbers - 带有数字的拼音字符串,数字范围为1到5,表示声调
* @returns {string} - 转换为带有声调标记的拼音字符串
*/
function numberToTone(pinyinWithNumbers) {
const toneMarks = {
a: ['ā', 'á', 'ǎ', 'à', 'a'],
o: ['ō', 'ó', 'ǒ', 'ò', 'o'],
e: ['ē', 'é', 'ě', 'è', 'e'],
i: ['ī', 'í', 'ǐ', 'ì', 'i'],
u: ['ū', 'ú', 'ǔ', 'ù', 'u'],
ü: ['ǖ', 'ǘ', 'ǚ', 'ǜ', 'ü']
};
const vowelPriority = 'aoeiu';
return pinyinWithNumbers.replace(/([a-zü]+)([1-5])/gi, (match, syllable, tone) => {
const toneIndex = parseInt(tone) - 1;
if (toneIndex === 4) return syllable; // 处理轻声
syllable = syllable.toLowerCase().replace(/v/g, 'ü');
const vowels = syllable.match(/[aoeiu]/gi) || [];
if (vowels.length === 0) return match;
let mainVowel = 'ü';
let mainVowelIndex = syllable.indexOf('ü');
// 处理特殊情况
if (syllable.includes('iu')) {
mainVowel = 'u';
mainVowelIndex = syllable.lastIndexOf('u');
} else if (syllable.includes('ui')) {
mainVowel = 'i';
mainVowelIndex = syllable.lastIndexOf('i');
} else {
// 按优先级查找主要元音
for (let vowel of vowelPriority) {
if (syllable.includes(vowel)) {
mainVowel = vowel;
mainVowelIndex = syllable.indexOf(vowel);
break;
}
}
}
// 处理 er 的特殊情况
if (syllable === 'er') {
mainVowel = 'e';
mainVowelIndex = 0;
}
let result = syllable.substring(0, mainVowelIndex) +
toneMarks[mainVowel][toneIndex] +
syllable.substring(mainVowelIndex + 1);
// 恢复原始大小写
return result.split('').map((char, index) => {
return match[index] === match[index].toUpperCase() ? char.toUpperCase() : char;
}).join('');
});
}
改进版本:
const toneMarks = new Map([
['a', ['ā', 'á', 'ǎ', 'à', 'a']], // a的不同声调
['o', ['ō', 'ó', 'ǒ', 'ò', 'o']], // o的不同声调
['e', ['ē', 'é', 'ě', 'è', 'e']], // e的不同声调
['i', ['ī', 'í', 'ǐ', 'ì', 'i']], // i的不同声调
['u', ['ū', 'ú', 'ǔ', 'ù', 'u']], // u的不同声调
['ü', ['ǖ', 'ǘ', 'ǚ', 'ǜ', 'ü']] // ü的不同声调
]);
const vowelPriority = 'aoeiuü'; // 元音优先级顺序,表示遇到多个元音时,优先给谁加声调
const pinyinRegex = /([a-zü]+)([1-5])/gi; // 匹配带声调数字的拼音正则表达式
/**
* 将带数字声调的拼音转换为带音调符号的拼音。
*
* @param {string} pinyinWithNumbers - 带数字声调的拼音字符串
* @returns {string} 转换后的带音调符号的拼音字符串
*/
function numberToTone(pinyinWithNumbers) {
// 将输入字符串按声调数字分割成各个拼音音节
const syllables = pinyinWithNumbers.split(/(?<=[1-5])/g);
// 处理每个音节,将数字转换为音调符号
const convertedSyllables = syllables.map(syllableWithTone => {
return syllableWithTone.replace(pinyinRegex, (match, syllable, tone) => {
const toneIndex = parseInt(tone) - 1; // 将拼音中的数字转换为数组索引
if (toneIndex === 4) return syllable; // 轻声(数字5)不需要转换声调
// 将拼音中的v替换为ü,以处理ü的特殊情况
syllable = syllable.toLowerCase().replace(/v/g, 'ü');
// 找到拼音中的主元音,用于加声调
let mainVowel, mainVowelIndex;
if (syllable.includes('iu')) {
// iu情况下,u为主要元音
[mainVowel, mainVowelIndex] = ['u', syllable.lastIndexOf('u')];
} else if (syllable.includes('ui')) {
// ui情况下,i为主要元音
[mainVowel, mainVowelIndex] = ['i', syllable.lastIndexOf('i')];
} else if (syllable === 'er') {
// er的特殊处理,e为主要元音
[mainVowel, mainVowelIndex] = ['e', 0];
} else {
// 默认找到优先级最高的元音作为主要元音
mainVowel = vowelPriority.split('').find(v => syllable.includes(v)) || 'ü';
mainVowelIndex = syllable.indexOf(mainVowel);
}
// 如果没有找到主要元音,直接返回原始字符串
if (mainVowelIndex === -1) return match;
// 根据主元音和声调索引获取带声调的元音
const newVowel = toneMarks.get(mainVowel)[toneIndex];
// 拼接出新的拼音字符串,将带声调的元音替换掉原有元音
const result = syllable.substring(0, mainVowelIndex) + newVowel + syllable.substring(mainVowelIndex + 1);
// 保留原始输入中的大小写格式
return result.replace(/./g, (char, index) =>
match[index] === match[index].toUpperCase() ? char.toUpperCase() : char
);
});
}).join(''); // 将处理好的拼音片段重新连接为字符串
// 插入必要的撇号 查找符合隔音符号规则的地方并插入'
// 规则是:如果一个音节以元音(或n / ng)结尾,而下一个音节以元音开头,则需要添加隔音符号。
const result = convertedSyllables.replace(/([aeiouü])([aeiouü])/gi, '$1\'$2').replace(/([aeiouü])(n?g?)([aeiouü])/gi, '$1$2\'$3');
return result;
}
/**
* 运行测试用例并输出结果,比较生成的拼音与预期结果。
*
* @param {string} input - 输入的带数字声调的拼音字符串
* @param {string} expected - 期望的带音调符号的拼音字符串
*/
function runTest(input, expected) {
const result = numberToTone(input);
console.log(`Input: ${input}`);
console.log(`Result: ${result}`);
console.log(`Expected: ${expected}`);
console.log(`Test ${result === expected ? 'PASSED' : 'FAILED'}`);
console.log('---');
}
测试用例
测试的详细分类和相应的测试用例:
1. 基本元音音节测试
这是最简单的测试,包含单个音节并带有不同的声调。
a1 -> āo2 -> óe3 -> ěi4 -> ìu5 -> u(轻声)ü1 -> ǖv2 -> ǘ(v被转换为ü)
2. 复合元音音节测试
这些拼音包含多个元音,需要根据优先级给正确的元音加上声调。
ai4 -> àiei2 -> éiao3 -> ǎoou1 -> ōuiu4 -> iù(特殊复合音)ui2 -> uí(特殊复合音)
3. 多音节词测试
多音节拼音,应该正确标注每个音节的声调,不需要隔音符号 '。
zhong1guo2 -> zhōngguóbei3jing1 -> běijīngpu3tao2 -> pǔtáolao3shi1 -> lǎoshī
4. 轻声测试
轻声应该输出没有声调符号的拼音。
ma5 -> mazi5 -> zi
5. ü 和 v 的处理
在拼音中,v 通常被用来表示 ü,需要在输出时将其转换为带声调的 ü。
lü3 -> lǚnü2 -> nǘlv4 -> lǜ
6. 大小写混合测试
确保转换时保留输入的大小写格式。
Zhong1Guo2 -> ZhōngGuóBei3Jing1 -> BěiJīngAI4YO1 -> ÀIYŌ
7. 隔音符号测试
隔音符号 ' 应该在两个音节之间产生混淆时使用(即前一个音节以元音、n、ng 结尾,且下一个音节以元音开头时)。
xi1an1 -> xī'ānji1an1 -> jī'ānlu:e4 -> lüèxian1 -> xiān(不需要隔音符号)xiang1 -> xiāng(不需要隔音符号)ji'an -> ji'an(已经含有隔音符号)
8. 特殊拼音音节测试
这些拼音音节有特殊的发音或拼写规则。
zhi1 -> zhīchi4 -> chìshi2 -> shíri4 -> rìzi3 -> zǐci2 -> císi1 -> sīer2 -> érer4 -> èr
9. 非法输入测试
测试非法或未定义输入,以确保代码处理得当。
zhong6guo2 -> zhong6guó(6是非法声调)bei3jing0 -> běijing0(0是非法声调)
10. 复杂组合测试
多音节长拼音词的测试,包括正确处理声调和隔音符号。
yi1lou2ding3shang4tian1 -> yīlóudǐngshàngtiānZhong1Hua2Ren2Min2Gong4He2Guo2 -> ZhōngHuáRénMínGòngHéGuó
const testCases = [
// 基本元音测试
["a1", "ā"],
["o2", "ó"],
["e3", "ě"],
["i4", "ì"],
["u5", "u"], // 轻声
["ü1", "ǖ"],
["v2", "ǘ"],
// 复合元音测试
["ai4", "ài"],
["ei2", "éi"],
["ao3", "ǎo"],
["ou1", "ōu"],
["iu4", "iù"],
["ui2", "uí"],
// 多音节词测试
["zhong1guo2", "zhōngguó"],
["bei3jing1", "běijīng"],
["pu3tao2", "pǔtáo"],
["lao3shi1", "lǎoshī"],
// 轻声测试
["ma5", "ma"],
["zi5", "zi"],
// ü 的处理
["lü3", "lǚ"],
["nü2", "nǘ"],
["lv4", "lǜ"],
// 大小写混合测试
["Zhong1Guo2", "ZhōngGuó"],
["Bei3Jing1", "Běijīng"],
["AI4YO1", "ÀIYŌ"],
// 隔音符号测试
["xi1an1", "xī'ān"],
["ji1an1", "jī'ān"],
["lu:e4", "lüè"],
["xian1", "xiān"], // 不需要隔音符号
["xiang1", "xiāng"], // 不需要隔音符号
["ji'an", "ji'an"], // 已含有隔音符号
// 特殊拼音音节测试
["zhi1", "zhī"],
["chi4", "chì"],
["shi2", "shí"],
["ri4", "rì"],
["zi3", "zǐ"],
["ci2", "cí"],
["si1", "sī"],
["er2", "ér"],
["er4", "èr"],
// 非法输入测试
["zhong6guo2", "zhong6guó"],
["bei3jing0", "běijing0"],
// 复杂组合测试
["yi1lou2ding3shang4tian1", "yīlóudǐngshàngtiān"],
["Zhong1Hua2Ren2Min2Gong4He2Guo2", "ZhōngHuáRénMínGòngHéGuó"],
];
// 运行所有测试用例
testCases.forEach(([input, expected]) => {
runTest(input, expected);
});