DateFormatは前方一致だったのか

yyyy/MMという日付書式に、2010/06/15っていう文字列日付を入れてもパースできちゃうっていうのはどうゆう仕様なんだろうか。

JavaのDateFormat - 何言語でも話したいPGの開発日記


気になったので調べてみました。

メソッドは pos によって指定されたインデックスを開始位置としてテキストの解析を試みます。解析が完了すると、pos のインデックスは、使用された最後の文字 (解析では、文字列の最後までのすべての文字を使用する必要はありません) のあとのインデックスに更新され、解析された日付が返されます。更新された pos は、このメソッドの次の呼び出しの開始点を示すのに使用できます。エラーが発生した場合は、pos のインデックスは変更されず、エラーが発生した文字のインデックスに pos のエラーインデックスが設定され、null が返されます。

DateFormat#parseObject (Java Platform SE 6)


DateFormatの基底クラスにあたるFormatクラスは、区切り文字で区切られてる連続した文字列の集まりをパースできるように作られているらしく、そのため解析を完了したらその後に続く文字列は無視する仕様みたいです。

正規表現でのチェックや、日付に変換してから文字列に戻して変換前の文字列になるかっていうチェックでも問題ない気がしますが、コストを重視するなら文字列長のチェックだけ行うのが良いと思いました。また、parseメソッドにParsePositionっていうのを渡してあげると、チェックNGのときにParseExceptionが発生しないようです。これもコスト的なメリットになりそうです。

せっかくなので書いてみました

import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;

public class DateValidator {
  public static boolean isValid(String date, String format) {

    if(date.length() != format.length()) {
      return false;
    }
    
    DateFormat dateFormat = createStrictDateFormat(format);
    ParsePosition pos = new ParsePosition(0);
    return dateFormat.parse(date, pos) != null;
  }
  
  protected static DateFormat createStrictDateFormat(String format) {
    DateFormat dateFormat = new SimpleDateFormat(format);
    dateFormat.setLenient(false);
    return dateFormat;
  }
}

テスト

import static org.junit.Assert.*;

import org.junit.Test;


public class DateValidatorTest {

  @Test
  public void testIsValid() {
    assertFalse(DateValidator.isValid("2010/06/30", "yyyy/MM"));
    assertFalse(DateValidator.isValid("2010/6/31", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/06/00", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/06/30<script>...</script>", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/06/31", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/6/3", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("13:57", "hh:mm"));
    assertTrue(DateValidator.isValid("2010", "yyyy"));
    assertTrue(DateValidator.isValid("2010/06", "yyyy/MM"));
    assertTrue(DateValidator.isValid("2010/06/30", "yyyy/MM/dd"));
    assertTrue(DateValidator.isValid("2010年06月30日", "yyyy年MM月dd日"));
  }

}

あ、書いてから気がつきました。DateFormatはスレッドセーフでないから毎回newしなければいけないと思ってましたが、ThreadLocalクラスを使えば、同一スレッド内で使いまわすことができてもう少し節約できそうです。今度やってみよう。

追記

元記事の方からコメントを頂いて気がつきました。

SimpleDateFormatは各フィールドの桁数はチェックしてくれないのですね。

書式 yyyy/MM
日付文字列 2010/0000006/

とか

書式 yyyy/MM/dd
日付文字列 1/2/3

とかもエラーにならずにパースしてくれます。setLenient(false)しててもエラーになりません。なんてこったい。

さらに、前方一致での解析というのが手伝って

書式 yyyy/MM/dd
日付文字列 1/2/3あいうえお

という場合でもエラーにならないため「書式文字列の長さと日付文字列の長さを比較する」という方法が通用しなくなってしまいました。なんてこったい。

「SimpleDateFormatはバリデーションには使えない。やっぱり正規表現しかないのか」と悲しみに暮れていたら、ParsePositionが役に立ってくれました。ParsePostionはパースした文字数を保持しているので、それと書式文字列の長さを比較したらいい感じになりました。

修正版DateValidator

import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;

public class DateValidator {
  public static boolean isValid(String date, String format) {

    if(date.length() != format.length()) {
      return false;
    }
    
    DateFormat dateFormat = createStrictDateFormat(format);
    ParsePosition pos = new ParsePosition(0);
    return dateFormat.parse(date, pos) != null &&
      pos.getIndex() == format.length();  // <-- 解析した長さと書式の長さを比較
  }
  
  protected static DateFormat createStrictDateFormat(String format) {
    DateFormat dateFormat = new SimpleDateFormat(format);
    dateFormat.setLenient(false);
    return dateFormat;
  }
}

テスト

import static org.junit.Assert.*;

import org.junit.Test;


public class DateValidatorTest {

  @Test
  public void testIsValid() {
    assertFalse(DateValidator.isValid("2010/06/30", "yyyy/MM"));
    assertFalse(DateValidator.isValid("2010/6/", "yyyy/MM")); // <- 書式と日付の長さが一致するケース(7文字)
    assertFalse(DateValidator.isValid("1/2/3あいうえお", "yyyy/MM/dd")); // <- 書式と日付の長さが一致するケース(10文字)
    assertFalse(DateValidator.isValid("2010/6/31", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/06/00", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/06/30<script>...</script>", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/06/31", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("2010/6/3", "yyyy/MM/dd"));
    assertFalse(DateValidator.isValid("13:57", "hh:mm"));
    assertTrue(DateValidator.isValid("2010", "yyyy"));
    assertTrue(DateValidator.isValid("2010/06", "yyyy/MM"));
    assertTrue(DateValidator.isValid("2010/06/30", "yyyy/MM/dd"));
    assertTrue(DateValidator.isValid("2010年06月30日", "yyyy年MM月dd日"));
  }
}

できたにはできたけど、なんだか無理やり感が出てきました。やっぱりparse()したものをformat()で元に戻して、元の文字列と一致するか調べるのがわかりやすくて良いのだろうなあ。

CaseFormat

guava-librariesのCaseFormatというenumを見ていました。

キャメルケースとかハイフン区切りとか大文字小文字とか、文字列の書式を表す列挙のようです。

「helloWorld」を「HELLO_WORLD」に変換してくれる「to」というメソッドが提供されていました。

    LOWER_CAMEL.to(UPPER_UNDERSCORE, "helloWorld")); // HELLO_WORLD

エンティティのクラス名からテーブル名に変換するのに使えそうです。

テスト

package com.google.common.base;

import static com.google.common.base.CaseFormat.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.List;

import org.junit.Test;

public class CaseFormatTest {

  @Test
  public void testValues() {
    List<CaseFormat> actual = Arrays.asList(CaseFormat.values());
    assertEquals(Arrays.asList(
        LOWER_HYPHEN,        // 小文字 ハイフン区切り
        LOWER_UNDERSCORE,    // 小文字 アンダースコア区切り
        LOWER_CAMEL,         // 小文字で始まるキャメルケース
        UPPER_CAMEL,         // 大文字で始まるキャメルケース
        UPPER_UNDERSCORE     // 大文字 アンダースコア区切り
    ), actual);
  }

  @Test
  public void testTo() {
    String word = "hello-world";
    assertEquals("hello_world", LOWER_HYPHEN.to(LOWER_UNDERSCORE, word));
    assertEquals("helloWorld" , LOWER_HYPHEN.to(LOWER_CAMEL     , word));
    assertEquals("HelloWorld" , LOWER_HYPHEN.to(UPPER_CAMEL     , word));
    assertEquals("HELLO_WORLD", LOWER_HYPHEN.to(UPPER_UNDERSCORE, word));
    
    word = "hello_world";
    assertEquals("hello-world", LOWER_UNDERSCORE.to(LOWER_HYPHEN    , word));
    assertEquals("helloWorld" , LOWER_UNDERSCORE.to(LOWER_CAMEL     , word));
    assertEquals("HelloWorld" , LOWER_UNDERSCORE.to(UPPER_CAMEL     , word));
    assertEquals("HELLO_WORLD", LOWER_UNDERSCORE.to(UPPER_UNDERSCORE, word));
    
    word = "helloWorld";
    assertEquals("hello-world", LOWER_CAMEL.to(LOWER_HYPHEN    , word));
    assertEquals("hello_world", LOWER_CAMEL.to(LOWER_UNDERSCORE, word));
    assertEquals("HelloWorld" , LOWER_CAMEL.to(UPPER_CAMEL     , word));
    assertEquals("HELLO_WORLD", LOWER_CAMEL.to(UPPER_UNDERSCORE, word));
    
    word = "HelloWorld";
    assertEquals("hello-world", UPPER_CAMEL.to(LOWER_HYPHEN    , word));
    assertEquals("hello_world", UPPER_CAMEL.to(LOWER_UNDERSCORE, word));
    assertEquals("helloWorld" , UPPER_CAMEL.to(LOWER_CAMEL     , word));
    assertEquals("HELLO_WORLD", UPPER_CAMEL.to(UPPER_UNDERSCORE, word));
    
    word = "HELLO_WORLD";
    assertEquals("hello-world", UPPER_UNDERSCORE.to(LOWER_HYPHEN    , word));
    assertEquals("hello_world", UPPER_UNDERSCORE.to(LOWER_UNDERSCORE, word));
    assertEquals("helloWorld" , UPPER_UNDERSCORE.to(LOWER_CAMEL     , word));
    assertEquals("HelloWorld" , UPPER_UNDERSCORE.to(UPPER_CAMEL     , word));
    
  }

}

GoogleのJavaライブラリ guava-libraries

夜中にインターネットを見ていたらguava-librariessというJavaライブラリを見つけました。

GoogleJavaライブラリだから「guava(ぐわば)」ライブラリ。ダジャレですね。ダジャレは嫌いじゃないです。

パッケージはこんな感じ

apache-commonsみたいな感じで、言語のコアな部分を提供してくれるようです。Google Collections Libraryも、この中に含まれています。concurrentっていうのは並列処理の便利ライブラリなのかな。

面白そうだなあ

面白そうなのでjavadoc読んだり遊んでみたりしたいです。

noopのBoolean型

noopにBoolean型が実装されたみたいです。
10f733a293 - noop - Project Hosting on Google Code



Booleanクラスには以下のメソッドが定義されてました。

  • and
  • or
  • xor
  • not
  • toString

試してみよう

import noop.Application;
import noop.Console;

class HelloBoolean(Console console) implements Application {

  Int main(List args) {
    Boolean t = true;
    Boolean f = false;

    console.println( t ); // true
    console.println( f ); // false
    
    console.println( t.and(f) ); // false
    console.println( t.or(f) );  // true
    console.println( t.xor(f) ); // true
    console.println( t.not() );  // false
    
    return 0;
  }
}

Booleanもオブジェクトなので、下のように直接メソッドを呼び出すことができました。

import noop.Application;
import noop.Console;

class HelloBoolean(Console console) implements Application {

  Int main(List args) {
    console.println( true.and(false) ); // false
    console.println( true.or(false) );  // true
    console.println( true.xor(false) ); // true
    console.println( true.not() );      // false
    
    return 0;
  }
}

noopでフィボナッチ数列

whileループが実装されました。
4c84f065ce - noop - Project Hosting on Google Code

まだ比較演算子ができてない状態なので、10回実行されるループ文みたいなのは書けないみたいです。


無限ループとか

while(true){
  ...
}

一度も実行されないループしか書けません。

while(false){
  ...
}

と思ってたら、新しいサンプルが追加されてて、一度だけ実行されるwhileループのサンプルがありました。

Boolean b = true;
while(b){
  console.println("Hello World!");
  b = false;
}

なるほどなー
あれ?そういえば変数はデフォルトでfinalになるんじゃなかったっけ?!その辺はまだ実装されていないのか。

フィボナッチ数列

ループが書けるようになったので、無限ループ上等でフィボナッチ数列を出力するコードを書いてみました。

import noop.Application;
import noop.Console;

class Fibonacci(Console console) implements Application {

  Int main(List args) {
    Int prev = 0;
    Int curr = 1;
    Int next = 1;
    
    while(true){
      console.println(curr);
      next = curr.plus(prev); // 「next = curr + prev;」と同じ
      prev = curr;
      curr = next;
    }
    
    return 0;
  }
  
}


実行結果

target>scala -cp classes;resources;"%HOME%\.m2\repository\org\antlr\antlr\3.1.1\antlr-3.1.1.jar" noop.interpreter.InterpreterMain Fibonacci resources\stdlib resources\helloworld
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025
121393
196418
317811
514229
832040
1346269
2178309
3524578
5702887
9227465
14930352
24157817
39088169
63245986
102334155
165580141
267914296
433494437
701408733
1134903170
1836311903
-1323752223
512559680
-811192543

おおー

思ったこと

まだ実装途中のようで、できないことがいっぱいあるのだけど、その制約が逆に面白くなってきました。できることが段々増えていくのを見てるのも楽しいです。
「Intクラスにeqメソッドがあればいいのになー」と思い、自分で書いてみようと思ったのだけどよくわからなくなって諦めました。無念です。

エンジニアの未来サミット0905見た

色々あって最初の30分くらいしか見られなかったー。残念だー。
去年のが好評だったので、今年もリンク集を作って参加した気になろう。

感想

編集ツール

javascript:(function(){var%20isMSIE%20=%20/*@cc_on!@*/false;var%20opacity%20=%2070;var%20setOpacity%20=%20function(elm){if(isMSIE)%20{elm.style.filter%20=%20'alpha(opacity='+%20opacity%20+')';}%20else%20{elm.style.MozOpacity%20=%20(opacity/100);}};var%20info%20=%20document.createElement('div');info.style.position%20=%20isMSIE%20?%20'absolute':'fixed';info.style.top%20=%20'0';info.style.left%20=%20'0';info.style.width%20=%20'100%';info.style.background%20=%20'#000000';info.style.color%20=%20'#FCFCFC';info.style.textAlign%20=%20'left';info.style.zIndex%20=%201000;info.style.MozBorderRadius%20=%20'0%200%2020px%2020px';info.style.height%20=%200;setOpacity(info);var%20body%20=%20document.getElementsByTagName('body')[0];body.appendChild(info);var%20h%20=%200;show%20=%20setInterval(function(){info.style.height%20=%20(h%20+%20'em');h%20+=%200.4;if(h%20>%206)%20{clearInterval(show);info.innerHTML%20=%20'<div%20style="margin:1em%200%200%202em">-['%20+%20location.href%20+%20":title="%20+%20document.title.replace(/\[\]/,"")%20+%20']</div>';wait%20=%20setTimeout(function(){hide%20=%20setInterval(function(){opacity%20-=%2010;setOpacity(info);if(opacity%20==%200)%20{body.removeChild(info);clearInterval(hide);clearTimeout(wait);}},25);},%205000);}},30);})();void(0);