キッカケ
数年前に社内の勉強会で出題されたらしい問題で「数字の読み方を表示するプログラムを作る」というのがありました。
※ らしいと言っていますが、私は参加しなかったので。資料だけ拝借しました。へ( ̄o ̄へ)スッ
それで、この問題を見たとき、初めは簡単そうな問題だと思ったのです。
1234 → せんにひゃくさんじゅうよん
こんな問題、ただ桁ごとに分割して switch 文とかで繰り返して読み方に変換していくだけなのでは、と。
ただ、思いのほか上手くいかず。
例えばコレとか。
610 → ろっぴゃくじゅう
おそらく、あまり考えずにプログラムを実装してしまうと、
610 → ろくひゃくいちじゅう
になるパターンです。
「こ、これは、思いのほか、いい問題な気がする。。」
と考え改めまして、実装してみることにしました。
数字の正しい読み方って何?
我々が普段何気なく読んでいる数字ですが、実はその読み方にはちょっとした例外が潜んでいます。
既に記載した 610
もそうですが、他にも 800
も はちひゃく
ではなく、 はっぴゃく
と読み、
300
に限っては さんびゃく
とひゃくが濁音に変換されています。
他にも、9
は きゅう
とも読みますが、 く
とも読めます。
これは思い付くだけ挙げていくのではちょっとスマートではなさそうです。
(ニホンゴハムズカシイネ)
果たして、こういう例外的な読み方も含めてきちんと整理されている資料は存在するのか。
ちょっと調べてみたのですが、なんと存在してました。
どうやら、こういう疑問は言語学の形態論で研究されているジャンルらしく、
論文の 1.1 ~ 1.2 で解答が得られそうです。
では、こちらの論文を元に今回プログラムの仕様となるべく読み方の早見表を作ってみます。
前項 \ 後項 | じゅう | ひゃく | せん | まん | おく | ちょう |
---|---|---|---|---|---|---|
いち | じゅう | ひゃく | せん | まん | いちおく | __いっ__ちょう |
に | にじゅう | にひゃく | にせん | にまん | におく | にちょう |
さん | さんじゅう | さん__びゃく__ | さん__ぜん__ | さんまん | さんおく | さんちょう |
よん | よんじゅう | よんひゃく | よんせん | よんまん | よんおく | よんちょう |
ご | ごじゅう | ごひゃく | ごせん | ごまん | ごおく | ごちょう |
ろく | ろくじゅう | ろっぴゃく | ろくせん | ろくまん | ろくおく | ろくちょう |
なな | ななじゅう | ななひゃく | ななせん | ななまん | ななおく | ななちょう |
はち | はちじゅう | はっぴゃく | __はっ__せん | はちまん | はちおく | __はっ__ちょう |
きゅう | きゅうじゅう | きゅうひゃく | きゅうせん | きゅうまん | きゅうおく | きゅうちょう |
じゅう | - | - | - | じゅうまん | じゅうおく | __じゅっ__ちょう |
ひゃく | - | - | - | ひゃくまん | ひゃくおく | ひゃくちょう |
せん | - | - | - | せんまん | せんおく | せんちょう |
※ 一桁目が 9 の場合は「く」と読めるパターンもありますが、処理を簡単にするために一律「きゅう」としています。
※ 7 の場合は「しち」とも読めますが、聴覚上紛らわしいので「なな」とします。
※ 一桁目と二桁目が 4 の場合は「し」とも読めますが、こちらも処理を簡単にするために一律「よん」としています。
※ 千の位が 1 の場合、「いっせん」「いっせんまん」「いっせんおく」「いっせんちょう」と言ったりする事例もありますが、
それぞれ「せん」「せんまん」「せんおく」「せんちょう」と読んでも誤りではないため、一律「いっせん」ではなく「せん」とします。
では、数字の読み方の仕様が整理できましたので、プログラムで実装していきます。
タイトルにもありますが、こういうのは普通は Python とかで実装例を示すものですが、
こういう問題を見てしまうと、どうしても SQL で実装してみたくなってしまう性なので、ご了承ください。
ストアド(PL/pgSQL)での実装例
後で、 SQL の実装例も記載しますが、先にストアド(以下、PL/pgSQL)の実装を載せておきます。
PL/pgSQL であれば、その辺の手続き型言語とそんなに処理の流れはあまり変わらないです。
PL/pgSQL 未経験者でも前節の表通りに実装されているのが、なんとなくわかると思います。
CREATE OR REPLACE FUNCTION numerical_to_reading(numerical_value bigint) RETURNS varchar AS $$
DECLARE
numerical_char CONSTANT varchar := abs(numerical_value)::varchar;
numerical_length CONSTANT integer := char_length(numerical_char);
current_digit integer;
current_val integer;
ret varchar := '';
BEGIN
IF numerical_value = 0 THEN
-- 一桁の0の場合のみ「ぜろ」が読まれるため、すぐに RETURN で返す
RETURN 'ぜろ';
ELSIF numerical_value < 0 THEN
-- マイナス符号の読み方を定義
ret := ret || 'まいなす';
END IF;
-- 整数部の読み方を求める
FOR i IN 1 .. numerical_length LOOP
current_digit := numerical_length - i + 1;
current_val := substring(numerical_char FROM i FOR 1)::integer;
-- 数字の読み方を逐次解釈する
CASE current_val
WHEN 1 THEN
IF current_digit % 13 = 0 THEN
-- 1兆(いっちょう)の読み方を定義
ret := ret || 'いっ';
ELSIF current_digit % 4 = 1 THEN
ret := ret || 'いち';
ELSE
-- 十、百、千の場合は1自体は読まない
NULL;
END IF;
WHEN 2 THEN
ret := ret || 'に';
WHEN 3 THEN
ret := ret || 'さん';
WHEN 4 THEN
ret := ret || 'よん';
WHEN 5 THEN
ret := ret || 'ご';
WHEN 6 THEN
IF current_digit % 4 = 3 THEN
-- 6百(ろっぴゃく)の読み方を定義
ret := ret || 'ろっ';
ELSE
ret := ret || 'ろく';
END IF;
WHEN 7 THEN
ret := ret || 'なな';
WHEN 8 then
IF current_digit % 13 = 0 OR current_digit % 4 IN (3, 0) THEN
-- 8兆(はっちょう)、8百(はっぴゃく)、8千(はっせん)の読み方を定義
ret := ret || 'はっ';
ELSE
ret := ret || 'はち';
END IF;
WHEN 9 THEN
ret := ret || 'きゅう';
ELSE
NULL;
END CASE;
-- 1桁ごとの読み方を定義(一、十、百、千)
IF current_val = 0 THEN
NULL;
ELSE
CASE current_digit % 4
WHEN 2 then
IF current_digit = 14 AND substring(numerical_char FROM i + 1 FOR 1) = '0' then
-- 10兆(じゅっちょう)の読み方を定義
ret := ret || 'じゅっ';
ELSE
ret := ret || 'じゅう';
END if;
WHEN 3 THEN
IF current_val = 3 THEN
-- 3百(さんびゃく)の読み方を定義
ret := ret || 'びゃく';
ELSIF current_val IN (6, 8) THEN
-- 6百(ろっぴゃく)、8百(はっぴゃく)の読み方を定義
ret := ret || 'ぴゃく';
ELSE
ret := ret || 'ひゃく';
END IF;
WHEN 0 THEN
IF current_val = 3 THEN
-- 3千(さんぜん)の読み方を定義
ret := ret || 'ぜん';
ELSE
ret := ret || 'せん';
END IF;
ELSE
NULL;
END CASE;
END IF;
-- 4桁ごとにつく単位の読み方を定義(万、億、兆)
IF current_digit % 4 = 1 THEN
IF substring(numerical_char FROM i - 3 FOR 4)::integer = 0 THEN
NULL;
else
CASE current_digit
WHEN 5 THEN
ret := ret || 'まん';
WHEN 9 THEN
ret := ret || 'おく';
WHEN 13 THEN
ret := ret || 'ちょう';
ELSE
-- 1の位は何も読まない
NULL;
END CASE;
END IF;
END IF;
END LOOP;
RETURN ret;
END
$$ LANGUAGE plpgsql
IMMUTABLE
RETURNS NULL ON NULL INPUT;
では、 PL/pgSQL で実装した関数を何パターン化の数字で呼び出し、
期待している読み方が取得できていることを確認していきます。
WITH
parameterized_tests(args, expected) AS (
VALUES
(1234567890123456, 'せんにひゃくさんじゅうよんちょうごせんろっぴゃくななじゅうはちおくきゅうせんじゅうにまんさんぜんよんひゃくごじゅうろく'),
(2345678901234567, 'にせんさんびゃくよんじゅうごちょうろくせんななひゃくはちじゅうきゅうおくひゃくにじゅうさんまんよんせんごひゃくろくじゅうなな'),
(3456789012345678, 'さんぜんよんひゃくごじゅうろくちょうななせんはっぴゃくきゅうじゅうおくせんにひゃくさんじゅうよんまんごせんろっぴゃくななじゅうはち'),
(4567890123456789, 'よんせんごひゃくろくじゅうななちょうはっせんきゅうひゃくいちおくにせんさんびゃくよんじゅうごまんろくせんななひゃくはちじゅうきゅう'),
(5678901234567890, 'ごせんろっぴゃくななじゅうはっちょうきゅうせんじゅうにおくさんぜんよんひゃくごじゅうろくまんななせんはっぴゃくきゅうじゅう'),
(6789012345678901, 'ろくせんななひゃくはちじゅうきゅうちょうひゃくにじゅうさんおくよんせんごひゃくろくじゅうななまんはっせんきゅうひゃくいち'),
(7890123456789012, 'ななせんはっぴゃくきゅうじゅっちょうせんにひゃくさんじゅうよんおくごせんろっぴゃくななじゅうはちまんきゅうせんじゅうに'),
(8901234567890123, 'はっせんきゅうひゃくいっちょうにせんさんびゃくよんじゅうごおくろくせんななひゃくはちじゅうきゅうまんひゃくにじゅうさん'),
(9012345678901234, 'きゅうせんじゅうにちょうさんぜんよんひゃくごじゅうろくおくななせんはっぴゃくきゅうじゅうまんせんにひゃくさんじゅうよん'),
( 123456789012345, 'ひゃくにじゅうさんちょうよんせんごひゃくろくじゅうななおくはっせんきゅうひゃくいちまんにせんさんびゃくよんじゅうご'),
( 0, 'ぜろ'),
( -1, 'まいなすいち'),
( 10000000000000, 'じゅっちょう')
)
SELECT args, CASE WHEN numerical_to_reading(args) = expected THEN 'Passed' ELSE 'Failed' END
FROM parameterized_tests
;
上の結果が全て Passed になっていれば OK です。
SQL での実装例
次に先ほど PL/pgSQL で実装した処理を SQL で実装しなおしていきます。
CREATE OR REPLACE FUNCTION numerical_to_reading(numerical_value bigint) RETURNS varchar AS $$
-- 数字の読み方を出力する。
WITH RECURSIVE
-- 数字の読み方を定義
general_numeral_readings(numeral, reading) AS (
VALUES
(0, ''),
(1, 'いち'), (2, 'に'), (3, 'さん'),
(4, 'よん'), (5, 'ご'), (6, 'ろく'),
(7, 'なな'), (8, 'はち'), (9, 'きゅう')
),
-- 1桁ごとの読み方を定義(一、十、百、千)
general_digit_readings_for_every_digit(digit, reading) AS (
VALUES
(1, ''), -- 一の位の数については呼称なし。
(2, 'じゅう'), (3, 'ひゃく'), (4, 'せん')
),
-- 4桁区切りの桁ごとの読み方を定義(万、億、兆、...)
general_digit_readings_for_every_four_digit(digit, reading) AS (
VALUES
(5, 'まん'), (9, 'おく'), (13, 'ちょう')
),
-- 例外的な数字の読み方
specific_numeral_readings_for_every_digit(digit, numeral, reading) AS (
VALUES
(2, 1, ''), (3, 1, ''), (4, 1, ''), -- 十、百、千(いっせん)の読み方を定義
(3, 6, 'ろっ'), -- 6百(ろっぴゃく)の読み方を定義
(3, 8, 'はっ'), (4, 8, 'はっ') -- 8百(はっせん)、8千(はっせん)の読み方を定義
),
-- 例外的な桁の読み方
specific_digit_readings_for_every_digit(digit, numeral, reading) AS (
VALUES
(2, 0, ''), (3, 0, ''), (4, 0, ''), -- 0の場合は十百千は読まない
(3, 3, 'びゃく'), (4, 3, 'ぜん'), -- 3百(さんびゃく)、3千(さんぜん)の読み方を定義
(3, 6, 'ぴゃく'), (3, 8, 'ぴゃく') -- 6百(ろっぴゃく)、8百(はっぴゃく)の読み方を定義
),
-- 例外的な数字の読み方(万億兆)
specific_numeral_readings_for_every_four_digit(digit, numeral, reading) AS (
VALUES
(13, 1, 'いっ'), (13, 8, 'はっ')
),
-- 引数に指定された数字を桁ごとに分割する。
digit_numeral_mapping(digit, numeral) AS (
SELECT
1,
substring(
abs(numerical_value)::varchar from char_length(abs(numerical_value)::varchar) for 1
)::integer
UNION ALL
SELECT
parts.digit + 1,
substring(
abs(numerical_value)::varchar from char_length(abs(numerical_value)::varchar) - (parts.digit) for 1
)::integer
FROM digit_numeral_mapping AS parts
WHERE parts.digit < char_length(abs(numerical_value)::varchar)
),
-- 他の桁の数字に依存する例外的な読み方を定義
other_digit_readings_for_every_digit(digit, reading) AS (
SELECT 14, 'じゅっ'
WHERE EXISTS (SELECT 'X' FROM digit_numeral_mapping WHERE digit = 13 AND numeral = 0)
),
-- 他の桁の数字に依存する例外的な読み方を定義(万億兆)
other_digit_readings_for_every_four_digit(digit, numeral, reading) AS (
SELECT 9, 0, ''
WHERE 3 = (SELECT COUNT(1) FROM digit_numeral_mapping WHERE digit IN (10, 11, 12) AND numeral = 0)
UNION ALL
SELECT 5, 0, ''
WHERE 3 = (SELECT COUNT(1) FROM digit_numeral_mapping WHERE digit IN (6, 7, 8) AND numeral = 0)
),
-- 桁ごとに数字の読み方を出力する。
readings_per_digit(digit, reading) AS (
SELECT
parts.digit,
coalesce(s_num_efd.reading, s_num_ed.reading, g_num.reading) -- 0~9までの数字
|| coalesce(o_dgt_ed.reading, s_dgt_ed.reading, g_dgt_ed.reading) -- 十百千の位
|| coalesce(o_dgt_efd.reading, g_dgt_efd.reading, '') -- 万、億の位
FROM digit_numeral_mapping AS parts
NATURAL JOIN general_numeral_readings AS g_num
JOIN general_digit_readings_for_every_digit AS g_dgt_ed
ON g_dgt_ed.digit % 4 = parts.digit % 4
LEFT JOIN general_digit_readings_for_every_four_digit AS g_dgt_efd
ON g_dgt_efd.digit = parts.digit
LEFT JOIN specific_numeral_readings_for_every_digit AS s_num_ed
ON s_num_ed.digit % 4 = parts.digit % 4
AND s_num_ed.numeral = parts.numeral
LEFT JOIN specific_digit_readings_for_every_digit AS s_dgt_ed
ON s_dgt_ed.digit % 4 = parts.digit % 4
AND s_dgt_ed.numeral = parts.numeral
LEFT JOIN specific_numeral_readings_for_every_four_digit AS s_num_efd
ON s_num_efd.digit = parts.digit
AND s_num_efd.numeral = parts.numeral
LEFT JOIN other_digit_readings_for_every_digit AS o_dgt_ed
ON o_dgt_ed.digit = parts.digit
LEFT JOIN other_digit_readings_for_every_four_digit AS o_dgt_efd
ON o_dgt_efd.digit = parts.digit
AND o_dgt_efd.numeral = parts.numeral
ORDER BY parts.digit DESC
)
-- 数字の読み方を出力
SELECT
CASE
WHEN numerical_value = 0 THEN 'ぜろ'
WHEN numerical_value < 0 THEN 'まいなす'
ELSE ''
END || array_to_string(array_agg(reading ORDER BY digit DESC), '') AS reading
FROM readings_per_digit
;
$$ LANGUAGE SQL
IMMUTABLE
RETURNS NULL ON NULL INPUT;
PL/pgSQL の時に確認した方法と同じ方法で実装に問題がないか確認できます。
書き方は前回のを参照してください。
さいごに
過去にも SQL で今回のようなアルゴリズム問題を解いた投稿をしたので、リンクだけ貼ります。