Inspired by CtCI 16.8: Integer to English phrase in C++, I wrote a program to show the Japanese reading of an integer (positive, negative, or zero). The Japanese is written in Hepburn romanization to avoid encoding problems. Also, traditional Hepburn is used to avoid macrons.
Unlike English, Japanese has sound changes when two words come together. The details are given at the end of the question.
The program handles integers \$n\$ in the range \$-2^{63} \le n < 2^{63}\$.
Code
/**
* Integer to Japanese reading
*
* Japanese reading is written in Hepburn Roomaji. Traditional
* Hepburn is used to avoid the need of macrons.
*/
#include <cassert>
#include <cstdint>
#include <iostream>
#include <string>
#include <vector>
using number_t = std::int_fast64_t;
const std::vector<std::string> magnitudes = {
"", "man", "oku", "choo", "kee",
};
const std::vector<std::string> thousands = {
"", "sen", "nisen", "sanzen", "yonsen",
"gosen", "rokusen", "nanasen", "hassen", "kyuusen",
};
const std::vector<std::string> hundreds = {
"", "hyaku", "nihyaku", "sanbyaku", "yonhyaku",
"gohyaku", "roppyaku", "nanahyaku", "happyaku", "kyuuhyaku",
};
const std::vector<std::string> tens = {
"", "juu", "nijuu", "sanjuu", "yonjuu",
"gojuu", "rokujuu", "nanajuu", "hachijuu", "kyuujuu",
};
const std::vector<std::string> ones = {
"", "ichi", "ni", "san", "yon",
"go", "roku", "nana", "hachi", "kyuu",
};
// returns 10000^n
constexpr number_t magnitude(std::size_t n)
{
number_t result = 1;
while (n--)
result *= 10000;
return result;
}
constexpr bool is_vowel(char c)
{
switch (c) {
case 'a': case 'e': case 'i': case 'o': case 'u': case 'y':
return true;
default:
return false;
}
}
// joins two strings according to Japanese rules
void push(std::string& lhs, const std::string& rhs)
{
if (lhs.back() == 'n' && is_vowel(rhs.front()))
lhs += '\'';
lhs += rhs;
}
// converts nonnegative numbers less than 10000
std::string group_name(number_t number)
{
assert(0 <= number && number < 10000);
std::string result = thousands[number / 1000];
number %= 1000;
push(result, hundreds[number / 100]);
number %= 100;
push(result, tens[number / 10]);
number %= 10;
push(result, ones[number]);
return result;
}
std::string to_Japanese(number_t number)
{
if (number == 0)
return "zero";
std::string result;
if (number < 0) {
result = "mainasu";
number = -number;
}
number_t mag = magnitude(magnitudes.size() - 1);
for (std::size_t i = magnitudes.size(); i-- > 0; mag /= 10000) {
if (auto group = number / mag; group > 0) {
push(result, group_name(group));
push(result, magnitudes[i]);
}
number %= mag;
}
return result;
}
int main()
{
for (number_t number; std::cin >> number;)
std::cout << to_Japanese(number) << "\n";
}
Example session
0
zero
1
ichi
2
ni
3
san
6789678967896789
rokusennanahyakuhachijuukyuuchoorokusennanahyakuhachijuukyuuokurokusennanahyakuhachijuukyuumanrokusennanahyakuhachijuukyuu
-1234567898765432
mainasusennihyakusanjuuyonchoogosenroppyakunanajuuhachiokukyuusenhappyakunanajuurokumangosen'yonhyakusanjuuni
Japanese numerals
(You can skip this part if you are familiar with Japanese.)
In the following table, red entries involve sound change.
\begin{array}{ll} \text{Number} & \text{Japanese reading} \\ 1 & \text{一 (ichi)} \\ 2 & \text{二 (ni)} \\ 3 & \text{三 (san)} \\ 4 & \text{四 (yon)} \\ 5 & \text{五 (go)} \\ 6 & \text{六 (roku)} \\ 7 & \text{七 (nana)} \\ 8 & \text{八 (hachi)} \\ 9 & \text{九 (kyuu)} \\ 10 & \text{十 (juu)} \\ 20 & \text{二十 (nijuu)} \\ 30 & \text{三十 (sanjuu)} \\ 40 & \text{四十 (yonjuu)} \\ 50 & \text{五十 (gojuu)} \\ 60 & \text{六十 (rokujuu)} \\ 70 & \text{七十 (nanajuu)} \\ 80 & \text{八十 (hachijuu)} \\ 90 & \text{九十 (kyuujuu)} \\ 100 & \text{百 (hyaku)} \\ 200 & \text{二百 (nihyaku)} \\ 300 & \text{三百 } \color{red}{\text{(sanbyaku)}} \\ 400 & \text{四百 (yonhyaku)} \\ 500 & \text{五百 (gohyaku)} \\ 600 & \text{六百 } \color{red}{\text{(roppyaku)}} \\ 700 & \text{七百 (nanahyaku)} \\ 800 & \text{八百 } \color{red}{\text{(happyaku)}} \\ 900 & \text{九百 (kyuuhyaku)} \\ 1000 & \text{千 (sen)} \\ 2000 & \text{二千 (nisen)} \\ 3000 & \text{三千 } \color{red}{\text{(sanzen)}} \\ 4000 & \text{四千 (yonsen)} \\ 5000 & \text{五千 (gosen)} \\ 6000 & \text{六千 (rokusen)} \\ 7000 & \text{七千 (nanasen)} \\ 8000 & \text{八千 } \color{red}{\text{(hassen)}} \\ 9000 & \text{九千 (kyuusen)} \\ \end{array}
Larger numbers are considered the sums of smaller numbers. For example:
\begin{array}{ccccccc} 2019 & = & 2000 & + & 10 & + & 9 \\ \text{二千十九 (nisenjuukyuu)} & & \text{二千 (nisen)} & & \text{十 (juu)} & & \text{九 (kyuu)} \end{array}
The missing hundred place is simply ignored.
Four digits are considered a group, unlike English, where three digits are a group. The group markers are:
\begin{array}{ccc} 10^4 & 10^8 & 10^{12} & 10^{16} \\ \text{万 (man)} & \text{億 (oku)} & \text{兆 (choo)} & \text{京 (kee)} \end{array}
For example, \$1\,2345\,6789\$ is read as 一億二千三百四十五万六千七百八十九 (ichioku nisensanbyakuyonjuugoman rokusennanahyakuhachijuukyuu). (The spaces are for ease of recognition only.) Note that 一 (ichi) is required before these group markers, unlike 十 (juu), 百 (hyaku), and 千 (sen).
0 is ゼロ (zero).
A negative integer \$-n\$ is read as マイナス (mainasu) followed by its absolute value \$n\$. For example, -5 is read as マイナス五 (mainasugo), because 5 is read as 五 (go).
When two syllables are joined, if the first syllable ends with "n" and the second starts with one of "aeiouy", then a separator ' is added between. For example, 1001 is 千一 (sen'ichi) and 1004 is 千四 (sen'yon).