多言語化
一般的なAndroidアプリで多言語化するとき,注意するのはstrings.xmlと日付の表示方法ぐらいだけど,通貨を扱うような場合には注意が必要.
1ドルが何円になる?っていう為替の話ではなくて,
プログラム的に1という数字が通貨の何を表しているのか?という話.
通貨のフォーマット
通貨をLocaleに合わせて表示させるにはNumberFormatを使用する.
例えば1000という数字をLocaleに合わせて通貨表示させるには下記のようにする.
public static void main(String[] args) {
int value = 1234;
printValue(value, Locale.JAPAN);
printValue(value, Locale.US);
printValue(value, Locale.UK);
printValue(value, Locale.FRANCE);
}
public static void printValue(int value, Locale locale){
NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
System.out.println(locale+" "+nf.format(value));
}
Localeを指定しなければデフォルトのLocaleが使用される.
Androidで試すのが手っ取り早くて,設定のLanguageから切り替えができる.
で,実行結果は下記の通り.
ja_JP ¥1,234
en_US $1,234.00
en_GB £1,234.00
fr_FR 1 234,00 €
通貨記号とカンマがちゃんと入って見やすくなったねー・・・って小数点???
数値の最小単位と通貨の最小単位が一致しているのは日本ぐらい
1という数値が1円という値を示す分かり易いのは実は日本ぐらいで,
海外だと補助通貨単位(セントとかペニーとか)があるため,1は通貨の最小単位に換算するとだいたいの国では100になる.
マジメに実装しようとするとこれが意外とめんどくさい.
浮動小数点を使う
NumberFormatは浮動小数点にも対応しているので,当然下記のような書き方はできる.
public static void main(String[] args) {
double value = 12.34;
printValue(value, Locale.JAPAN);
printValue(value, Locale.US);
printValue(value, Locale.UK);
printValue(value, Locale.FRANCE);
}
public static void printValue(double value, Locale locale){
NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
System.out.println(locale+" "+nf.format(value));
}
実行結果は下記の通り.
ja_JP ¥12
en_US $12.34
en_GB £12.34
fr_FR 12.34 €
まぁ,そりゃそうなる.とはいえ,浮動小数点を使うということは
- 足し算していくと誤差が出るよね?
- 日本の場合は小数点以下の入力を制限しないといけないからそのたびに切り替えるの?
- 小数点2位までを許可する?
- 最小単位が100分の1以外の場合はどうする?
ということで面倒くさい.
Androidアプリの場合,日本なら整数値のみ許可,ドル圏なら小数点2位まで許可,といった感じでLocaleに応じて入力値のチェック方法が変わってしまう.
ときどき見かける海外サイトで日本円なのに小数点以下が表示されるのはこの辺を端折ってるんだと思う.
値は最小単位に限定する
例えば時間はミリ秒を最小単位で保存する,ということを決めてDBを設計するように,
通貨も最小単位の入力しか受け付けないように(少なくともDBは)設計するのが良い.
つまり,valueに格納する値は常にそのLocaleの最小単位の通貨と定義する.
Android/JavaにはCurrency#getDefaultFractionDigits()
というメソッドが用意されているので,Localeに合わせて最小単位の桁数は取得できる.これを使う.
public static void main(String[] args) {
int value = 1234;
printValue(value, Locale.JAPAN);
printValue(value, Locale.US);
printValue(value, Locale.UK);
printValue(value, Locale.FRANCE);
}
public static void printValue(int value, Locale locale){
NumberFormat nf = NumberFormat.getCurrencyInstance(locale);
Currency c = Currency.getInstance(locale);
double d = (double) value / Math.pow(10, c.getDefaultFractionDigits());
System.out.println(locale+" "+nf.format(d));
}
入力された値は最小単位のint値だけど,表示するときに桁数をずらしてdoubleにしてからformatする.とても単純.
実行結果は
ja_JP ¥1,234
en_US $12.34
en_GB £12.34
fr_FR 12,34 €
ということで表示は上手く行っていると思う.
ユーザーが小数を入力してきたら,FractionDigitsの数だけ桁をずらして後は丸めてintにしてしまえばいい.
これで余計なことは考えなくて済むと思う.
ジンバブエドルみたいにintの限界超えた場合はどうするの?
しらん.longでも使ってろ.
追記
ふとジンバブエドルってlongで扱えるのかな?って思ったので調べて見たけど
Wikipediaの記事によると,インフレ率は$6.5×10^{108}$とか言ってるので全然無理.
あと,intってせいぜい21億ぐらいしか扱えないので,ちょっとした企業の会計とかちょっとした土地持ってる富裕層の方は全然足りない.
単に数値として使うならBigIntegerでも使えばいいんだけど,Locale使ってNumberFormatするにはどうにか変換しないとダメ.
まぁ,そもそもそういうちゃんとした会計で使うなら通貨毎に処理を分けたりDBも全然別だったりするだろうから関係ないだろうけど・・・.
おまけ
Androidで作るときはNumberFormatのオブジェクトをstaticにしてはいけない.
Activityを実行しているときにバックグラウンドで設定メニューからLocaleが変わる可能性がある.
当たり前だけどstaticオブジェクトを作ると終了までそれが使われるので,アプリに反映されなくなってしまう.
なので,UIに絡む場所であれば必ず毎回getInstanceするように.(これでしばらくハマってた)
おまけの追記
コメントでご指摘を貰ったように,そもそもJavaDocに「NumberFormatはスレッドセーフじゃないからスレッド毎にインスタンス作成してね」って書いてあるのでstaticに書いてはダメです.
ソース追いかけてみたら内部的な変数を使ってるっぽいので確かに危なそうです.
ついでにJavaでも実行中にLocaleが変わることが皆無ではないので毎回getInstanceするべきです.
ちなみに上記のハマってた事例は
「Androidアプリ作ったでー」
→しばらくしてから「多言語化する必要があるぞー」
→動作確認のためにアプリ実行
→「よしちゃんと動いとるな」
→戻るで終了
→「設定から言語変えてみるでー」
→「アイコンクリックで起動・・・あれ?変わってないぞ?」
→「Android Studioからリビルドかけたら上手く行ったしまぁええか」
→「あれから時々変わってないことあるぞ・・・なんやこれ・・・」
→「よく見たらstaticやんけこいつ!」
っていう感じでした.