Home Reference Source

src/encode/SJIS.js

/**
 * The script is part of MojiJS.
 * 
 * AUTHOR:
 *  natade (http://twitter.com/natadea)
 * 
 * LICENSE:
 *  The MIT license https://opensource.org/licenses/MIT
 */

import Unicode from "./Unicode.js";

/**
 * 面区点情報
 * @typedef {Object} MenKuTen
 * @property {string} [text] 面-区-点
 * @property {number} [men=1] 面
 * @property {number} ku 区
 * @property {number} ten 点
 */

/**
 * Shift_JIS を扱うクラス
 * @ignore
 */
export default class SJIS {

	/**
	 * 文字列を Shift_JIS の配列に変換
	 * @param {String} text - 変換したいテキスト
	 * @param {Object<number, number>} unicode_to_sjis - Unicode から Shift_JIS への変換マップ
	 * @returns {Array<number>} Shift_JIS のデータが入った配列
	 * @ignore
	 */
	static toSJISArray(text, unicode_to_sjis) {
		const map = unicode_to_sjis;
		const utf32 = Unicode.toUTF32Array(text);
		const sjis = [];
		const ng = "?".charCodeAt(0);
		for(let i = 0; i < utf32.length; i++) {
			const map_bin = map[utf32[i]];
			if(map_bin) {
				sjis.push(map_bin);
			}
			else {
				sjis.push(ng);
			}
		}
		return sjis;
	}

	/**
	 * 文字列を Shift_JIS のバイナリ配列に変換
	 * - 日本語文字は2バイトとして、配列も2つ分、使用します。
	 * @param {String} text - 変換したいテキスト
	 * @param {Object<number, number>} unicode_to_sjis - Unicode から Shift_JIS への変換マップ
	 * @returns {Array<number>} Shift_JIS のデータが入ったバイナリ配列
	 * @ignore
	 */
	static toSJISBinary(text, unicode_to_sjis) {
		const sjis = SJIS.toSJISArray(text, unicode_to_sjis);
		const sjisbin = [];
		for(let i = 0; i < sjis.length; i++) {
			if(sjis[i] < 0x100) {
				sjisbin.push(sjis[i]);
			}
			else {
				sjisbin.push(sjis[i] >> 8);
				sjisbin.push(sjis[i] & 0xFF);
			}
		}
		return sjisbin;
	}

	/**
	 * SJISの配列から文字列に変換
	 * @param {Array<number>} sjis - 変換したいテキスト
	 * @param {Object<number, number|Array<number>>} sjis_to_unicode - Shift_JIS から Unicode への変換マップ
	 * @returns {String} 変換後のテキスト
	 * @ignore
	 */
	static fromSJISArray(sjis, sjis_to_unicode) {
		const map = sjis_to_unicode;
		const utf16 = [];
		const ng = "?".charCodeAt(0);
		for(let i = 0; i < sjis.length; i++) {
			let x = sjis[i];
			/**
			 * @type {number|Array<number>}
			 */
			let y = [];
			if(x >= 0x100) {
				// すでに1つの変数にまとめられている
				y = map[x];
			}
			else {
				// 2バイト文字かのチェック
				if( ((0x81 <= x) && (x <= 0x9F)) || ((0xE0 <= x) && (x <= 0xFC)) ) {
					x <<= 8;
					i++;
					x |= sjis[i];
					y = map[x];
				}
				else {
					y = map[x];
				}
			}
			if(y) {
				// 配列なら配列を結合
				// ※ Unicodeの結合文字の可能性があるため
				if(y instanceof Array) {
					for(let j = 0; j < y.length; j++) {
						utf16.push(y[j]);
					}
				}
				// 値しかない場合は値を結合
				else {
					utf16.push(y);
				}
			}
			else {
				utf16.push(ng);
			}
		}
		return Unicode.fromUTF32Array(utf16);
	}

	/**
	 * 指定したコードポイントの文字から Shift_JIS 上の符号化数値に変換
	 * @param {Number} unicode_codepoint - Unicodeのコードポイント
	 * @param {Object<number, number>} unicode_to_sjis - Unicode から Shift_JIS への変換マップ
	 * @returns {Number} 符号化数値(変換できない場合はnullとなる)
	 * @ignore
	 */
	static toSJISCodeFromUnicode(unicode_codepoint, unicode_to_sjis) {
		if(!unicode_to_sjis[unicode_codepoint]) {
			return null;
		}
		const utf16_text = Unicode.fromUTF32Array([unicode_codepoint]);
		const sjis_array = SJIS.toSJISArray(utf16_text, unicode_to_sjis);
		return sjis_array[0];
	}

	/**
	 * 指定した Shift_JIS-2004 のコードから面区点番号に変換
	 * @param {Number} sjis_code - Shift_JIS-2004 のコードポイント
	 * @returns {MenKuTen} 面区点番号(存在しない場合(1バイトのJISコードなど)はnullを返す)
	 */
	static toMenKuTenFromSJIS2004Code(sjis_code) {
		if(!sjis_code) {
			return null;
		}
		const x = sjis_code;
		if(x < 0x100) {
			return null;
		}
		// アルゴリズムは面区点番号表からリバースエンジニアリング

		let s1 = x >> 8;
		let s2 = x & 0xFF;
		let men = 0;
		let ku = 0;
		let ten = 0;

		// 面情報の位置判定
		if(s1 < 0xF0) {
			men = 1;
			// 区の計算方法の切り替え
			// 63区から、0x9F→0xE0に飛ぶ
			if(s1 < 0xE0) {
				s1 = s1 - 0x81;
			}
			else {
				s1 = s1 - 0xC1;
			}
		}
		else {
			// ※2面は第4水準のみ
			men = 2;
			// 2面1区 ~ 2面8区
			if((((s1 === 0xF0) || (s1 === 0xF2)) && (s2 < 0x9F)) || (s1 === 0xF1)) {
				s1 = s1 - 0xF0;
			}
			// 2面12区 ~ 2面15区
			else if(((s1 === 0xF4) && (s2 < 0x9F)) || (s1 < 0xF4)) {
				s1 = s1 - 0xED;
			}
			// 2面78区 ~ 2面94区
			else {
				s1 = s1 - 0xCE;
			}
		}

		// 区情報の位置判定
		if(s2 < 0x9f) {
			ku = s1 * 2 + 1;
			// 点情報の計算方法の切り替え
			// 0x7Fが欠番のため「+1」を除去
			if(s2 < 0x80) {
				s2 = s2 - 0x40 + 1;
			}
			else {
				s2 = s2 - 0x40;
			}
		}
		else {
			ku = s1 * 2 + 2;
			s2 = s2 - 0x9f + 1;
		}

		// 点情報の位置判定
		ten = s2;

		return {
			text : "" + men + "-" + ku + "-" + ten,
			men : men,
			ku : ku,
			ten : ten
		};
	}

	/**
	 * 指定したコードポイントの文字から Shift_JIS-2004 上の面区点番号に変換
	 * @param {Number} unicode_codepoint - Unicodeのコードポイント
	 * @param {Object<number, number>} unicode_to_sjis - Unicode から Shift_JIS-2004 への変換マップ
	 * @returns {MenKuTen} 面区点番号(存在しない場合(1バイトのJISコードなど)はnullを返す)
	 * @ignore
	 */
	static toMenKuTenFromUnicode(unicode_codepoint, unicode_to_sjis) {
		if(!unicode_to_sjis[unicode_codepoint]) {
			return null;
		}
		const x = SJIS.toSJISCodeFromUnicode(unicode_codepoint, unicode_to_sjis);
		return SJIS.toMenKuTenFromSJIS2004Code(x);
	}
	
	/**
	 * 指定した面区点番号から Shift_JIS-2004 コードに変換
	 * @param {MenKuTen|string} menkuten - 面区点番号(面が省略された場合は、1とみなす)
	 * @returns {Number} Shift_JIS-2004 のコードポイント(存在しない場合はnullを返す)
	 */
	static toSJIS2004CodeFromMenKuTen(menkuten) {
		let m = null, k = null, t = null;
		let text = null;
		if(menkuten instanceof Object) {
			if(menkuten.text && (typeof menkuten.text === "string")) {
				text = menkuten.text;
			}
			else if((menkuten.ku) && (menkuten.ten)) {
				m = menkuten.men ? menkuten.men : 1;
				k = menkuten.ku;
				t = menkuten.ten;
			}
		}
		else  if((typeof menkuten === "string")) {
			text = menkuten;
		}
		if(text) {
			const strmkt = text.split("-");
			if(strmkt.length === 3) {
				m = parseInt(strmkt[0], 10);
				k = parseInt(strmkt[1], 10);
				t = parseInt(strmkt[2], 10);
			}
			else if(strmkt.length === 2) {
				m = 1;
				k = parseInt(strmkt[0], 10);
				t = parseInt(strmkt[1], 10);
			}
		}
		if(!m || !k || !t) {
			throw "IllegalArgumentException";
		}

		let s1 = -1;
		let s2 = -1;

		/**
		 * @type {Object<number, number>}
		 */
		const kmap = {1:1,3:1,4:1,5:1,8:1,12:1,13:1,14:1,15:1};

		// 参考
		// 2019/1/1 Shift JIS - Wikipedia
		// https://en.wikipedia.org/wiki/Shift_JIS
		//
		// 区や点の判定部分は、通常94までであるため、正確にはkやtは <=94 とするべき。
		// しかし、Shift_JIS範囲外(IBM拡張漢字)でも利用されるため制限を取り払っている。

		if(m === 1) {
			if((1 <= k) && (k <= 62)) {
				s1 = Math.floor((k + 257) / 2);
			}
			else if(63 <= k) {
				s1 = Math.floor((k + 385) / 2);
			}
		}
		else if(m === 2) {
			if(kmap[k]) {
				s1 = Math.floor((k + 479) / 2) - (Math.floor(k / 8) * 3);
			}
			else if(78 <= k) {
				s1 = Math.floor((k + 411) / 2);
			}
		}

		if((k % 2) === 1) {
			if((1 <= t) && (t <= 63)) {
				s2 = t + 63;
			}
			else if(64 <= t) {
				s2 = t + 64;
			}
		}
		else {
			s2 = t + 158;
		}

		if((s1 === -1) || (s2 === -1)) {
			return null;
		}
		return (s1 << 8) | s2;
	}
	
	/**
	 * 指定した面区点番号から Unicode コードポイントに変換
	 * @param {MenKuTen|string} menkuten - 面区点番号
	 * @param {Object<number, number|Array<number>>} sjis_to_unicode - Shift_JIS-2004 から Unicode への変換マップ
	 * @returns {Array<number>} UTF-32の配列(存在しない場合はnullを返す)
	 * @ignore
	 */
	static toUnicodeCodeFromMenKuTen(menkuten, sjis_to_unicode) {
		const sjis_code = SJIS.toSJIS2004CodeFromMenKuTen(menkuten);
		if(!sjis_code) {
			return null;
		}
		const unicode = sjis_to_unicode[sjis_code];
		if(!unicode) {
			return null;
		}
		if(unicode instanceof Array) {
			return unicode;
		}
		else {
			return [unicode];
		}
	}

	/**
	 * 指定した Shift_JIS のコードから区点番号に変換
	 * @param {Number} sjis_code - Shift_JIS のコードポイント
	 * @returns {MenKuTen} 区点番号(存在しない場合(1バイトのJISコードなど)はnullを返す)
	 */
	static toKuTenFromSJISCode(sjis_code) {
		if(!sjis_code) {
			return null;
		}
		const x = sjis_code;
		if(x < 0x100) {
			return null;
		}
		// アルゴリズムは区点番号表からリバースエンジニアリング

		let s1 = x >> 8;
		let s2 = x & 0xFF;
		let ku = 0;
		let ten = 0;

		// 区の計算方法の切り替え
		// 63区から、0x9F→0xE0に飛ぶ
		if(s1 < 0xE0) {
			s1 = s1 - 0x81;
		}
		else {
			s1 = s1 - 0xC1;
		}

		// 区情報の位置判定
		if(s2 < 0x9f) {
			ku = s1 * 2 + 1;
			// 点情報の計算方法の切り替え
			// 0x7Fが欠番のため「+1」を除去
			if(s2 < 0x80) {
				s2 = s2 - 0x40 + 1;
			}
			else {
				s2 = s2 - 0x40;
			}
		}
		else {
			ku = s1 * 2 + 2;
			s2 = s2 - 0x9f + 1;
		}

		// 点情報の位置判定
		ten = s2;

		return {
			text : ku + "-" + ten,
			men : 1,
			ku : ku,
			ten : ten
		};
	}
	
	/**
	 * 指定したコードポイントの文字から Shift_JIS 上の面区点番号に変換
	 * @param {Number} unicode_codepoint - Unicodeのコードポイント
	 * @param {Object<number, number>} unicode_to_sjis - Unicode から Shift_JIS への変換マップ
	 * @returns {MenKuTen} 面区点番号(存在しない場合(1バイトのJISコードなど)はnullを返す)
	 * @ignore
	 */
	static toKuTenFromUnicode(unicode_codepoint, unicode_to_sjis) {
		if(!unicode_to_sjis[unicode_codepoint]) {
			return null;
		}
		const x = SJIS.toSJISCodeFromUnicode(unicode_codepoint, unicode_to_sjis);
		return SJIS.toKuTenFromSJISCode(x);
	}

	/**
	 * 指定した面区点番号/区点番号から Shift_JIS コードに変換
	 * @param {MenKuTen|string} kuten - 面区点番号/区点番号
	 * @returns {Number} Shift_JIS のコードポイント(存在しない場合はnullを返す)
	 */
	static toSJISCodeFromKuTen(kuten) {
		// 1~94区まで存在しているため面句点変換用で流用可能。
		// ただ、CP932の場合、範囲外の区、115区〜119区にIBM拡張文字が存在している。
		// 今回、toSJIS2004CodeFromMenKuTenでは区の範囲チェックをしないため問題なし。
		return SJIS.toSJIS2004CodeFromMenKuTen(kuten);
	}
	
	/**
	 * 指定した区点番号から Unicode コードポイントに変換
	 * @param {MenKuTen|string} kuten - 区点番号
	 * @param {Object<number, number|Array<number>>} sjis_to_unicode - Shift_JIS から Unicode への変換マップ
	 * @returns {Array<number>} UTF-32の配列(存在しない場合はnullを返す)
	 * @ignore
	 */
	static toUnicodeCodeFromKuTen(kuten, sjis_to_unicode) {
		const sjis_code = SJIS.toSJISCodeFromKuTen(kuten);
		if(!sjis_code) {
			return null;
		}
		const unicode = sjis_to_unicode[sjis_code];
		if(!unicode) {
			return null;
		}
		if(unicode instanceof Array) {
			return unicode;
		}
		else {
			return [unicode];
		}
	}

	/**
	 * Shift_JIS のコードポイントからJIS漢字水準(JIS Chinese character standard)に変換
	 * @param {Number} sjis_code - Shift_JIS-2004 のコードポイント
	 * @returns {Number} -1...変換不可, 0...水準なし, 1...第1水準, ...
	 */
	static toJISKanjiSuijunFromSJISCode(sjis_code) {
		if(!sjis_code) {
			return 0;
		}
		const menkuten = SJIS.toMenKuTenFromSJIS2004Code(sjis_code);
		// アルゴリズムはJIS漢字一覧表からリバースエンジニアリング
		if(!menkuten) {
			return 0;
		}
		// 2面は第4水準
		if(menkuten.men > 1) {
			return 4;
		}
		// 1面は第1~3水準
		if(menkuten.ku < 14) {
			// 14区より小さいと非漢字
			return 0;
		}
		if(menkuten.ku < 16) {
			// 14区と15区は第3水準
			return 3;
		}
		if(menkuten.ku < 47) {
			return 1;
		}
		// 47区には、第1水準と第3水準が混じる
		if(menkuten.ku === 47) {
			if(menkuten.ten < 52) {
				return 1;
			}
			else {
				return 3;
			}
		}
		if(menkuten.ku < 84) {
			return 2;
		}
		// 84区には、第2水準と第3水準が混じる
		if(menkuten.ku === 84) {
			if(menkuten.ten < 7) {
				return 2;
			}
			else {
				return 3;
			}
		}
		// 残り94区まで第3水準
		if(menkuten.ku < 95) {
			return 3;
		}
		return 0;
	}

	/**
	 * Unicode のコードポイントからJIS漢字水準(JIS Chinese character standard)に変換
	 * @param {Number} unicode_codepoint - Unicodeのコードポイント
	 * @param {Object<number, number>} unicode_to_sjis - Unicode から Shift_JIS への変換マップ
	 * @returns {Number} -1...変換不可, 0...水準なし, 1...第1水準, ...
	 * @ignore
	 */
	static toJISKanjiSuijunFromUnicode(unicode_codepoint, unicode_to_sjis) {
		if(!unicode_to_sjis[unicode_codepoint]) {
			return -1;
		}
		const x = SJIS.toSJISCodeFromUnicode(unicode_codepoint, unicode_to_sjis);
		return SJIS.toJISKanjiSuijunFromSJISCode(x);
	}

	/**
	 * 指定した面区点番号から Shift_JIS の仕様上、正規な物か判定
	 * @param {MenKuTen|string} menkuten - 面区点番号(面が省略された場合は、1とみなす)
	 * @returns {Boolean} 正規なデータは true, 不正なデータは false
	 */
	static isRegularMenKuten(menkuten) {
		let m, k, t;

		// 引数のテスト
		if(menkuten instanceof Object) {
			m = menkuten.men ? menkuten.men : 1;
			k = menkuten.ku;
			t = menkuten.ten;
		}
		else if(typeof menkuten === "string") {
			const strmkt = menkuten.split("-");
			if(strmkt.length === 3) {
				m = parseInt(strmkt[0], 10);
				k = parseInt(strmkt[1], 10);
				t = parseInt(strmkt[2], 10);
			}
			else if(strmkt.length === 2) {
				m = 1;
				k = parseInt(strmkt[0], 10);
				t = parseInt(strmkt[1], 10);
			}
			else {
				return false;
			}
		}
		else {
			return false;
		}

		/**
		 * @type {Object<number, number>}
		 */
		const kmap = {1:1,3:1,4:1,5:1,8:1,12:1,13:1,14:1,15:1};
		if(m === 1) {
			// 1面は1-94区まで存在
			if(!((1 <= k) && (k <= 94))) {
				return false;
			}
		}
		else if(m === 2) {
			// 2面は、1,3,4,5,8,12,13,14,15,78-94区まで存在
			if(!((kmap[k]) || ((78 <= k) && (k <= 94)))) {
				return false;
			}
		}
		else {
			// 面が不正
			return false;
		}
		// 点は1-94点まで存在
		if(!((1 <= t) && (t <= 94))) {
			return false;
		}
		return true;
	}

}