まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:locale

今日、7月13日は「日本標準時制定記念日」です。


というわけで、この日にちなんだネタを1つ紹介します。

ウェブアプリケーションやウェブサービスを作っていると「数値のカンマ表示」を求められることがあります。数値が4桁以上の場合に "1234" を "1,234" と表示する、というやつです。"1234567890" だと "1,234,567,890" になります。画面上で数値を表示する場合はこのようなカンマを必要に応じて加えた上で表示してほしいという面倒なもの。

静的な HTML でページを作る場合は当然ハードコーディングする形で "1234" を "1,234" に書き換える必要がありますが、アプリケーションとしてページを作っていて、その中でアプリケーション的には変数となるような情報をカンマ表示する、という場合はプログラミングコードの中で自動処理をすることもできるので、それほど神経質にならなくても実現できたりします。

特に Node.js を使っている "大抵の" 場合であれば、JavaScript の標準機能を使って実現することができます。それが Number.toLocaleString() です。この関数、正確には「数値をカンマ表示にする関数」ではなく「数値をロケールに合わせた表示に変換する関数」という名前の通りの機能を持っているのですが、日本であれ欧米であれ、ほとんどのロケールにおいて「数値をロケールに合わせて表示」すると3桁ずつのカンマ表示になります。つまり事実上、この関数を通すことで数値をカンマ表記に切り替えることができるようになります。めでたし、めでたし・・・:
var num = 12345;
var str = num.toLocaleString();   // "12,345"


・・・が、今回のブログエントリはこれが上手くいかない例外ケースに関する情報です。それも何故かここで一見すると無関係なはずの docker や k8s といったコンテナ環境が関わってくる話だったりします。

コンテナ環境においては、コンテナイメージを小さくすることが求められているように感じています。その背景には「小さい方が速い」ことや「無料で利用できるコンテナサイズ枠」の問題などが関わっているように感じています。コンテナイメージのサイズを小さくする上で Alpine 製の Linux イメージが多く使われているようです。例えば Node.js アプリケーションのコンテナイメージを作ろうとすると、以下のような Dockerfile ファイルを用意してビルドします(1行目で alpine 製の Node.js v14 イメージを使うよう指定しています):
FROM node:14-alpine
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
COPY package*.json ./
COPY . .
EXPOSE 8080
CMD ["node", "app.js"]

この alpine イメージはたしかに標準イメージと比較して(余計なものが含まれていないため)軽量で、イメージサイズを小さくすることができます。コンテナイメージでディスク容量を圧迫するような使い方をしている人(自分)にとっては少しでも小さいイメージで使えるだけでも嬉しいし、無料の非公開レジストリを使おうとすると容量制限が厳しいので、こちらも少しでもイメージを小さくしたいという要望が多くあると思いますが、そういった需要を解決する便利なベースイメージだと認識しています。

ところが、この「余計なものが含まれていない」ことが問題になるケースもあります。その1つが「ロケール情報」です。alpine イメージにはロケールに関する情報が含まれていません。それもあっての軽量化なのですが、ロケールが含まれていないということは上述の Number.toLocaleString() 関数も使えないことを意味しています。ローカルの PC で動作確認している時は(ロケールが有効になって)問題と認識できない部分が、コンテナ化されて動かすと動かないということが起こり得るわけです。かなり気をつけていないと気付けない点であることも含めて厄介な制約だと感じます。

ちなみに、ロケールが使えない環境下で数値をカンマ表記するにはどうすればよいか・・・ ここは「困った時の正規表現」をおすすめします。例えばこんな感じで関数化して使う、とか:
function myLocaleString( num ){
  return String(num).replace( /(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
}

var num = 12345;
var str = myLocaleString( num );  // "12,345"

この部分だけではないのですが、コンテナ環境だけでロケール依存の実装がうまく動かない場合は各種 alpine イメージを使っているかどうかの確認にお気をつけください。そしてこの情報が参考になれば幸いです。


JavaScript 製オープンソース Web フレームワークである AngularJS は、リッチなフロントエンドを簡単に作ることができて便利です。実は業務でも使っています(個人的にはまあ色々言いたいこともあるけどw)。 一方で、AngularJS やマテリアルデザインである Angular Material は特に日本語での情報があまり多くなく、標準以外の機能を使おうとすると色々苦労することがあります。その辺りを補足する目的もあって、調べたことのメモを公開します。


AngularJS を使うことで普通に作っても見栄えがよくなりますが、サブモジュールである Datepicker を使うと、日付データの入力に便利な UI を簡単に配置できます(↓こんなやつです):
2018040301


この Datepicker を使う方法も簡単で、JavaScript コード側で @angular/material/datepicker をインポートしておいて、
import {MatDatepickerModule} from '@angular/material/datepicker';

その上で以下のような HTML を記述するだけです:
  :
  :

<mat-form-field>
  <input matInput [matDatepicker]="picker" placeholder="Choose a date">
  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker></mat-datepicker>
</mat-form-field>

  :
  :

すると上記画像のように <input> フィールドそのものに CSS が適用され、カレンダーアイコンをクリックして日付選択ができるようになる、というスグレモノです。モバイルブラウザのタッチイベントにも対応していたりするなど、機能的にも優秀です。日付選択は HTML5 の type="date" を使う方法もありますが、まだ主要ブラウザの実装もまちまちで、対応されていてもちと使いにくさを感じることもあるため、現時点での代用品としてはかなりの需要があると思っています。


一方で、この Datepicker にも問題点はあります。標準機能をそのまま使うと、選択した日付は日本ではあまり馴染みのない M/d/yyyy フォーマットで画面に表示されることです:
2018040302


この表示部分だけでもなんとか日本式の yyyy/M/d フォーマットにできないだろうか・・と調べた結果が以下の方法です。いくつかの対応手段がありそうですが、簡単なのは「日本ロケール(ja-JP)を指定する」以下のやり方でした。

HTML はそのままで、JavaScript を以下のように書き換えます:
//. 赤の2行を追加インポート
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MAT_MOMENT_DATE_FORMATS, MomentDateAdapter} from '@angular/material-moment-adapter';
import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core';

  :
  :

@Component({
  selector: 'datepicker-locale-example',
  templateUrl: 'datepicker-locale-example.html',
  styleUrls: ['datepicker-locale-example.css'],
  providers: [
    //. 'ja-JP' ロケールを指定
    {provide: MAT_DATE_LOCALE, useValue: 'ja-JP'},

    //. MomentDateAdapter と MAT_MOMENT_DATE_FORMATS をインポート
    {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]},
    {provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS},
  ],
})

  :
  :

export class DatepickerLocaleExample {
  //. クラスのコンストラクタに DateAdapter を渡す
  constructor(private adapter: DateAdapter) {}
}

この変更を加えて上で同じ HTML を参照すると、日付は日本ロケールで表示されるようになります:
2018040303



めでたしめでたし。


(参考)
https://material.angular.io/components/datepicker/overview

このページのトップヘ