Karakuri.com

Fintechではたらくアプリケーションエンジニアの技術録

言語設定によって小数点や桁区切り文字が違って海外で不具合が起きる話

前職は世界中で開発したソフトウェアを販売しており、同じWindowsでも日英中韓台独仏伊と8か国くらいの言語版でテストしてたりしました。するとドイツ版で変な挙動をするという報告を受けました。どうも数値がおかしくなるんだそうです。それで当時調べてみました。

値が1000分の1になったりクラッシュしたりする

問題の挙動

前職で開発していたソフトウェアは産業機械関連なので設定値がたくさんあります。例えば距離でメートルの設定はたくさん出てきますし、ミリメートルだとそれなりの桁数になるので、桁区切り文字の「,」を自動で入力するようにしていました。

1,000 mm

こんな感じで保存されます。しかし保存して再度設定画面を開くとドイツ版だと下記のようになってしまうとのこと。

1 mm

なんということでしょう。何が起こっているんだ。日本語や英語では起きないのに…。

原因は国や地域の桁区切り文字設定

桁区切り文字が国や地域によって違う

これが衝撃的だったのですが、実は桁区切り文字が国や地域によって違います。国際規格では小数点にピリオド「.」を使うことになっているようですが、少なくともWindowsは汎用的なOSなので国や地域の慣習を優先して使います。例えばドイツの場合は桁区切り文字がピリオド「.」で小数点が「,」で日本と逆です。
coliss.com
中には桁区切り文字がスペースな国もありますが、Windowsの標準設定はどうなっているんですかね。考えるだけでも恐ろしいです。

コードでは何が起きていたのか

桁区切り文字を追加するとき

例えばWindowsの場合は,Net Frameworkを使って数値に桁区切り文字を付けてString型にすることができます。

string.Format("{0:N0}", value);

これがWindowsの設定によって挙動が変わります。例えば「1000」を渡したとき桁区切り文字が「,」の場合は「1,000」ですし、「.」のときは「1.000」になります。

桁区切り文字を除去するとき

先ほど桁区切り文字を追加した文字列を再び数値に変換してDBなどに保存する場合を考えます。桁区切り文字を含む文字列を数値に素直に変換するメソッドが.Net Frameworkにはないので、簡易的に下記のように書くことが多いのではないかと思います。

int.Parse(valueString.Replace(",", ""));

ここでvalueStringが「1.000」だとクラッシュします。実際はint.TryParseを使ってエラーハンドリングで0とか無害な数値になるかもしれませんが、いずれにせよ正常には動きませんね。ちなみに最初に「1,000mm」が「1mm」になった理由はドイツ版で問題が起きた環境を開発陣の日本版環境で検証したからとかそんな感じだった気がします。もう環境が混ざるとカオスです。

どのように対策すれば良いのか

運用で回避してしまう

前職は割と規模が大きくソフトウェアの改修コストが大変なことになってしまうので、各国でも桁区切り文字に「,」を使うように運用で回避してもらうことにしました。Windowsの設定で桁区切り文字を変えることができます。
www.wannko.net

ちゃんと対応するには?

やってないので調べてないのですが、例えばここら辺とか使って何とかできそうな気もします。
docs.microsoft.com
これってAndroidやiOSだとどうなんでしょうね。

Ruby on RailsでTimeとDateTimeとPostgreSQLのTimestampの精度の差でRspecが失敗した話

言わずもがな保守運用をやってるレガシーシステムで時刻で使っている型がバラバラでして、ちょっと不具合修正をしたらジェンガのように音を立ててRspecが失敗するようになってしまいました。原因を調べていくと、DBで使用しているPostgreSQLのTimestamp型まで含めた精度の話に帰着したのでまとめておきます。

なにが起きたのか

先にも書いたのですが、意図が分からない to_datetime がソースコードに散らばっていて、ここら辺の不具合を修正したときに to_datetime するかどうかでテスト結果が変わるという謎の状態に陥ってしまったんですね。to_datetime する意図が分からないので、とりあえず全部消してみたら更にテスト失敗数が増えてどんどんシステムが壊れていきました。

expected: 2019-12-07 23:59:59.999999999 +0900
     got: 2019-12-07 23:59:59.999999000 +0900

こんな失敗が頻発してたんですね…。どういうこと…。

それぞれの型の精度を調べてみる

結果から察するとナノ秒の情報が欠落しているのが原因みたいです。それでRspecのコードを調べてみると、

  • expected: Time型
  • got: PostgreSQLのTimestamp型(ActiveRecordで取ってきた)

ということが分かりました。

Time型

Time型はナノ秒までの精度があるみたいです。文字列変換するとデフォルトでナノ秒まで含んだ文字列になります。
class Time (Ruby 2.6.0)

PostgreSQLのTimestamp型

Timestamp型はマイクロ秒までの精度があるようです。
https://www.postgresql.jp/document/9.4/html/datatype-datetime.html

ということで問題の原因はこの精度の差でナノ秒の情報が欠落していたからでした。

なぜ今までテストが通っていたのか?

ここで1つ疑問が残ります。なぜ今までテストが通っていたのか?

DateTime型の精度は?

先に書きましたが、意図が分からない to_datetime が散らばっていました。もしかしてこいつが原因かもしれません。DateTime型の精度を調べてみると、、、
docs.ruby-lang.org
精度が記載されていません( ノД`)シクシク…
ただ、文字列にして表示してみるとナノ秒の精度があるっぽいです。もしかすると今まではActiveRecordで取ってきても to_datetime されてナノ秒を復旧させていたのかな…。いずれにせよ to_datetime したらテスト通るからという理由で to_datetime していたんじゃないか疑惑が浮上しました。レガシーシステムは闇が深いですね。そもそも end_of_day で保持して比較している時点で…以下略

PostgreSQLのintervalを使えばselectのtimestampの年を加算できた

DBに保存されている日時から有効期限などを求めるような場合、SQLで計算後の日時でまとめてしまいたいケースに遭遇しました。調べてみたらPostgreSQLなら簡単にできるらしい。PostgreSQLって日時操作が強いんですね。

select created_at + interval '1 year' from user

Ruby on RailsとPostgreSQLとHerokuのタイムゾーンの設定と関係で混乱したので調べました

会社のWebサービスがRailsでPostgreSQLを使っているのですが、DBで保存されているタイムスタンプのタイムゾーンがよく分からず小一時間調べて複雑だったのでメモ。

それぞれのタイムゾーン設定状況

Ruby on Railsのタイムゾーン設定

config.active_record.default_timezone = :local
config.time_zone = 'Tokyo'

application.rbのconfig.time_zoneはRuby on Railsのタイムゾーンで理解しやすいのですが、config.active_record.default_timezoneの認識を間違っていてハマってしまいました。config.active_record.default_timezoneはActiveRecordがDBの読み書きに使用するタイムゾーンと理解すれば良いようです。このため、DBにタイムゾーンを設定していてもRails側はconfig.active_record.default_timezoneの設定したタイムゾーンで時間を取り扱います。そしてここを:localにしたとき、それはDBの設定を使うという意味ではなく、サーバーの設定を使うという意味です。Herokuを使っているならHeroku、DockerならDockerのタイムゾーンになるわけですね。

PostgreSQLのタイムゾーン設定

show timezone;

Postgreにログインして上記のコマンドで設定しているタイムゾーンを確認できます。UTCでした。

Herokuのタイムゾーン設定

$ heroku run bash
$ date

Herokuにログインしてbashでタイムゾーンを確認できます。JSTでした。

ActiveRecordのタイムゾーンは何になるのか

というわけで、まとめると設定は下記の通りになります。

  • Ruby on Rails JST
  • ActiveRecord Herokuに準拠
  • PostgreSQL UTC
  • Heroku JST

つまり、WebサービスはJSTで動いているということになります。ActiveRecordはJSTで時間を読み書きし、Ruby on RailsもJSTで取り扱います。しかしPostgreSQLは時刻をUTCで保存していると思っています。これは気持ち悪いですね…。なんでこんな設定にしてしまっているんだろう…。

Swiftで暗号ライブラリCommonCryptoを使ってAESや3DESなど導入する

WindowsやAndroidは暗号ライブラリが豊富にあるので困らないのですが、iOSのSwiftだと暗号ライブラリが少なくとても大変です。有志が様々なライブラリを作ってはいるのですが、例えば3DESだと対応しているライブラリは皆無です。結局iOSに標準で入っているCommonCryptoというC言語のライブラリを使うのが最も安全という話になります。今回はCommonCryptoをSwiftで使うのに苦労したので、その使用方法を書き残しておきます。

CommonCryptoをSwiftで使う方法

ググると様々な方法が出てくるのですが、Xcode9では失敗しました。結論から言うと、まずアプリディレクトリ(TestAppというアプリならTestAppディレクトリ)の中にCommonCryptoというディレクトリを作成します。そしてその中にmodule.mapというファイルを作って、中に下記のように書いておきます。

module CommonCrypto {
    header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/CommonCrypto/CommonCrypto.h"
    export *
}

どうもこのheaderで指定する場所がXcodeのバージョンによって違うみたい?そんな勝手に変えられたら分からないですよねえ…。だからググっても動かないわけです。ちなみにXcode10ではどうなってるか調べてないです。すみません。

CommonCryptoをSwiftから呼び出す方法

あとはimportすれば呼び出せるはずです。

import CommonCrypto

辛いですねえ。

テスト仕様書の書き方とフォーマットが分からない人のためにテンプレートを作成しました

テスト仕様書を書いてくれと言われてもそのフォーマットは多種多様なものがあります。IPAなどの機関がフォーマットを定義していることもないため、新卒の新人でも渡されたテスト仕様書のフォーマットを見て「こんなフォーマットで良いのか…?」と戸惑う人もいるのではないでしょうか(実際僕がそうでした)。今回は試行錯誤して独自にテスト仕様書のフォーマットを固めてきたので、テスト仕様書の書き方とフォーマットについて公開したいと思います。

  • テスト仕様書とは
    • テスト内容によってフォーマットや書き方は多種多様
    • シナリオテスト仕様書とは
  • テスト仕様書の書き方
    • テスト仕様書で抑えるべきポイント
    • 誰がいつ、何をどういう環境でテストしたかが分かること
    • 誰がテストを実施しても同じ結果になること
      • A. ダメな操作手順
      • B. 正しい操作手順
    • 誰がテストを実施しても合格か不合格が判断できること
      • A. ダメな挙動定義
      • B. 正しい挙動定義
  • テスト仕様書のフォーマットとは
    • テスト仕様書のテンプレートを作りました
    • テスト仕様書にはタイトルが必要
    • 各項目の説明
    • テスト結果は少なくとも3つのステータスが欲しい
    • Predictには画像も積極的に貼りたい

テスト仕様書とは

テスト内容によってフォーマットや書き方は多種多様

国内のテスト仕様書の多くはExcelで書かれているのではないかと思います(Googleスプレッドシートを含めて)。そのExcelファイルでもテストの内容によって書き方は変わってきます。

  • 単体テスト
  • シナリオテスト
  • 回帰テスト(レグレッションテスト)

今回はシナリオテストに焦点を当てます。ちなみに単体テストはユニットテストとして開発者(プログラマー)が自動化して品質を担保している必要があるもので、本来は仕様書は不要なはずです。回帰テストも回帰テストの仕様書があるケースや、今まで発生した不具合を再度チェックするなど会社や組織によってやり方は様々でフォーマットや下記型定義は難しいです。

続きを読む

Swiftで一部の画面の回転禁止を導入したらiOS9はsupportedInterfaceOrientations was invoked recursivelyと例外が飛んでクラッシュする

iOSアプリ開発で、デザインの問題で一部の画面だけ回転を禁止にする必要が生じました。この一部の画面だけ回転を禁止するというのはUX的に問題があるのはそうなのですが、同時にAppleとしても推奨していないことのような気がします。案の定、iOS9でクラッシュするという不具合を作ってしまったので、今回はその原因と対策についてまとめます。

  • 一部の画面だけ回転禁止にする方法
    • shouldAutorotateとsupportedInterfaceOrientationsをoverrideする
  • iOS9でUIAlertControllerがクラッシュする
    • supportedInterfaceOrientations was invoked recursivelyと例外が飛ぶ
    • iOS9に無限ループに陥るバグがあるらしい
  • 問題の解決方法
    • UIAlertControllerを拡張して解決

一部の画面だけ回転禁止にする方法

shouldAutorotateとsupportedInterfaceOrientationsをoverrideする

画面回転時に呼ばれるshouldAutorotateとsupportedInterfaceOrientationsをオーバーライドして回転をキャンセルすることができます。
qiita.com
qiita.com

続きを読む

VirtualBoxのUbuntu(ゲストOS)で起動したRails ServerにWindows(ホストOS)やLAN内のスマホでアクセスする方法

VirtualBoxのUbuntu上でRuby on Rails開発環境を構築したのは良いのですが、Rails ServerにホストOSのWindowsやスマホからアクセスするのに苦戦してしまったので忘れないうちにメモ書きしておきます。

VirtualBoxのネットワーク設定を変更する

ネットワークアダプターのポートフォワーディングを追加する

f:id:hazakurakeita:20180721183917p:plain
VirtualBoxのネットワーク設定を開きます。
f:id:hazakurakeita:20180721183953p:plain
ポートフォワーディングをクリックします。
f:id:hazakurakeita:20180721184034p:plain
ホストポートを適当な数字にします。

続きを読む

Windows10にVirtualBoxでUbuntuをインストールしてRuby on Rails開発環境を構築しました

Windows10にRuby on Rails開発環境を自宅に構築したので忘れないうちにメモ書きしておきます。

Ruby on Rails開発環境を構築する環境選び

Windows上に構築するのはやめた

最初はWindows上に構築しようと思ったのですが、もともとWindowsアプリ開発環境を整えているのでカオスになるのが嫌だなと思ってやめました。また、調べてみるとインストールが必要なものが多くて、それだけでOSを初期化したい衝動が後々に出てきても嫌だなと思ったのもあります。他にも、

  • Windows上の開発について情報が少ない
  • Windows向けのRubyのインストーラー開発が滞っている

などネガティブな情報が多くて個人的にもあまりオススメしません。

続きを読む

C#でcsharp2nemを使ってXEMやモザイクを任意のウォレットへ送金する

先日はcsharp2nemを使ってNEMのウォレットの情報を取得する方法について書きました。
www.k-karakuri.com
今回はcsharp2nemを使ってXEMやモザイクを送信する方法について書きます。

csharp2nemを使って送金の準備をする

接続するネットワークを選択する

前回同様、接続先をテストネットかメインネットか指定します。

var connection = new Connection();
connection.SetTestnet();

本番のメインネットの場合はこちら。

var connection = new Connection();
connection.SetMainnet();
続きを読む