Storing Japanese pitch accent as one number (and rendering it in 15 lines of JS)
Storing Japanese pitch accent as one number (and rendering it in 15 lines of JS)
If you are building a flashcard app, a dictionary UI or anything else that teaches Japanese, sooner or later someone asks for pitch accent. Japanese pitch accent is not stress: syllables are not louder or longer, they are simply higher or lower, and the place where the pitch drops can change the meaning of a word. Here is a short explanation of Japanese pitch accent with audio examples if you want the linguistics first. This post is about the data side: how to store it compactly and draw it.
Why Japanese pitch accent needs only one number
Japanese dictionaries describe a word's accent with a single integer — the mora after which the pitch drops. In standard (Tokyo) Japanese everything else follows from that number:
- 0 — no drop (heiban): first mora low, the rest high, and a following particle stays high;
- 1 — drop after the first mora (atamadaka): high, then everything low;
- n — the first mora is low, morae 2…n are high, then the pitch drops. If n equals the word length (odaka), the drop only shows up on the particle.
The classic triplet is はし:
| Word | Meaning | Accent | Pattern + particle |
|---|---|---|---|
| 箸 | chopsticks | 1 | H L – L |
| 橋 | bridge | 2 | L H – L |
| 端 | edge | 0 | L H – H |
Note that 橋 and 端 sound identical on their own. The difference appears only when you say はしが or はしを — which is exactly why your data model should keep the particle in mind.
Counting morae for pitch accent
The accent number counts morae, not kana characters. Small ゃ ゅ ょ merge with the preceding kana (きょ is one mora), while っ, ん and the long-vowel mark ー each count as a full mora. That is easy to code:
const SMALL = new Set([...'ゃゅょぁぃぅぇぉャュョァィゥェォ']); function morae(kana) { const out = []; for (const ch of kana) { if (SMALL.has(ch) && out.length) out[out.length - 1] += ch; else out.push(ch); } return out; } morae('きょう'); // ['きょ', 'う'] morae('がっこう'); // ['が', 'っ', 'こ', 'う'] Rendering the pattern
With morae and the accent number, the high/low pattern is one expression:
function pitchPattern(kana, accent) { const m = morae(kana); const high = (i) => accent === 1 ? i === 0 : i > 0 && (accent === 0 || i < accent); const word = m.map((_, i) => (high(i) ? 'H' : 'L')).join(''); const particle = accent === 0 ? 'H' : 'L'; return `${word}-${particle}`; } pitchPattern('はし', 1); // 'HL-L' 箸 chopsticks pitchPattern('はし', 2); // 'LH-L' 橋 bridge pitchPattern('はし', 0); // 'LH-H' 端 edge pitchPattern('あなた', 2); // 'LHL-L' あなた you pitchPattern('たまご', 0); // 'LHH-H' 卵 egg From there you can draw an overline over the high morae, a small hook where the pitch drops, or a line chart — whatever fits your UI. Storing { kana: 'はし', accent: 2 } is far cheaper than storing a hand-made pattern for every word, and it never gets out of sync.
Caveats worth putting in your README
- This is the Tokyo standard. Kansai (Osaka) accent works differently, so do not label it as "the" correct pronunciation.
- Some words have two accepted accents; store an array if your source lists both.
- Beginners rarely need to memorise numbers. Context usually tells 箸 and 橋 apart — the pattern is a listening aid, not a test.
What to show learners
In practice the most useful UI is not the number but the melody: a visual H/L line plus native audio, and a way to repeat after it. If you want to see how a learner-facing page presents Japanese pitch accent, compare the examples above with the table and FAQ on Japanese Zen — then decide how much of the theory your users actually need.