単語マッチングの最適化

This commit is contained in:
tateisu 2018-01-04 14:13:00 +09:00
parent fc3b8d9555
commit cb01e80880
3 changed files with 56 additions and 96 deletions

View File

@ -1,63 +1,43 @@
package jp.juggler.subwaytooter.util; package jp.juggler.subwaytooter.util;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.util.SparseArrayCompat; import android.util.SparseIntArray;
import java.util.Locale; import java.util.Locale;
public class CharacterGroup { public class CharacterGroup {
// Tokenizerが終端に達したことを示す // 文字列からグループIDを調べるマップ
static final int END = - 1;
// 同じと見なす文字列の集合
static class Group {
// 文字列の配列
@NonNull final String[] list;
// 代表する文字のコード
final int id;
Group( @NonNull String[] list ){
this.list = list;
this.id = findGroupId();
}
private int findGroupId(){
// グループのIDはグループ中の文字(長さ1)のunicodeのどれか
for( String s : list ){
if( s.length() == 1 ){
return s.charAt( 0 );
}
}
throw new RuntimeException( "group has not id!!" );
}
}
// 文字数1: unicode => group_id
private final SparseIntArray map1 = new SparseIntArray();
// テキスト中に出現した文字列からグループを探すためのマップ // 文字数2: unicode 二つを合成した数値 => group_id半角カナ濁音など
private final SparseIntArray map2 = new SparseIntArray();
// キー文字数1 // グループのIDはグループ中の文字(長さ1)のどれかのunicode
private final SparseArrayCompat< Group > map1 = new SparseArrayCompat<>(); private static int findGroupId( @NonNull String[] list ){
for( String s : list ){
// キー文字数2 if( s.length() == 1 ){
private final SparseArrayCompat< Group > map2 = new SparseArrayCompat<>(); return s.charAt( 0 );
}
}
throw new RuntimeException( "group has not id!!" );
}
// グループをmapに登録する // グループをmapに登録する
private void addGroup( @NonNull String[] list ){ private void addGroup( @NonNull String[] list ){
// グループを生成 int group_id = findGroupId( list );
Group g = new Group( list );
// 含まれる各文字列をマップに登録する // 文字列からグループIDを調べるマップを更新
for( String s : list ){ for( String s : list ){
int len = s.length();
int v1 = s.charAt( 0 );
SparseArrayCompat< Group > map; SparseIntArray map;
int key; int key;
if( len == 1 ){
int v1 = s.charAt( 0 );
if( s.length() == 1 ){
map = map1; map = map1;
key = v1; key = v1;
}else{ }else{
@ -66,35 +46,37 @@ public class CharacterGroup {
key = v1 | ( v2 << 16 ); key = v1 | ( v2 << 16 );
} }
Group old = map.get( key ); int old = map.get( key );
if( old != null && old != g ){ if( old != 0 && old != group_id ){
throw new RuntimeException( String.format( Locale.JAPAN, "group conflict: %s", s ) ); throw new RuntimeException( String.format( Locale.JAPAN, "group conflict: %s", s ) );
} }
map.put( key, g ); map.put( key, group_id );
} }
} }
// CharSequence の範囲 から 文字,グループ,終端 のどれかを列挙する // Tokenizerが終端に達したことを示す
static final int END = - 1;
// 入力された文字列から 文字,グループ,終端 のどれかを順に列挙する
class Tokenizer { class Tokenizer {
public CharSequence text; CharSequence text;
public int end; int end;
// next() を読むと以下の変数が更新される // next() を読むと以下の変数が更新される
public int offset; int offset;
public int c; // may END or group.id or UTF-16 character int c; // may END or group_id or UTF-16 character
public Group group;
Tokenizer( @NonNull CharSequence text, int start, int end ){ Tokenizer( @NonNull CharSequence text, int start, int end ){
reset( text, start, end ); reset( text, start, end );
} }
public void reset( CharSequence text, int start, int end ){ void reset( CharSequence text, int start, int end ){
this.text = text; this.text = text;
this.offset = start; this.offset = start;
this.end = end; this.end = end;
} }
public void next(){ void next(){
int pos = offset; int pos = offset;
@ -106,28 +88,27 @@ public class CharacterGroup {
if( remain <= 0 ){ if( remain <= 0 ){
// 空白を読み飛ばしたら終端になった // 空白を読み飛ばしたら終端になった
// 終端の場合末尾の空白はoffsetに含めない // 終端の場合末尾の空白はoffsetに含めない
this.group = null;
this.c = END; this.c = END;
return; return;
} }
int v1 = text.charAt( pos ); int v1 = text.charAt( pos );
int v2 = remain > 1 ? text.charAt( pos + 1 ) : 0;
// グループに登録された文字を長い順にチェック // グループに登録された文字を長い順にチェック
int check_len = remain > 2 ? 2 : remain; int check_len = remain > 2 ? 2 : remain;
while( check_len > 0 ){ while( check_len > 0 ){
Group g = check_len == 1 ? map1.get( v1 ) : map2.get( v1 | ( v2 << 16 ) ); int group_id = ( check_len == 1
if( g != null ){ ? map1.get( v1 )
this.group = g; : map2.get( v1 | ( ( (int) text.charAt( pos + 1 ) ) << 16 ) )
this.c = g.id; );
if( group_id != 0 ){
this.c = group_id;
this.offset = pos + check_len; this.offset = pos + check_len;
return; return;
} }
-- check_len; -- check_len;
} }
this.group = null;
this.c = v1; this.c = v1;
this.offset = pos + 1; this.offset = pos + 1;
} }
@ -259,7 +240,7 @@ public class CharacterGroup {
addGroup( new String[]{ "", "", "" } ); addGroup( new String[]{ "", "", "" } );
// チルダ // チルダ
addGroup( new String[]{ "~", c2s(tmp,0x301C),c2s(tmp,0xFF5E) } ); addGroup( new String[]{ "~", c2s( tmp, 0x301C ), c2s( tmp, 0xFF5E ) } );
// 半角カナの濁音,半濁音は2文字になる // 半角カナの濁音,半濁音は2文字になる
addGroup( new String[]{ "", "", "ガ" } ); addGroup( new String[]{ "", "", "ガ" } );
@ -288,7 +269,7 @@ public class CharacterGroup {
addGroup( new String[]{ "", "", "ペ" } ); addGroup( new String[]{ "", "", "ペ" } );
addGroup( new String[]{ "", "", "ポ" } ); addGroup( new String[]{ "", "", "ポ" } );
addGroup( new String[]{ "", "う゛", "ヴ" } ); addGroup( new String[]{ "", "う゛", "ヴ" } );
addGroup( new String[]{ "", "", "", "", "", "" } ); addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } ); addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } ); addGroup( new String[]{ "", "", "", "", "", "" } );

View File

@ -218,18 +218,20 @@ public class HTMLDecoder {
MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ), options.link_tag ); MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ), options.link_tag );
sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
// リンクスパンを設定した後に色をつける
if( options.highlight_trie != null ){ }
ArrayList<WordTrieTree.Match > list = options.highlight_trie.matchList( sb,start,end ); }
if( list != null ){
for( WordTrieTree.Match range : list ){ // リンクスパンを設定した後に色をつける
HighlightWord word = HighlightWord.load( range.word ); if( options.highlight_trie != null ){
if( word !=null ){ ArrayList<WordTrieTree.Match > list = options.highlight_trie.matchList( sb,start,end );
sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ); if( list != null ){
if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){ for( WordTrieTree.Match range : list ){
options.highlight_sound = word; HighlightWord word = HighlightWord.load( range.word );
} if( word !=null ){
} sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){
options.highlight_sound = word;
} }
} }
} }

View File

@ -14,29 +14,6 @@ public class WordTrieTree {
int end; int end;
} }
// private static class Matcher {
//
// // ミュートの場合などは短いマッチでも構わない
// final boolean allowShortMatch;
//
// // マッチ範囲の始端を覚えておく
// int start;
//
// Matcher( boolean allowShortMatch ){
// this.allowShortMatch = allowShortMatch;
// }
//
// void setTokenizer( CharacterGroup grouper, CharSequence src, int start, int end ){
// this.match = null;
// this.start = start;
// if( t == null ){
//
// }else{
// t.reset( src, start, end );
// }
// }
// }
private static final CharacterGroup grouper = new CharacterGroup(); private static final CharacterGroup grouper = new CharacterGroup();
private static class Node { private static class Node {