Java から ActiveRecord を利用する(不完全)

Javaプログラムから、JRubyを利用してRails 3.0のActiveRecordを使う実験をしていたが、どうも上手くいかなかった。

環境構築

OSは Windows 7 の64bit版。

Javaのインストール

最新版のJDK(x64)をインストール。省略。

Maven2のインストール

最新版のMaven2(3.0.1)をインストール。

こんな感じ。

> mvn -v
Apache Maven 3.0.1 (r1038046; 2010-11-23 19:58:32+0900)
Java version: 1.6.0_23
Java home: C:\Program Files\Java\jdk1.6.0_23\jre
Default locale: ja_JP, platform encoding: MS932
OS name: "windows 7" version: "6.1" arch: "amd64" Family: "windows"
JRuby のインストール

JRuby(x64)の最新版をインストール。
1.5.6 を D:/bin/jruby-1.5.6 にインストールした。

こんな感じ。

> jruby -v
jruby 1.5.6 (ruby 1.8.7 patchlevel 249) (2010-12-03 9cf97c3) (Java HotSpot(TM) 6
4-Bit Server VM 1.6.0_23) [amd64-java]
Railsのgemをインストール

Railsの最新版(3.0.3)をインストール。

> jgem install rails

おわり。

ActiveRecord-JDBC のgemをインストール

JavaなのでJDBCで。activerecord-jdbc-adapterの最新版(1.1.0)をインストール。

> gem install activerecord-jdbc-adapter

Invalid output formatter とか出るけど気にしない。

あと、今回はSQLiteを使うのでSQLite3用のgemもインストール。

> gem install activerecord-jdbcsqlite3-adapter

実装

適当なディレクトリを作って、その中に以下のファイルを置く。

/pom.xml

Maven2を使うのでpom。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>jruby</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.jruby</groupId>
      <artifactId>jruby-complete</artifactId>
      <version>1.5.6</version>
    </dependency>

    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.0.1</version>
    </dependency>

  </dependencies>

  <repositories>
    <repository>
      <id>codehaus</id>
      <name>Maven Codehaus repository</name>
      <url>http://repository.codehaus.org/</url>
    </repository>
  </repositories>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
/src/main/java/User.java

ユーザ情報のI/F。実体はJRubyで実装する。

package example;

public interface User {
    String getName();
}
/src/main/java/UserManager.java

ユーザ管理クラスのI/F。実体はJRubyで実装する。

package example;

import java.util.List;

import org.jruby.RubyObject;

public interface UserManager {
    /* ユーザ一覧取得 */
    List<RubyObject> all_users_ro();
    /* ユーザ一覧取得 */
    List<User> all_users();
    /* ユーザー登録 */
    User create_user(String name);
    /* ユーザーテーブル作成 */
    void create_table();
}

実験のために、ユーザの一覧取得サービスはUserのリストで受け取るものと、RubyObjectという生のRubyオブジェクトのリストで受け取るものと、2種類用意した。

/src/main/resources/user_manager.rb

ActiveRecord を使ったユーザ管理クラスを作って、そのインスタンスを返すJRubyスクリプト

require 'rubygems'
require 'arjdbc'

class User < ActiveRecord::Base
  include Java::example.User
end

class UserManager
  include Java::example.UserManager

  def initialize
    ActiveRecord::Base.establish_connection(
      :adapter => "jdbcsqlite3",
      :database => "test.db"
    )
  end

  def create_table
    ActiveRecord::Schema.define do
      create_table :users, :force => true do |t|
        t.column :name, :string
      end
    end
  end

  def create_user(name)
    User.create(:name => name)
  end

  def all_users_ro
    User.all
  end

  alias :all_users :all_users_ro
end

UserManager.new

Java側のUserManagerインタフェースで定義したメソッドを実装している。
all_users() と all_users_ro() は、実体は同じ。

/src/main/java/Main.java

メインクラス。

package example;

import java.io.File;
import java.util.Arrays;
import java.util.List;

import org.jruby.RubyObject;
import org.jruby.embed.PathType;
import org.jruby.embed.ScriptingContainer;

public class Main {

    public static void main(String[] args) throws Exception {
        String classpath = System.getProperty("java.class.path");
        List<String> loadPaths = Arrays.asList(classpath.split(File.pathSeparator));

        ScriptingContainer container = new ScriptingContainer();
        container.setHomeDirectory("D:/bin/jruby-1.5.6");
        container.setLoadPaths(loadPaths);

        Object userManagerObject = container.runScriptlet(PathType.CLASSPATH, "user_manager.rb");
        UserManager userManager = container.getInstance(userManagerObject, UserManager.class);

        // 一回目だけ
        userManager.create_table(); // (1)

        userManager.create_user("John Doe");  // (2)

        System.out.println("*** RubyObject ***");
        for (RubyObject ro : userManager.all_users_ro()) {
            User user = (User)ro.toJava(User.class);
            System.out.println(user.getName());
        }

        System.out.println("*** Auto mapping ***");
        for (User user : userManager.all_users()) {
            System.out.println(user.getName());
        }
    }
}

実行

pom.xml のあるディレクトリから、以下のように実行する。

> mvn exec:java -Dexec.mainClass=example.Main

まず1回実行するとこんな結果が表示され、テーブルが作られてユーザが1人登録されたことがわかる。

-- create_table(:users, {:force=>true})
   -> 0.8940s
   -> 0 rows
*** RubyObject ***
John Doe
*** Auto mapping ***
John Doe

その後 (1) をコメントアウトすると、既存のテーブルに対してユーザが追加登録され、2件のユーザが表示される。

*** RubyObject ***
John Doe
John Doe
*** Auto mapping ***
John Doe
John Doe

で、ここからが問題なんだけど、さらに (2) をコメントアウトすると、何故か List を返す方のメソッドが失敗する。

*** RubyObject ***
John Doe
John Doe
*** Auto mapping ***
Exception in thread "main" java.lang.ClassCastException: org.jruby.RubyObject cannot be cast to example.User
	at example.Main.main(Main.java:38)

ruby側で User.create を呼び出しているとOK、呼び出していないと ClassCastException というよくわからない状況。RailsにもJRubyにもまだ未熟のため、この最後の問題がどうしても解決できず。

UIBarButtonItem 内に UISearchBar を配置するとキャンセルボタンが出ない

iOS SDK 3.2.5 でアプリを作っているんだけど、UIBarButtonItem 内に UISearchBar を配置するとキャンセルボタンが出ないという細かい問題に悩まされた。

具体的には、例えば以下のように UINavigationItem に UISearchBar を入れると検索バーは表示されて入力もできるが、UISearchBar の setShowsCancelButton で YES をセットしても、なぜかキャンセルボタンが表示されない。

UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 300, 44)];

self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:searchBar] autorelease];

SDKのソースが公開されていないので理由はさっぱりわからないけど、UISearchBar の右に兄弟ビューを入れられるような構造になっていないとだめなのかなぁと思い、以下のように適当に UIView をかませてみたら、ちゃんと表示されるようになった。

UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, 300, 44)];
UIView *searchBarContainer = [[[UIView alloc] initWithFrame:searchBar.frame] autorelease];
[searchBarContainer addSubview:searchBar];

self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:searchBarContainer] autorelease];

とりあえずめでたい。

それにしても、iOS SDKは妙なバグだか制限だかが多くて面倒だ。この件とか、UISplitViewController や UITabBarController のビューは root view にしないとだめだとか(UINavigationController に UISplitViewController を入れたいんですけど…)、位置とサイズを同時に変更するようなアニメーションが妙な動きをする*1とか(SafariiPadEvernoteの検索バーが拡大するような動きって、作り込みが必要なの?)、UIBarButtonSystemItemFlexibleSpace がバカで、「ツールバー中央と右だけに」ボタンを配置すると中央のボタンの位置がずれるとか*2
。ほかにもあったけど忘れた。

*1:http://stackoverflow.com/questions/2460313/animating-resizing-and-moving-uiview-at-the-same-time

*2:例: 左側のボタンが中央にならず、flexible spaceの意味がない

SQLite (SQLite3) で JPA

JPA (Java Persistence API) でも SQLite を使いたい!ということで試してみる。いちいちデータベースサーバとか用意したり起動したり面倒くさいし。

環境としてはMacでこんな感じ。要Maven2

$ java -version
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
java version "1.6.0_20"
Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)
$ mvn -v
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
Apache Maven 2.2.0 (r788681; 2009-06-26 22:04:01+0900)
Java version: 1.6.0_20
Java home: /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x" version: "10.6.4" arch: "x86_64" Family: "mac"

まず pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example.jpa</groupId>
  <artifactId>hibernate</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>Hibernate JPA Example</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <inherited>true</inherited>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-eclipse-plugin</artifactId>
        <configuration>
          <downloadJavadocs>true</downloadJavadocs>
          <downloadSources>true</downloadSources>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <!-- SQLite3のJDBCドライバ。Central Repository での最新版 3.6.20 だと不具合あり -->
    <!-- see: http://code.google.com/p/xerial/issues/detail?id=54 -->
    <dependency>
      <groupId>org.xerial</groupId>
      <artifactId>sqlite-jdbc</artifactId>
      <version>3.6.20.1</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.5.3-Final</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
    	<groupId>ch.qos.logback</groupId>
    	<artifactId>logback-classic</artifactId>
    	<version>0.9.24</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
    <!-- Hibernateから自動解決されるバージョンだと上の logback で問題があるので指定 -->
    <dependency>
    	<groupId>org.slf4j</groupId>
    	<artifactId>slf4j-api</artifactId>
    	<version>1.6.1</version>
    	<type>jar</type>
    	<scope>compile</scope>
    </dependency>
  </dependencies>

  <repositories>
    <repository>
    	<id>Xerial.org</id>
    	<name>Xerial.org Repo</name>
    	<url>http://www.xerial.org/maven/repository/artifact/</url>
    </repository>
    <repository>
    	<id>JBoss.org</id>
    	<name>JBoss.org Repo</name>
    	<url>https://repository.jboss.org/nexus/content/groups/public/</url>
    </repository>
  </repositories>
</project>

HibernateはDBごとの違いをdialect(方言)で吸収しているが、HibernateにはSQLite3用のDialectが無いようなので(EclipseLinkにも無い)、自分で作成する。一番近いのはおそらく MySQL なので、MySQL用の Dialect をもとに差異がある部分のみオーバライド。非常に手抜きで、このエントリのデモが動作する程度しか確認していない。

src/main/java/com/example/jpa/hibernate/SQLite3Dialect.java

package com.example.jpa.hibernate;

import org.hibernate.dialect.MySQL5Dialect;

public class SQLite3Dialect extends MySQL5Dialect {

	public SQLite3Dialect() {
		super();
	}
	
	/**
	 * INSERT後のID取得用SQL。
	 */
	@Override
	public String getIdentitySelectString() {
		return "select last_insert_rowid()";
	}

	/**
	 * 自動採番ID用のカラム定義。
	 * デフォルトだと Long 型は BIGINT になってしまう。
	 */
	@Override
	public String getIdentityColumnString() {
		return "integer";
	}
	
	/**
	 * IDカラムにデフォルトの型を使用するかどうか。
	 * {@link #getIdentityColumnString()} の定義のみを使いたいので false。
	 * true のままだと、"id bigint integer" のような定義になってしまう。
	 */
	@Override
	public boolean hasDataTypeInIdentityColumn() {
		return false;
	}
	
	/**
	 * タイムスタンプ取得用の関数名。
	 * デモでは使わない。これでいいのかも不明。
	 */
	@Override
	public String getCurrentTimestampSQLFunctionName() {
		return "datetime('now')";
	}
	
	/**
	 * タイムスタンプ取得用のSQL。
	 * デモでは使わない。これでいいのかも不明。
	 */
	@Override
	public String getCurrentTimestampSelectString() {
		return "select datetime('now')";
	}
}

JPA管理のエンティティの共通親クラス
src/main/java/com/example/jpa/hibernate/BasicEntity.java

package com.example.jpa.hibernate;

import java.util.Date;

import javax.persistence.*;

@MappedSuperclass
public class BasicEntity {
	
	@Id
	@GeneratedValue
	private Long id;
	@Version
	private Long version;
	private Date createdAt;
	private Date updatedAt;

	public Long getId() {
		return id;
	}
	
	public Long getVersion() {
		return version;
	}
	
	public void setVersion(Long version) {
		this.version = version;
	}

	public Date getCreatedAt() {
		return createdAt;
	}

	public Date getUpdatedAt() {
		return updatedAt;
	}

	@PrePersist
	public void onCreate() {
		createdAt = new Date();
		onUpdate();
	}
	
	@PreUpdate
	public void onUpdate() {
		updatedAt = new Date();
	}
}

テスト用の Customer クラス

src/main/java/com/example/jpa/hibernate/Customer.java

package com.example.jpa.hibernate;

import javax.persistence.*;

@Entity
@NamedQueries({
	@NamedQuery(name = Customer.FIND_ALL, query = "SELECT c FROM Customer c")
})
public class Customer extends BasicEntity {
	
	public static final String FIND_ALL = "findAllCustomers";

	private String name;

	public Customer() {
		super();
	}

	public Customer(String name) {
		this();
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	public String toString() {
		return String.format(
				"id=%d, name=%s, version=%d, created_at=%s, updated_at=%s",
				getId(), getName(), getVersion(), getCreatedAt(), getUpdatedAt());
	}
}

JPAの設定。さっき作った Dialect を使うように指定する。

src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
        version="2.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="testPu" transaction-type="RESOURCE_LOCAL">
        <class>com.example.jpa.hibernate.Customer</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.sqlite.JDBC" />
            <property name="javax.persistence.jdbc.url" value="jdbc:sqlite:test.db" />
            
            <property name="hibernate.dialect" value="com.example.jpa.hibernate.SQLite3Dialect" />
            <!-- createdAt (Java) <=> created_at (DB) のような naming strategy  -->
            <property name="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            
            <property name="hibernate.show-sql" value="true" />
            <property name="hibernate.format-sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

テスト用のメインクラス。引数ひとつでその名前の Customer を追加、引数ふたつ(名前、ID)でそのIDの Customer の名前を更新する。

src/main/java/com/example/jpa/hibernate/Customer.java

package com.example.jpa.hibernate;

import javax.persistence.*;

public class App {

	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("testPu");
		EntityManager em = emf.createEntityManager();
		EntityTransaction tx = em.getTransaction();

		Customer customer;

		tx.begin();

		if (args.length >= 2) {
			customer = em.find(Customer.class, Long.valueOf(args[1]));
		} else {
			customer = new Customer();        	
		}

		customer.setName(args[0]);
		em.persist(customer);
		tx.commit();

		Customer found = em.find(Customer.class, customer.getId());
		System.out.println("PERSISTED OBJECT: " + found);

		TypedQuery<Customer> query = em.createNamedQuery(Customer.FIND_ALL, Customer.class);
		for (Customer c : query.getResultList()) {
			System.out.println("OBJECTS IN TABLE: " + c);
		}

		em.close();
		emf.close();
	}
}

これで、pom.xml のあるフォルダから

mvn exec:java -Dexec.mainClass=com.example.jpa.hibernate.App -Dexec.args="山田太郎"

とやると

OBJECTS IN TABLE: id=1, name=山田太郎, version=0, created_at=Mon Jul 19 18:51:12 JST 2010, updated_at=Mon Jul 19 18:51:12 JST 2010

のようにデータが作成され、

mvn exec:java -Dexec.mainClass=com.example.jpa.hibernate.App -Dexec.args="上田次郎 1"

とやると

OBJECTS IN TABLE: id=1, name=上田次郎, version=1, created_at=2010-07-19 18:51:12.448, updated_at=Mon Jul 19 18:53:11 JST 2010

のように更新されましたとさ*1

EclipseLink でも上と同じようなことを試したけど、

  • 「主キーカラムだけ型のマッピングを変える」ということができないため、id に @Column(columnDefinition = "INTEGER") をつけなきゃいけない
  • @GeneratedValue のデフォルトである strategy = GenerationType.AUTO だと、Hibernateのような「DBごとに適した方法」ではなく、「ID管理用テーブルを作って使う」方式が固定で選択され、しかもデフォルトを変更する方法がない。

等々、だめな部分が目立った。

で、あとで気づきましたが、既に Dialect は存在するようで

*1:created_at のフォーマットが違うのは、最初は自分で new Date() した値がそのまま使われているため Date 型のインスタンスだが、更新後はDBから取得した値なので Timestamp 型になるため

システム日付を安易に使ったプログラムのテスト

全くテストを意識しないで Calendar.getInstance() とか new Date() とかを使いまくるプログラムがあったとする。例えば、以下のようなものだ。

public void oomisoka() {
    Date date = new Date();
    if (date.getMonth() == 11 && date.getDate() == 31) {
        // 12月31日にのみ行う処理…
    }
}

せめて引数で Date を受け取るようになっていれば…という感じだが、こういったメソッドをテストするにはどうしたらいいだろう?

  • ユニットテスト実行前にシステム時刻を手動で変更
  • ユニットテスト内で date コマンドを実行してシステム日付を変更し、テスト終了後元に戻す
  • あきらめる

など様々な対処を見たが、JMockit などを使えば上記のような挑戦的プログラムも普通にテストができる。

たとえば、new Date() や Calendar.getInstance() で作られるインスタンスの年を2000年に変更してしまうには、以下のようにすれば良い*1

package com.example.mockdate;

import static junit.framework.Assert.assertEquals;

import java.util.Calendar;
import java.util.Date;

import mockit.Mock;
import mockit.MockUp;
import mockit.Mockit;

import org.junit.BeforeClass;
import org.junit.Test;

public class AppTest {

	@BeforeClass
	public static void setUpClass() {
		// 大抵不要だが一応
		// see: http://jmockit.googlecode.com/svn/trunk/www/tutorial/RunningTests.html
		Mockit.setUpMocks();
	}
	
	// staticメソッドのモックを作るにはクラス定義が必要?
	public static class MockCalendar {
		private static Calendar instance;
		
		public static void setInstance(Calendar desired) {
			instance = (Calendar)desired.clone();
		}
		
		@Mock
		public static Calendar getInstance() {
			return (Calendar)instance.clone();
		}
	}

	@Test
	public void testCalendar() {
		// MockCalendar.getInstance() が返す Calendar を設定
		Calendar desired = Calendar.getInstance();
		desired.set(Calendar.YEAR, 2000);
		MockCalendar.setInstance(desired);
		
		// Calendarクラスのモックとして MockCalendar を指定
		Mockit.setUpMock(Calendar.class, MockCalendar.class);
		
		// millennium now!
		assertEquals(2000, Calendar.getInstance().get(Calendar.YEAR));
	}

	@Test
	@SuppressWarnings({ "unused", "deprecation" })
	public void testMockDate() {
 		new MockUp<Date>() {
			Date it;
			
			// Date のデフォルトコンストラクタの差し替え
			// インスタンス変数 it で、コンストラクタが返す値を操作できる
			@Mock
			void $init() {
				it.setYear(100);
			}
		};

		// millennium again!
		assertEquals(100, new Date().getYear());
	}
}

*1:使い込んでいないためもっといい方法がある可能性は大

ヘッダ固定テーブルの最良解 Pushpin Header

HTMLで、ヘッダを固定してボディを縦方向にスクロール可能にしたテーブルを作る方法はいろいろあって、テーブル ヘッダ固定ぐぐると百花繚乱、群雄割拠という感じだけれども、個人的には http://codylindley.com/CSS/249/pushspin-header-a-simplified-data-grid-with-a-stationary-header に載っていた方法が

  • HTML+CSSのみで動作。JavaScript不要
  • IE6, IE7, IE8でも動作*1
  • tableタグとその中身は全く普通の構造。tableの分割もしない
  • 「ヘッダ部分だけを、CSSの絶対位置指定で無理矢理スクロール領域の外に固定する」と一行で説明できる単純で理解しやすい動作原理

と、ベストだと思う。

ところが、最近上記ページを見直してみたらなんか消えていて確認できなくなっていたので*2、記憶をたよりにやり方を再確認しておく。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>Pushpin Header test</title>
<style type="text/css">

body, table {
  font-size: 12px;
}

/* ---------- キモになる部分+枠線 ---------- */

/* 全体の枠。ヘッダが入る分だけ上部を空けておく */
.container {
  position: relative;
  padding-top: 20px;
  border: 1px solid red;
}

/* .container からヘッダのスペースを除いた部分。ここがスクロール対象 */
.content {
  overflow: auto;
}

.scrollable {
  border-collapse: collapse;
}

/* ヘッダ部分。位置を .container の左上端に移動 */
.scrollable thead tr {
  position: absolute;
  top: 0;
  left: 0;
}

/* このheightと .container の padding-top を合わせる */
.scrollable thead th {
  height: 20px;
  border-color: blue;
  border-style: solid;
  border-width: 0px 1px 1px 0px;
}

.scrollable tbody td {
  height: 20px;
  border-color: black;
  border-style: solid;
  border-width: 1px 1px 0px 0px;
}

/* ---------- 表ごとに異なる値はclassを分ける ---------- */

.container1 {
  width: 200px;
}

.content1 {
  width: 200px;
  height: 80px;
}

/* 各カラムにはwidth を設定する必要がある */
.table1 th, .table1 td {
  padding: 0 3px 0 3px;
  width: 80px;
}

.table1 th {
  background-color: #ccc;
}
</style>
</head>
<body>

<!-- 全体枠 -->
<div class="container container1">
<!-- スクロール領域 -->
<div class="content content1">
<table class="scrollable table1">
<thead>
  <tr><th>head1</th><th>head2</th></tr> 
</thead>
<tbody>
  <tr><td>body1</td><td>body2</td></tr>
  <tr><td>body1</td><td>body2</td></tr>
  <tr><td>body1</td><td>body2</td></tr>
  <tr><td>body1</td><td>body2</td></tr>
  <tr><td>body1</td><td>body2</td></tr>
</tbody>
</table>
</div>
</div>

</body>
</html>
  • IEでは、DOCTYPE等を書いて標準準拠モードで動かす必要あり
  • 細かい幅や枠線の調整ではIE/Firefox等で違いが出る場合あり。ただしこの手法自体とは関係ないCSSの一般的な問題なので自分で頑張る

*1:上記のサンプル自体はIE6, IE7では未確認。ただ、以前同様の方法を採ったときはIE6, IE7でも動作を確認したので、微調整すれば使えるはず。

*2:ブログ記事抜きの実例だけは http://codylindley.com/blogstuff/css/pushpin/pushpin.html に残っているっぽい。

ActiveScriptRuby で WAVE DASH 問題にハマる

Windows 7 に ActiveScriptRuby 1.8.7 をインストールして使っているんだけど、Oracle+JDBCではよくあるWAVE DASH問題的な問題にはまってしまった。

こんな環境で、

> ruby -v
ruby 1.8.7 (2010-01-10 patchlevel 249) [i386-mswin32]

UTF-8の文字列をShift JISに変換したいだけなんだけど、Iconvを使うと文字列の中に FULLWIDTH TILDE (U+FF5E) がある場合にエラーになってしまう。

require 'iconv'
Iconv.conv 'SHIFT_JIS', 'UTF-8', [0xff5e].pack('U')

Iconv::IllegalSequence: "\357\275\236"
        from (irb):7:in `conv'
        from (irb):7
        from :0

Macだとこちら d:id:kyut:20081229:1230519610 にあるように SHIFT_JIS ではなく CP932 を指定すれば良いみたいだけど、ActiveScriptRuby では CP932 を指定しても結果は同じ。どうも現在の ActiveScriptRuby に入っている iconv はバージョンが古く(1.9.1)、どこかのバージョンで取り込まれた(?)CP932 指定時の U+FF5E ⇒ 波ダッシュ への変換ができないらしい(手元の Snow Leopard は iconv 1.13 で、CP932 がうまく動く)。

どうしようもなさそうなので結局信頼と伝統のNKFを使うようにした*1

require 'nkf'
NKF.nkf '-sW8m0', [0xff5e].pack('U') # => "〜"

*1:単にコマンドライン丸出しのインタフェースが何となく好きになれないので Iconv にしていただけだったので

プログラマのための文字コード技術入門

プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

プログラマのための文字コード技術入門 (WEB+DB PRESS plus) (WEB+DB PRESS plusシリーズ)

もちろん内容は興味深く、楽しく読めたんだけど、こぼれ話的なものにも面白いものがあった。

  • 「妛」という幽霊文字は、もともと上が「山」で下が「女」という別の字を入れようと作業をしていたのが、2枚の紙に「山」と「女」を書いたものを貼りあわせて使っていたので、継ぎ目部分が線のように見えてしまいこんな字になったと推定されているらしい。
  • 北朝鮮文字コードKPS 9566では「金日成」「金正日」に当たるハングル6文字が他のハングルより前の特別な位置に割り当てられており、ソートすると最初に来るようになっている。さらには後継者を想定してか、金正日の後ろに空きがある*1Webで確認したら(4-72 から 4-77 までが該当)、あと5代は安泰のようだ。