JavaScriptとJavaで、UNICODE補助文字に対応した文字列操作を行う

JIS X 0213ではUNICODEの補助文字が一部で使われています。VistaやMac OSX、またはJIS2004対応フォントをインストールしたXpなど、この補助文字が入力可能な環境はこれから徐々に増えていくと思われます。結局、いつかはシステム側も対応しなくてはいけなくなるでしょう。だったら今からプログラマーUNICODE補助文字を意識した文字列操作を記述するよう心がけておくべきなのではないかと考えます。データベースとロジック側が対応しておけば、後は補助文字の利用可否はフィルター等で別途管理すればいい訳ですから。
というわけで、自分がよく使う言語であるJavaJavaScriptでの文字列操作を確認してみました(とりあえず動けばいいというレベルですが・・・)。まずはJavaScriptから

util.js

var StringUtil = {};
StringUtil.length = function(str) {
	var ret = 0;
	for (var i = 0; i < str.length; i++,ret++) {
		var x = str.charCodeAt(i);
		var y = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
		if(StringUtil.checkSurrogate(x, y)) {
			i++;
		}
	}
	return ret;
};

StringUtil.subString = function(str, from, to) {
	var ret = "";
	for (var i = 0, length = 0; i < str.length; i++,length++) {
		var x = str.charCodeAt(i);
		var y = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
		var s = "";
		if(StringUtil.checkSurrogate(x, y)) {
			i++;
			s = String.fromCharCode(x, y);
		} else {
			s = String.fromCharCode(x);
		}
		if (from <= length && length < to) {
			ret = ret + s;
		}
		
	}
	return ret;
	
};

StringUtil.charAt = function(str, index) {
	for (var i = 0, length = 0; i < str.length; i++, length++) {
		var flg = length == index;
		var x = str.charCodeAt(i);
		var y = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
		var s = "";
		if(StringUtil.checkSurrogate(x, y)) {
			i++;
			s = String.fromCharCode(x, y);
		} else {
			s = String.fromCharCode(x);
		}
		if (flg) {
			return s;
		}
	}
	return null;
};

StringUtil.checkSurrogate = function(x, y) {
	return 0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF;
};

String#charCodeAtメソッドを使えば、UTF-16として操作可能なようですね。これと同じロジックを使って、Javaも実装してみます。

StrintUtil.java

package test;


public class StringUtil {
	
	public static int length(String str) {
		int res = 0;
		if (str != null) {
			for (int i = 0; i < str.length(); i++) {
				char c = str.charAt(i);
				char d = i + 1 < str.length() ? str.charAt(i + 1) : 0;
				if (StringUtil.checkSurrogate(c, d)) {
					i++;
				}
				res++;
			}
		}
		return res;
	}
	
	public static String substring(String str, int beginIndex, int endIndex) {
		StringBuffer ret = new StringBuffer();
		for (int i = 0, length = 0; i < str.length(); i++,length++) {
			char x = str.charAt(i);
			char y = i + 1 < str.length() ? str.charAt(i + 1) : 0;
			String add = null;
			if(StringUtil.checkSurrogate(x, y)) {
				i++;
				add = String.valueOf(new char[]{x, y});
			} else {
				add = String.valueOf(x);
			}
			if (beginIndex <= length && length < endIndex) {
				ret.append(add);
			}
			
		}
		return ret.toString();
	}
	
	public static String charAt(String str, int index) {
		for (int i = 0, length = 0; i < str.length(); i++, length++) {
			boolean flg = length == index;
			char x = str.charAt(i);
			char y = i + 1 < str.length() ? str.charAt(i + 1) : 0;
			String s = null;
			if(StringUtil.checkSurrogate(x, y)) {
				i++;
				s = String.valueOf(new char[]{x, y});
			} else {
				s = String.valueOf(x);
			}
			if (flg) {
				return s;
			}
		}
		return null;
	}
	
	public static boolean checkSurrogate(char x, char y) {
		return '\uD800' <= x && x <= '\uDBFF' && '\uDC00' <= y && y <= '\uDFFF';
	}
}

では、これを使ったテスト。JavaのWebアプリのルートとなるHTMLを作成

index.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>UNICODE補助文字対応</title>
<script type="text/javascript" src="./js/jquery.js"></script>
<script type="text/javascript" src="./js/util.js"></script>
<script type="text/javascript">

$(function() {
	$("#lgth").click(function() {
		var lang = $("#lang").val();
		var str = $("#str").val();
		if (lang == "JavaScript") {
			$("#label").html(StringUtil.length(str));
		} else if (lang == "Java") {
			$("#label").load("./length", {str: str});
		}
	});
	$("#substr").click(function() {
		var lang = $("#lang").val();
		var str = $("#str").val();
		var from = $("#from").val();
		var to = $("#to").val();
		if (lang == "JavaScript") {
			$("#label").html(StringUtil.subString(str, from, to));
		} else if (lang == "Java") {
			$("#label").load("./substring", {str: str, from: from, to: to});
		}
	});
	$("#charAt").click(function() {
		var lang = $("#lang").val();
		var str = $("#str").val();
		var index = $("#index").val();
		if (lang == "JavaScript") {
			$("#label").html(StringUtil.charAt(str, index));
		} else if (lang == "Java") {
			$("#label").load("./charat", {str: str, index: index});
		}
	});
});
</script>
</head>
<body>
<form>
<select id="lang">
	<option value="JavaScript">JavaScript</option>
	<option value="Java">Java</option>
</select><br>
<input type="text" id="str">
<input type="button" id="lgth" value="length"><br>
from:<input type="text" id="from">
to:<input type="text" id="to">
<input type="button" id="substr" value="substring"><br>
index:<input type="text" id="index">
<input type="button" id="charAt" value="charAt"><br>
</form>
<div id="label"></div>
</body>
</html>

jQueryを使ってみました。SELECTボックスでJavaScriptを選べばutil.jsで定義したStringUtilの関数を利用します。Javaを選択するとAjaxでサーバから結果を取得します。
ではサーバ側のServlet

LengthServlet.java

package test;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LengthServlet extends HttpServlet {

	/**
	 * 
	 */
	private static final long serialVersionUID = -4117595403170112041L;

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		request.setCharacterEncoding("UTF-8");
		String str = request.getParameter("str");
		int res = StringUtil.length(str);
		
		response.setContentType("text/plain; charset=UTF-8");
		response.getWriter().print(res + ":" + str);
	}

}

SubstringServlet.java

package test;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SubstringServlet extends HttpServlet {

	/**
	 * 
	 */
	private static final long serialVersionUID = -5955352056333838158L;
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		request.setCharacterEncoding("UTF-8");
		String str = request.getParameter("str");
		String from = request.getParameter("from");
		String to = request.getParameter("to");
		String substr = StringUtil.substring(str,Integer.parseInt(from), Integer.parseInt(to));
		
		response.setContentType("text/plain; charset=UTF-8");
		response.getWriter().print(substr + ":" + str);
	}

}

CharAtServlet.java

package test;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CharAtServlet extends HttpServlet {

	/**
	 * 
	 */
	private static final long serialVersionUID = -5505794727739592392L;
	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		request.setCharacterEncoding("UTF-8");
		String str = request.getParameter("str");
		String index = request.getParameter("index");
		String charAt = StringUtil.charAt(str, Integer.parseInt(index));
		
		response.setContentType("text/plain; charset=UTF-8");
		response.getWriter().print(charAt + ":" + str);
	}
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	id="WebApp_ID" version="2.5">
	<display-name>surrogate</display-name>
	<servlet>
		<servlet-name>LengthServlet</servlet-name>
		<servlet-class>test.LengthServlet</servlet-class>
	</servlet>

	<servlet>
		<servlet-name>SubstringServlet</servlet-name>
		<servlet-class>test.SubstringServlet</servlet-class>
	</servlet>

	<servlet>
		<servlet-name>CharAtServlet</servlet-name>
		<servlet-class>test.CharAtServlet</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>LengthServlet</servlet-name>
		<url-pattern>/length</url-pattern>
	</servlet-mapping>

	<servlet-mapping>
		<servlet-name>SubstringServlet</servlet-name>
		<url-pattern>/substring</url-pattern>
	</servlet-mapping>

	<servlet-mapping>
		<servlet-name>CharAtServlet</servlet-name>
		<url-pattern>/charat</url-pattern>
	</servlet-mapping>
</web-app>

・・・適当な作りですが、これで一応UNICODE補助文字をJavaScriptでもJavaでも文字列操作できることを確認できました。これからは、Stringクラスのlengthやsubstringを使わずに、こういったユーティリティ関数を使う必要がありそうです。ちなみにJava5からはUNICODE補助文字対応用のメソッドもいくつか追加されてますが、lengthやsubstringのような便利メソッドは提供されていません。ここら辺が、如何にもユーザに優しくないJavaらしい対応です(苦笑)
・・・ちなみに、プログラム言語だけではなく、データベース側の文字列操作関数も、UNICODE補助文字に対応しているかどうかの確認が必要です。OraclePostgreSQLは大丈夫らしいですが、MySQLはそもそも補助文字を保存することすら出来ないし、他のDBも確認が必要な模様です。例えば、H2 Databaseは補助文字の保存は大丈夫ですが、LENGTHやSUBSTRINGは補助文字を2文字とカウントしてしまい、おかしな結果を返しました。