小数点を含む数値を英訳するプログラムの実装

投稿者: | 2021年4月9日

就活や研究が嫌すぎて、どっかで見た問題を実装していました。ただ解くだけでは面白くないので、jsonファイルを読み込むようにしました。

1.問題

入力された数値を英訳して出力せよ。数値は小数を含む場合もある。数値の整数部の最大値は999兆である。数値の途中にカンマは入らない。

例:

235 → two hundred thirty five

123.321 → one hundred twenty three point three two one

1000000000000 → one trillion

2.翻訳ルール

数値を英語で読む時のルールは次の通り。

  • 0~100は小学校で習ったとおり
  • 3桁ごとに区切る
  • 区切りの次の桁からthousandやmillionをつける
  • 区切り内は普通に読む
  • 小数点はpointを読んで、一つずつ数字を読み上げる

小数点以下は3.14ならthree point fourteenと呼ぶこともありますが、実装が面倒くさくなるので1文字ずつ読む方を採用します。

3.C++でjsonファイルを読み込む

nlohmann-jsonライブラリを用いました。

必要なものはヘッダーファイルのjson.hppだけですので、cloneしてそのファイルだけ取り出しました。

使い方はREADMEを読むか、英語が嫌ならC++のjsonライブラリ決定版 nlohmnn-jsonがいいと思います。

4.コード

メイン

用いたjsonファイル

5.説明

5-1.数値を整数部と小数部に分割する

pair<string, string> splite_number(string s) {
  string integral_part, decimal_part;
  //小数点があるか判定し、整数部と小数部に分割
  bool decimal_flag = false;
  for (char c : s) {
    if (!decimal_flag) {
      if (c == &#039;.&#039;) {
        decimal_flag = true;
        continue;
      } else {
        integral_part += c;
      }
    } else {
      decimal_part += c;
    }
  }
  //小数部がないならば、小数部の文字列をnothingにする
  if (!decimal_flag) decimal_part = "nothing";
  return {integral_part, decimal_part};
}

範囲forを使って小数点が出てくるまで整数部にpush_backし、小数点が出たら小数部にpush_backして分割しています。

小数点以下がなかったことを示すために、ない場合はnothingという文字列を小数部に代入します。

この関数の戻り値の型はpairです。理由は、最近C++17で実装された構造化束縛というものを実装に使ってみたかったからです。

  //文字列を整数部と小数部に分割
  auto [integral_part, decimal_part] = splite_number(s);

このように、多値の戻り値を分解して各要素を取り出せます。

AtCoderの過去問の解説で初めて見て、1時間ぐらいググりました。わかってしまえばこれで実装したほうが範囲forなどでは読みやすいと思いました。

5-2.jsonファイルの読み込み方

  // jsonファイルを開く
  ifstream i("number.json");
  json j;
  i >> j;

これはREADMEにあるコードそのままです。READMEのページを"input"とページ内検索すると最初にヒットします。

5-3.処理に必要な値(整数部)

  //整数部の長さを取得
  int l = s.size();

  // 3ずつに区切ったときのブロックの数
  int block = l / 3 + (l % 3 == 0 ? 0 : 1);

  //先頭のブロックの文字数
  int head;
  if (l % 3 == 0)
    head = 3;
  else if (l % 3 == 2)
    head = 2;
  else if (l % 3 == 1)
    head = 1;

3つずつ区切った時に何個に区切れるかという値と、先頭の区切り内の数字の数を事前に計算しています。

区切りの数は、長さが3の倍数の時以外は1を足さないといけないため、三項演算子を用いて処理をしています。

三項演算子を読むのは苦手ですけど、これぐらいの処理なら「別にいいかー」って思うようになってきました。

これを書いているときに、三項演算子を使わなくても次のように書けばシンプルに実装できることに気が付きました。

int block = (l + 2) / 3;

l=1のときに1になるように調整すれば、他のときもうまいこと計算できます。

先頭の区切り内の桁数はうまい数式が思い浮かばなかったので愚直に場合分けしています。

3で割ったあまりが0の時3、2の時2、1の時1になるような数式って何かありますでしょうか?

5-4.英語に変換(整数部)

  //ブロックごとに処理
  for (int i = 0; i < block; i++) {
    //ブロックの切り出し
    string tmp;
    if (i == 0) {  //先頭ブロックの処理
      tmp = s.substr(0, head);
    } else {  //それ以外のブロックの処理
      tmp = s.substr(head * i, 3);
    }

    //ブロック内が000なら読まない
    if (tmp == "000") continue;

    // jsonファイルを参照する時のkey
    string moji;

    //英語に変換
    if (i == 0 && head != 3) {  //先頭ブロックが3文字でないとき
      if (head == 2) {          //先頭ブロックが2文字
        //十の位
        moji = tmp[0];
        moji += &#039;0&#039;;
        ans += j[moji].get<string>();

        //一の位
        moji = tmp[1];
        if (tmp[1] != &#039;0&#039;) {
          //前に単語があるので空白を入れる
          ans += " ";

          ans += j[moji].get<string>();
        }
      } else {  //先頭ブロックが1文字
        moji = tmp[0];
        ans += j[moji].get<string>();
      }
    } else {  //先頭ブロックが3文字のときとその他ブロックの処理
      //百の位
      moji = tmp[0];
      ans += j[moji].get<string>();
      ans += " ";
      ans += "hundred ";

      //十の位
      moji = tmp[1];
      moji += &#039;0&#039;;
      ans += j[moji].get<string>();
      ans += " ";

      //一の位
      moji = tmp[2];
      if (tmp[2] != &#039;0&#039;) ans += j[moji].get<string>();
    }

    //単語の間に空白を入れる
    ans += " ";

    // thousand,million,billion,trillionなどを付与する
    int scale = block - i - 1;
    if (scale == 4)
      ans += "trillion ";
    else if (scale == 3)
      ans += "billion ";
    else if (scale == 2)
      ans += "million ";
    else if (scale == 1)
      ans += "thousand ";
  }
  return ans;
}

先頭の区切りでなければ区切り内は必ず3桁である(もしくは整数部が先頭の区切りしか無い)ため、全ての区切りは3桁あることを前提として翻訳します。しかし、先頭の区切りだけは一桁や二桁の場合があるので分けて処理します。

string型の変数にpush_backする形で翻訳するため、入力された数値の最高位の方から順に翻訳していきます。この時、先頭の区切りが3桁でなければ別の処理をするようにしています。その他の場合は全て区切り内が三桁であるときの処理を行います。

ここで、jsonファイルのプロパティに対する値を取り出しています。

取り出し方はREADMEに書かれていますが、注意点としては、jsonファイルのkeyをstring型にしているなら引数の型は必ずstring型ということです。その他は、まるでオブジェクトや配列のように扱えます。めっちゃ便利。

また、そのまま取り出す場合と、データ型に変換して取り出す場合があります。

そのまま取り出す場合は、オブジェクトや配列を扱うように次の方法で動きます。

const auto tmp = j["0"]
// -> "one"が出力される

ダブルクオーテーションマークまで一緒に取り出されることに注意してください。

データ型に変換する場合は次のようにします。

const auto tmp = j["0"].get<string>();
// -> oneが出力される

プロパティに対する値だけ取り出すことができます。ただし、その値に適した型以外を選択するとエラーになります。今回の場合、値は文字列型であるのに整数型で取り出そうとするとエラーが出ます。

最後に、区切りごとにthousandやmillionをつければ整数部の翻訳は終わりです。

5-5.英語に翻訳(小数部)

string decimal_translate(string s) {
  // jsonファイルを開く
  ifstream i("number.json");
  json j;
  i >> j;

  //返す文字列
  string ans;

  // 1文字ずつ変換する
  for (char c : s) {
    // keyがstring型なので変換する
    string tmp = {c};

    //英訳する
    ans += j[tmp];

    //単語の間に空白を入れる
    ans += " ";
  }
  //最後の空白はいらないので削除する
  ans.pop_back();

  return ans;
}

小数部の変換は整数部と比較してとても簡単です。

1文字ずつ取り出して英訳をすればいいので、範囲forを用いて取り出し処理しています。

strng型に変換するために、初期化子リストを用いています。

string tmp = {c};

vectorなどユーザ定義型のオブジェクトに対して用いますが、これに名前があったことに驚いています。そしてC++11からの機能であることに更に驚きました。

全ての単語の間ごとに空白を入れるようにしていますが、末尾の空白はいらないのでpop_backで削除しています。

5-6.整数部と小数部の結合

string translate(string s) {
  string answer;
  //文字列を整数部と小数部に分割
  auto [integral_part, decimal_part] = splite_number(s);
  //整数部を英語読みにする
  answer = integral_translate(integral_part);
  //小数部を英語読みする
  if (decimal_part != "nothing") {
    //小数部がある場合、区切りにpointが入る
    answer += "point ";
    //小数点以下の翻訳
    answer += decimal_translate(decimal_part);
  }
  return answer;
}

処理のメイン部です。2つの文字列を英訳して、戻り値同士を結合しています。

小数部がある場合は"point"という文字列を挟んで結合しています。

5-6.メイン関数

int main() {
  string s;
  cin >> s;
  cout << translate(s) << endl;
  return 0;
}

入力して、translate関数を呼び出すだけです。

6.おわりに

このコードですが、デバッガを用いてデバッグすると必ずjsonファイルの文法がおかしいというエラーが出てしまいます。jsonファイルは構文チェッカーに通したりして正しいことを確認済みなのですが、やはりダメです。今回用いたライブラリはUTF-8以外サポートしないというものなのでUTF-8で保存されていることを確認したファイルですら同じエラーが出ます。

しかし、gccでコンパイルして実行ファイルを実行すると正常に動作します。

デバッグ環境は次のとおりです。

デバッガ : gdb(GNU gdb (Ubuntu 8.2-0ubuntu1~18.04) 8.2)
args    :  -std=gnu++17
           -Wall
           -Wextra
           -Wshadow
           -Wconversion
           -fsanitize=undefined
           -ggdb
           ${file}
           -o
           ${fileDirname}/${fileBasenameNoExtension}

gcc環境は以下の通り。

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion=&#039;Ubuntu 9.3.0-11ubuntu0~18.04.1&#039; --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.3.0 (Ubuntu 9.3.0-11ubuntu0~18.04.1)

とりあえず動くものができたのでヨシ。

ところでこれを時間内で実装するの大変。コーディングテストの場合外部ライブラリを使えないので、数値と英単語の対応関係をjsonファイルに書き出せず、配列を用いて処理をしないといけないので可読性が落ちます。

Pythonとかなら楽なんでしょうけど、C言語メインだときついかなあと思いました。

7.参考文献

nlohmann/json

C++のjsonライブラリ決定版 nlohmnn-json

構造化束縛

AtCoder Beginner Contest 190 C - Bowls ans Dishes 解説

C++の記号一覧 (List of C++ symbols)←すごく見やすくてわかりやすい

初期化子リスト

英語で数字の単位が読み方まで一覧でわかる!音声付きまとめ

範囲for文

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください