src/tools/StringComparator.js
/**
* The script is part of Mojix.
*
* AUTHOR:
* natade (http://twitter.com/natadea)
*
* LICENSE:
* The MIT license https://opensource.org/licenses/MIT
*/
import Japanese from "../language/Japanese.js";
import Unicode from "../encode/Unicode.js";
/**
* 文字列比較関数を作成用のツールクラス
* @ignore
*/
class ComparatorTool {
/**
* 文字列の揺れを除去し正規化します。
* @param {string} string_data - 正規化したいテキスト
* @returns {string} 正規化後のテキスト
*/
static toNormalizeString(string_data) {
let normalize_text = null;
// NORM_IGNOREWIDTH 半角全角区別しない(半角英数記号と全角カタカナに統一)
normalize_text = Japanese.toHalfWidthAsciiCode(Japanese.toHalfWidthAsciiCode(string_data));
// LCMAP_LOWERCASE 半角に統一
normalize_text = normalize_text.toLowerCase();
// NORM_IGNOREKANATYPE ひらがなとカタカナを区別しない
normalize_text = Japanese.toHiragana(normalize_text);
// NORM_IGNORENONSPACE 簡単に場所をとらない記号を削除
normalize_text = normalize_text.replace(/[゛゜]/g, "");
// NORM_IGNORESYMBOLS 英文法の記号を無視
normalize_text = normalize_text.replace(/["'-]/g, "");
return normalize_text;
}
/**
* ASCIIコードが半角数値かどうかを判定する
* @param {number} string_number - ASCIIコード
* @returns {boolean} 数値ならTRUE
*/
static isNumberAscii(string_number) {
const ASCII_0 = 0x30;
const ASCII_9 = 0x39;
return ASCII_0 <= string_number && string_number <= ASCII_9;
}
/**
* ASCIIコード配列の中で指定した位置から数値が何バイト続くか
* @param {number[]} string_number_array - ASCIIコードの配列
* @param {number} offset - どの位置から調べるか
* @returns {number} 数値ならTRUE
*/
static getNumberAsciiLength(string_number_array, offset) {
for (let i = offset; i < string_number_array.length; i++) {
if (!ComparatorTool.isNumberAscii(string_number_array[i])) {
return i - offset;
}
}
return string_number_array.length - offset;
}
/**
* ASCIIコード配列の中の指定した位置にある数値データ同士をCompareする
* @param {number[]} t1 - ASCIIコードの配列(比較元)
* @param {number} t1off - どの位置から調べるか
* @param {number} t1size - 調べるサイズ
* @param {number[]} t2 - ASCIIコードの配列(比較先)
* @param {number} t2off - どの位置から調べるか
* @param {number} t2size - 調べるサイズ
* @returns {number} Compare結果
*/
static compareNumber(t1, t1off, t1size, t2, t2off, t2size) {
const ASCII_0 = 0x30;
const t1end = t1off + t1size;
const t2end = t2off + t2size;
// 前方から調査
let t1p = t1off;
let t2p = t2off;
// 先頭の0は飛ばして比較したい
// 0以外の数値がどこに含まれているか調査
for (; t1p < t1end; t1p++) {
if (t1[t1p] !== ASCII_0) {
break;
}
}
for (; t2p < t2end; t2p++) {
if (t2[t2p] !== ASCII_0) {
break;
}
}
// 0しかなかった場合
if (t1p == t1end || t2p == t2end) {
if (t1p != t1end) {
//t2pのみはみだした == 0
return 1;
} else if (t2p != t2end) {
return -1;
} else {
return 0;
}
}
// 桁数のみでどちらが大きいか比較
const t1keta = t1size - (t1p - t1off);
const t2keta = t2size - (t2p - t2off);
if (t1keta > t2keta) {
return 1;
} else if (t1keta < t2keta) {
return -1;
}
// 同じ桁同士の比較
for (; t1p < t1end;) {
if (t1[t1p] > t2[t2p]) {
return 1;
} else if (t1[t1p] < t2[t2p]) {
return -1;
}
t1p++;
t2p++;
}
return 0;
}
/**
* ASCIIコード配列の同士をCompareする
* @param {number[]} t1 - ASCIIコードの配列(比較元)
* @param {number[]} t2 - ASCIIコードの配列(比較先)
* @returns {number} Compare結果
*/
static compareText(t1, t2) {
const l1 = t1.length;
const l2 = t2.length;
if (l1 === 0 && l2 === 0) {
return 0;
}
if (l1 === 0) {
return -1;
}
if (l2 === 0) {
return 1;
}
//この地点で両方とも長さが1より大きい
let t1off = 0;
let t2off = 0;
while (t1off <= l1 && t2off <= l2) {
const t1isnum = ComparatorTool.isNumberAscii(t1[t1off]);
const t2isnum = ComparatorTool.isNumberAscii(t2[t2off]);
//文字目の種類が違う
if (t1isnum !== t2isnum) {
if (t1isnum) {
return -1; //数値が前
} else {
return 1; //文字が後ろ
}
}
if (t1isnum) {
//両方とも数値
const t1size = ComparatorTool.getNumberAsciiLength(t1, t1off);
const t2size = ComparatorTool.getNumberAsciiLength(t2, t2off);
const r = ComparatorTool.compareNumber(t1, t1off, t1size, t2, t2off, t2size);
if (r !== 0) {
return r;
}
t1off += t1size;
t2off += t2size;
} else {
//両方とも文字列
if (t1[t1off] > t2[t2off]) {
return 1;
} else if (t1[t1off] < t2[t2off]) {
return -1;
}
t1off++;
t2off++;
}
if (t1off >= l1 && t2off >= l2) {
//両方ともオーバー
//長さも等しい
if (l1 === l2) {
return 0;
} else if (l1 > l2) {
return 1;
} else {
return -1;
}
} else if (t2off >= l2) {
//片方のほうがサイズが大きい
//t2の方が短い
return 1;
} else if (t1off >= l1) {
//t1の方が短い
return -1;
}
}
// ※ここには達成しない
return 0;
}
/**
* 2つの文字列を比較する
*
* @param {any} a - 比較元
* @param {any} b - 比較先
* @returns {number} Compare結果
*/
static compareToForDefault(a, b) {
if (a === b) {
return 0;
}
if (typeof a === typeof b) {
return a < b ? -1 : 1;
}
return typeof a < typeof b ? -1 : 1;
}
/**
* 2つの文字列を自然順に比較を行う(自然順ソート(Natural Sort)用)
* - 入力引数は文字列化して比較します
*
* @param {any} a - 比較元
* @param {any} b - 比較先
* @returns {number} Compare結果
*/
static compareToForNatural(a, b) {
if (a.toString === undefined || b.toString === undefined) {
return 0;
}
const a_str = Unicode.toUTF16Array(ComparatorTool.toNormalizeString(a.toString()));
const b_str = Unicode.toUTF16Array(ComparatorTool.toNormalizeString(b.toString()));
return ComparatorTool.compareText(a_str, b_str);
}
}
/**
* 日本語の文字列比較用の関数
* - sortの引数で利用できます
* @ignore
*/
const stringComparator = {
/**
* 2つの文字列を比較する関数
* @type {function(any, any): number}
*/
DEFAULT: ComparatorTool.compareToForDefault,
/**
* 2つの文字列を自然順ソートで比較する関数
* - 入力引数は文字列化して比較します
*
* @type {function(any, any): number}
*/
NATURAL: ComparatorTool.compareToForNatural
};
export default stringComparator;