将带有数字的拼音字符串转换为带有声调标记的拼音字符串 js实现

204 阅读7分钟

拼音标注规则

整理一下拼音标注规则。以下是拼音标注的主要规则,按照优先级排序:

  1. 元音优先级 主要元音的优先级顺序为:a > o > e > i > u > ü 在多元音组合中,声调标在优先级最高的元音上。

  2. 单元音 直接在唯一的元音上标注声调。例如:mā, lé, nǐ, bò, chū

  3. 多元音组合 a. 有 'a' 或 'o' 时,标在 'a' 或 'o' 上。例如:

    • huái (怀), kuài (快), biāo (标), guó (国)

    b. 有 'e' 时,标在 'e' 上。例如:

    • piē (撇), tiē (贴)

    c. 'i', 'u', 'ü' 组合时,标在最后一个元音上。例如:

    • liú (流), guī (归), lǜ (绿)
  4. 特殊情况 a. 'iu' 组合:声调标在 'u' 上。例如:

    • liú (流), qiú (球)

    b. 'ui' 组合:声调标在 'i' 上。例如:

    • guì (贵), kuí (亏)
  5. 'ü' 的处理

    • 在 'ju', 'qu', 'xu', 'yu' 中,'u' 读作 'ü',但写作 'u'。例如:
      • jú (橘), qú (区), xú (需), yú (鱼)
    • 其他情况下直接使用 'ü'。例如:
      • lǜ (绿), nǚ (女)
  6. 轻声(第五声) 不标注声调符号。例如:ma (吗)

  7. 'i' 和 'u' 作为介音 声调不标在介音上,而是按照上述规则标在主要元音上。例如:

    • jiā (家), kuài (快)
  8. 多音节词 每个音节独立按照上述规则标注。例如:

    • Běijīng (北京), Zhōngguó (中国)
  9. 大小写 声调符号应该保持原有的大小写。例如:

    • Āiyō (哎哟), WǓHÀN (武汉)
  10. '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 -> ài
  • ei2 -> éi
  • ao3 -> ǎo
  • ou1 -> ōu
  • iu4 -> iù (特殊复合音)
  • ui2 -> uí (特殊复合音)

3. 多音节词测试

多音节拼音,应该正确标注每个音节的声调,不需要隔音符号 '

  • zhong1guo2 -> zhōngguó
  • bei3jing1 -> běijīng
  • pu3tao2 -> pǔtáo
  • lao3shi1 -> lǎoshī

4. 轻声测试

轻声应该输出没有声调符号的拼音。

  • ma5 -> ma
  • zi5 -> zi

5. ü 和 v 的处理

在拼音中,v 通常被用来表示 ü,需要在输出时将其转换为带声调的 ü

  • lü3 -> lǚ
  • nü2 -> nǘ
  • lv4 -> lǜ

6. 大小写混合测试

确保转换时保留输入的大小写格式。

  • Zhong1Guo2 -> ZhōngGuó
  • Bei3Jing1 -> BěiJīng
  • AI4YO1 -> ÀIYŌ

7. 隔音符号测试

隔音符号 ' 应该在两个音节之间产生混淆时使用(即前一个音节以元音、nng 结尾,且下一个音节以元音开头时)。

  • xi1an1 -> xī'ān
  • ji1an1 -> jī'ān
  • lu: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 -> ér
  • er4 -> èr

9. 非法输入测试

测试非法或未定义输入,以确保代码处理得当。

  • zhong6guo2 -> zhong6guó6 是非法声调)
  • bei3jing0 -> běijing00 是非法声调)

10. 复杂组合测试

多音节长拼音词的测试,包括正确处理声调和隔音符号。

  • yi1lou2ding3shang4tian1 -> yīlóudǐngshàngtiān
  • Zhong1Hua2Ren2Min2Gong4He2Guo2 -> 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);
});