2017-08

java製のシステム・バッチにおいてdoubleはいらない子

簡単に言えば、
プログラムの改修などで、doubleを見かけたら、
流用せずに取得元からBigDecimalで持ってくるような直し方をした方が良いですよ。
的な記事。

-----

Javaの1.8をそろそろ仕事でも使いそうだな、ということで、
1.7との互換性について資料を眺めていたところ、こんな記述がありました。
http://www.oracle.com/technetwork/jp/java/javase/overview/8-compatibility-guide-2156366-ja.html
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=7131459

(一部引用)

例として、推奨されるデフォルトのNumberFormatFormat API形式を使用し、
NumberFormat nf = java.text.NumberFormat.getInstance()、nf.format(0.8055d)の順でコードを記述した場合、
この0.8055dという値はコンピュータでは0.80549999999999999378275106209912337362766265869140625として記録されます。
この値をバイナリ形式で正確に表現することはできないためです。
ここで、デフォルトの丸めルールは"half-even"であり、JDK 7でformat()をコールした結果、"0.806"という誤った結果が出力されます。
コンピュータによってメモリに記録されている値が、中間の値よりも"小さい"ため、正しい結果は"0.805"となります。



見た目、四捨五入であれば"0.806"が正しいですが、実際コンピュータ内では"0.8054999..."であるため、
四捨五入した結果は"0.805"になるのが正しい、と。

eclipseの日本語化パッケージ、pleiadesをフルでダウンロードすると、jdkの1.7(7)と1.8(8)があるので、
ビルドパスを都度それぞれ変更して実行することで、挙動の確認ができます。

確かに結果が異なっていました。
なるほどなーと。(小並感

小数点に関しては、個人的にはもうdoubleとかfloatなんてものは、
考えから捨て去ったほうが良いのではないかと思っています。
マニアックな世界では、正確さは求めてなく、
とにかくスピードが重要ってのもあるかもしれませんが、そこは除きます。

プログラムの改修を仕事でやることがありますが、数値を使うときにdoubleを用いているものもちらほら。
最初にdouble使って初期化されているけど、数値計算はBigDecimalじゃないとだめだよな、
ということで途中でdouble値を用いてインスタンス化しているものも。

以下の3つ。それぞれ.toString()の結果として何が出力されるでしょうか。

1:BigDecimal bda = new BigDecimal("0.8055");
2:BigDecimal bdb = new BigDecimal(0.8055d);
3:BigDecimal bdc = new BigDecimal(0.8055);
4:BigDecimal bdd = BigDecimal.valueOf(0.8055d);


結果は以下の通り。(これはjava1.7でも1.8でも同じ結果)
1:0.8055
2:0.80549999999999999378275106209912337362766265869140625
3:0.80549999999999999378275106209912337362766265869140625
4:0.8055


こういったように、Webシステム、バッチ等で小数を用いる際、
doubleは計算で使わないものだとしても、一切使わない方が良い
わけです。
誤差出ますよ。1円違いに泣いたりしますね。

Webで入力値から値を変換する場合でも、数値チェックを行った後に、
単純に new BigDecimal("【入力値】") で良い。
データベースから取る時も、BigDecimalで直接取れればそれでOK、ダメなら文字列から同様にインスタンス化する。

フルスクラッチや機能追加であれば、こういったことで事故になることは、
不具合作った人が悪いので納得ですが、

既存機能の改修となると、既存の変数値の流用を行いがち。(・・・と思ってます)
で、こういった小数による誤差って大体本番まで見つかりにくく、本番以降でこのバグが発見され、
「この機能作ったの誰や!」ってなって、最終更新者が槍玉に挙がり、泣かされる、と。

本当に悪いのは最初にdoubleを使った人なんだけどな・・・
ちょっとかわいそう。2分の1に負けて(というよりはvalueOfを知っているかどうか)。

でもdoubleの危険性を知っていれば、そんな事故もなくなりますね。
そういった部分はBigDecimal.valueOf()で回避します。
apiでもそのように推奨されています。


プリミティブって言語の勉強するときの最初に出てくるので、
本をかじってさぁ実践、といった時に小数=float/doubleってのが浮かんじゃうんですよね。
だから急ごしらえのJava技術者ほどこれは落とし穴かもしれません。

テーマ:プログラミング - ジャンル:コンピュータ

jQueryバージョンによるajax/JSON問題

結構手間取ったのでメモ。

jQueryを1.3から1.11に上げた際、既存のajax部分が動かなくなったので、調査。

結果、jQueryによる$.ajaxメソッドの利用に関しては、
バージョン1.8の前後で変更が行われています。

success → done
error → fail

これに伴い、バージョン1.8以降、
success及びerrorは非推奨メソッドとなってしまいました。

また、これはバージョン不明ですが、
これまで解析できていたJSON文字列が解析できなくなったという事象が発生しています。

(1.3で読めたもの)
[
  [ '1', 'foo']
  [ '2', 'bar']
  [ '3', 'foobar']
]

これらの問題に対処するため、以下の対応を講じます。

● JSON文字列生成部分の修正
大外の囲み文字を「{}」にし、キー名と値を明記して、値をダブルクオーテーションでくくります。
※元々の文字列を全てダブルクオーテーション括りにしてもエラーでした。

{
  "results":["code":"1","value":"foo"
  ,"code":"2","value":"bar"
  ,"code":"3","value":"foobar"
}

● $.ajaxメソッドの修正
asyncは非同期の設定。できれば使用しないようにします。
urlにパラメータがある場合は、なるべくdataに書くようにします。
また、受け取ったJSONは既に解析済みの状態となっていますので、パースする必要はありません。
(before)
$.ajax({
      url: '/hoge/hogehoge?foo=1',
      type: 'GET',
      async: false,
      cache: false,
      dataType:'json',
      timeout: 50000,
      error: function(a,b,c){
        // エラー時の処理
      },
      success: function(ret){
        // 正常時の処理
    }
  });

(after)
$.ajax({
    url: '/hoge/hogehoge',
    type: 'GET',
    cache: false,
    data: {
        foo: 1
    },
    dataType:'json',
    timeout: 50000,
}).done(function(data){
    // 正常時の処理
    // 取得した値は下のように使います
    for ( key in data.results ) {
        //data.results[key].code
        //data.results[key].value
    }
}).fail(function(data){
        // エラー時の処理
});

jQuery、よく使いますがそこそこ変更が掛かってるみたいなので、
リリースノートを見るのは必須です。

デザインで使用するメソッドでjQueryのバージョンアップを行ってくるパターンもあるので、
デザイン反映でも要注意です。
(実際、デザイン反映で発生した事件。)

JSON文字列はそもそも非推奨な形(既存バグ)だったんじゃないかと思いますが、
1.3では読めたみたいで。それがさらに混乱を招きました・・・。
技術メモがほとんどバージョン絡み。。

【Java】iTextを用いたTiff→PDF変換に関するメモ

たまには技術もの。
iTextによるTiffファイルをJavaによってPDFに変換する処理を作る中での出来事。
Java:1.7.0 72
iText:5.4.4
iTextのページ:http://itextpdf.com/

iTextって、PDF作成で以前使ったことが有りますが、画像ファイルをPDFファイルに変換することもできるんですね。

ただ、その場合、メモリのことを気にしてあげた方が良く、
その実装時、RandomAccessFileOrArrayの使い方によってはdeprecated警告が出てしまうので、
その対策を忘れないようにメモ。

JavaDocにも書かれている。
→Deprecated. use RandomAccessSourceFactory.createSource(InputStream)
and RandomAccessFileOrArray(RandomAccessSource) instead

●RandomAccessFileOrArrayに直接InputStreamを渡している場合
→@deprecated警告が出る

public void processFile(final File pdfFile, final File targetFile) throws Exception {

	Document document = new Document(PageSize.A4
			, 0
			, 0
			, 0
			, 0);

	try {
		PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(pdfFile));
		writer.setStrictImageSequence(true);
		document.open();
		
		RandomAccessSourceFactory factory = new RandomAccessSourceFactory();
		try (FileInputStream stream = new FileInputStream(targetFile)) {
			RandomAccessFileOrArray ra = new RandomAccessFileOrArray(stream);
			int pagesTif = TiffImage.getNumberOfPages(ra);
			for (int i = 1; i <= pagesTif; i++) {
				Image image = TiffImage.getTiffImage(ra, i);
				image.scaleAbsolute(PageSize.A4.getHeight(), PageSize.A4.getWidth());
				Rectangle pageSize = new Rectangle(image.getScaledWidth(), image.getScaledHeight());
				document.setPageSize(pageSize);
				document.newPage();
				document.add(image);
			}
			document.close();
		} catch (IOException e) {
			// 例外処理
		}
	} finally {
		if (document.isOpen()) {
			document.close();
		}
	}
}

●RandomAccessFileOrArrayにRandomAccessSource経由でInputStreamを渡している場合
→正常

public void processFile(final File pdfFile, final File targetFile) throws Exception {

	Document document = new Document(PageSize.A4
			, 0
			, 0
			, 0
			, 0);

	try {
		PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(pdfFile));
		writer.setStrictImageSequence(true);
		document.open();
		
		RandomAccessSourceFactory factory = new RandomAccessSourceFactory();
		try (FileInputStream stream = new FileInputStream(targetFile)) {
			RandomAccessSource source = factory.createSource(stream);
			RandomAccessFileOrArray ra = new RandomAccessFileOrArray(source);
			int pagesTif = TiffImage.getNumberOfPages(ra);
			for (int i = 1; i <= pagesTif; i++) {
				Image image = TiffImage.getTiffImage(ra, i);
				image.scaleAbsolute(PageSize.A4.getHeight(), PageSize.A4.getWidth());
				Rectangle pageSize = new Rectangle(image.getScaledWidth(), image.getScaledHeight());
				document.setPageSize(pageSize);
				document.newPage();
				document.add(image);
			}
			document.close();
		}
	} finally {
		if (document.isOpen()) {
			document.close();
		}
	}
}

実際のコーディングでは、PageSize.A4とか余白設定(0,0,0,0のところ)は外出し、
Tiff以外にも変換したいものがある可能性があるので、
processFileはインタフェースとしてprocessFileを持たせ、実装し、
各々のフォーマットごとに変換できるよう、Factoryクラスを作って各種フォーマットに対応すると言った感じです。

ADD_MONTHSの仕様について

忘れそう(多分忘れる)のでメモ。
要件で、「3ヶ月経過したら・・・」とか言われた時は、そんな曖昧な表現で本当にいいのか、
確認をしたほうが後々揉めませんよ、的な話。

OracleのADD_MONTHS関数についてです。

対象件数をカウントしたいということで、
条件として伝えられたのは以下の通り。

「対象の日付が、本日日付より3ヶ月以上前の場合、カウントする。」

3ヶ月ということで浮かぶのが月計算関数、ADD_MONTHS。
引数にDATE型と加減算月数を入れるだけで良いので気楽に使えます。

ただ、知らないで関数を使うとえらいことになる可能性を秘めているのです。

--●通常→3ヶ月前の同日

SELECT ADD_MONTHS(TO_DATE('20140227','YYYYMMDD'), -3) FROM DUAL
-- 2013/11/27

--●月末の場合→3ヶ月前の末日

SELECT ADD_MONTHS(TO_DATE('20140228','YYYYMMDD'), -3) FROM DUAL
-- 2013/11/30

SELECT ADD_MONTHS(TO_DATE('20140430','YYYYMMDD'), -3) FROM DUAL
-- 2014/01/31

--●3ヶ月前の当日が存在しない場合→存在する日に自動補正

SELECT ADD_MONTHS(TO_DATE('20140531','YYYYMMDD'), -3) FROM DUAL
-- 2014/02/28

SELECT ADD_MONTHS(TO_DATE('20140530','YYYYMMDD'), -3) FROM DUAL
-- 2014/02/28

SELECT ADD_MONTHS(TO_DATE('20140529','YYYYMMDD'), -3) FROM DUAL
-- 2014/02/28

SELECT ADD_MONTHS(TO_DATE('20140528','YYYYMMDD'), -3) FROM DUAL
-- 2014/02/28

この仕様のため、
例の場合は3ヶ月の計算をしていますが、
日数換算すると、89日~92日の間で幅があることになります。

よく、文面で「有効期間は90日です。」と書かれている場合、
この方法で計算すると、90日経過していないのに期限切れみたいな扱いをされてしまうことが稀に発生します。
そもそも90日ピッタリになることはありません。
ここでいう稀は、運用面から見た場合であり、狙ってやると必ず発生します。

なので、きっちりと経過日数で計算したい場合は、この関数を使用せず、
DATE型に変換後、日数分引き算します。

SELECT TO_DATE('20140228','YYYYMMDD') - 90 FROM DUAL

定量的に見えて実は完全ではないものだった、ということですね。

SCPコマンドにおける情報保持オプションについて

ミスったのでメモ。

ファイルサーバ移転の際、今回はSCPコマンドを用いて移動を行ったのですが、
何も指定せず行ったため、作成日時や更新日時が全て移動した日になってしまった、という話。

そもそも最終更新日時等でファイル管理するのもどうかという個人的な意見は置いておいて、
SCPコマンドで何を指定すればよかったのか。そもそも可能なのか。

答えは「scp -p」。

[説明(man scpより)]
Preserves modification times, access times, and modes from the original file.

変更時間、最終アクセス日時、モード(770とかそういうの)を保持してくれるようです。
所有者、所有グループは保持しない。

cpコマンドの-pとは所有者・所有グループの保持という点で違うみたいなので注意。

今回のような、ディレクトリごとコピー(-r)する場合は以下の通りに修正。

scp -r "[転送元ユーザ]@[転送元IP]:[転送元パス]" "[転送先ユーザ]@[転送先IP]:[転送先パス]"

scp -rp "[転送元ユーザ]@[転送元IP]:[転送元パス]" "[転送先ユーザ]@[転送先IP]:[転送先パス]"

私はもうやってしまったのでこの件に関してはごめんなさいするしかありませんがorz


«  | ホーム |  »

プロフィール

青竜斬

Author:青竜斬
声優ヲタで萌えスロッターでオーガスト好きでMJプレイヤーなプログラマー、青竜斬が書いております。
プログラマーなので主に文字しか産み出せません。

※当サイトは声優、1986年生まれの方々を応援してます。
現状、スロットネタが多いのでジャンルをギャンブルにしてます。

最新記事

最新コメント

最新トラックバック

月別アーカイブ

カテゴリ

日記 (18)
技術メモ (9)
スロット実戦 (96)
スロット戦績 (35)
スロット雑談 (27)
パズドラ (5)
艦これ (25)
収支管理(パチスロ) (5)
城プロ (2)
MJ (5)
かんぱに☆ガールズ (4)

RSSリンクの表示

カウンター

検索フォーム

RSSリンクの表示

リンク

このブログをリンクに追加する

ブロとも申請フォーム

この人とブロともになる

QRコード

QR