1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4  *******************************************************************************
5  * Copyright (C) 2014, International Business Machines Corporation and         *
6  * others. All Rights Reserved.                                                *
7  *******************************************************************************
8  */
9 package com.ibm.icu.text;
10 
11 import java.io.IOException;
12 import java.text.CharacterIterator;
13 
14 import com.ibm.icu.lang.UCharacter;
15 import com.ibm.icu.lang.UProperty;
16 import com.ibm.icu.lang.UScript;
17 
18 class KhmerBreakEngine extends DictionaryBreakEngine {
19 
20     // Constants for KhmerBreakIterator
21     // How many words in a row are "good enough"?
22     private static final byte KHMER_LOOKAHEAD = 3;
23     // Will not combine a non-word with a preceding dictionary word longer than this
24     private static final byte KHMER_ROOT_COMBINE_THRESHOLD = 3;
25     // Will not combine a non-word that shares at least this much prefix with a
26     // dictionary word with a preceding word
27     private static final byte KHMER_PREFIX_COMBINE_THRESHOLD = 3;
28     // Minimum word size
29     private static final byte KHMER_MIN_WORD = 2;
30     // Minimum number of characters for two words
31     private static final byte KHMER_MIN_WORD_SPAN = KHMER_MIN_WORD * 2;
32 
33 
34     private DictionaryMatcher fDictionary;
35     private static UnicodeSet fKhmerWordSet;
36     private static UnicodeSet fEndWordSet;
37     private static UnicodeSet fBeginWordSet;
38     private static UnicodeSet fMarkSet;
39 
40     static {
41         // Initialize UnicodeSets
42         fKhmerWordSet = new UnicodeSet();
43         fMarkSet = new UnicodeSet();
44         fBeginWordSet = new UnicodeSet();
45 
46         fKhmerWordSet.applyPattern("[[:Khmer:]&[:LineBreak=SA:]]");
fKhmerWordSet.compact()47         fKhmerWordSet.compact();
48 
49         fMarkSet.applyPattern("[[:Khmer:]&[:LineBreak=SA:]&[:M:]]");
50         fMarkSet.add(0x0020);
51         fEndWordSet = new UnicodeSet(fKhmerWordSet);
52         fBeginWordSet.add(0x1780, 0x17B3);
53         fEndWordSet.remove(0x17D2); // KHMER SIGN COENG that combines some following characters
54 
55         // Compact for caching
fMarkSet.compact()56         fMarkSet.compact();
fEndWordSet.compact()57         fEndWordSet.compact();
fBeginWordSet.compact()58         fBeginWordSet.compact();
59 
60         // Freeze the static UnicodeSet
fKhmerWordSet.freeze()61         fKhmerWordSet.freeze();
fMarkSet.freeze()62         fMarkSet.freeze();
fEndWordSet.freeze()63         fEndWordSet.freeze();
fBeginWordSet.freeze()64         fBeginWordSet.freeze();
65     }
66 
KhmerBreakEngine()67     public KhmerBreakEngine() throws IOException {
68         setCharacters(fKhmerWordSet);
69         // Initialize dictionary
70         fDictionary = DictionaryData.loadDictionaryFor("Khmr");
71     }
72 
73     @Override
equals(Object obj)74     public boolean equals(Object obj) {
75         // Normally is a singleton, but it's possible to have duplicates
76         //   during initialization. All are equivalent.
77         return obj instanceof KhmerBreakEngine;
78     }
79 
80     @Override
hashCode()81     public int hashCode() {
82         return getClass().hashCode();
83     }
84 
85     @Override
handles(int c)86     public boolean handles(int c) {
87         int script = UCharacter.getIntPropertyValue(c, UProperty.SCRIPT);
88         return (script == UScript.KHMER);
89     }
90 
91     @Override
divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd, DequeI foundBreaks)92     public int divideUpDictionaryRange(CharacterIterator fIter, int rangeStart, int rangeEnd,
93             DequeI foundBreaks) {
94 
95         if ((rangeEnd - rangeStart) < KHMER_MIN_WORD_SPAN) {
96             return 0;  // Not enough characters for word
97         }
98         int wordsFound = 0;
99         int wordLength;
100         int current;
101         PossibleWord words[] = new PossibleWord[KHMER_LOOKAHEAD];
102         for (int i = 0; i < KHMER_LOOKAHEAD; i++) {
103             words[i] = new PossibleWord();
104         }
105         int uc;
106 
107         fIter.setIndex(rangeStart);
108 
109         while ((current = fIter.getIndex()) < rangeEnd) {
110             wordLength = 0;
111 
112             //Look for candidate words at the current position
113             int candidates = words[wordsFound % KHMER_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd);
114 
115             // If we found exactly one, use that
116             if (candidates == 1) {
117                 wordLength = words[wordsFound % KHMER_LOOKAHEAD].acceptMarked(fIter);
118                 wordsFound += 1;
119             }
120 
121             // If there was more than one, see which one can take us forward the most words
122             else if (candidates > 1) {
123                 boolean foundBest = false;
124                 // If we're already at the end of the range, we're done
125                 if (fIter.getIndex() < rangeEnd) {
126                     do {
127                         int wordsMatched = 1;
128                         if (words[(wordsFound+1)%KHMER_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) > 0) {
129                             if (wordsMatched < 2) {
130                                 // Followed by another dictionary word; mark first word as a good candidate
131                                 words[wordsFound%KHMER_LOOKAHEAD].markCurrent();
132                                 wordsMatched = 2;
133                             }
134 
135                             // If we're already at the end of the range, we're done
136                             if (fIter.getIndex() >= rangeEnd) {
137                                 break;
138                             }
139 
140                             // See if any of the possible second words is followed by a third word
141                             do {
142                                 // If we find a third word, stop right away
143                                 if (words[(wordsFound+2)%KHMER_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) > 0) {
144                                     words[wordsFound%KHMER_LOOKAHEAD].markCurrent();
145                                     foundBest = true;
146                                     break;
147                                 }
148                             } while (words[(wordsFound+1)%KHMER_LOOKAHEAD].backUp(fIter));
149                         }
150                     } while (words[wordsFound%KHMER_LOOKAHEAD].backUp(fIter) && !foundBest);
151                 }
152                 wordLength = words[wordsFound%KHMER_LOOKAHEAD].acceptMarked(fIter);
153                 wordsFound += 1;
154             }
155 
156             // We come here after having either found a word or not. We look ahead to the
157             // next word. If it's not a dictionary word, we will combine it with the word we
158             // just found (if there is one), but only if the preceding word does not exceed
159             // the threshold.
160             // The text iterator should now be positioned at the end of the word we found.
161             if (fIter.getIndex() < rangeEnd && wordLength < KHMER_ROOT_COMBINE_THRESHOLD) {
162                 // If it is a dictionary word, do nothing. If it isn't, then if there is
163                 // no preceding word, or the non-word shares less than the minimum threshold
164                 // of characters with a dictionary word, then scan to resynchronize
165                 if (words[wordsFound%KHMER_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd) <= 0 &&
166                         (wordLength == 0 ||
167                                 words[wordsFound%KHMER_LOOKAHEAD].longestPrefix() < KHMER_PREFIX_COMBINE_THRESHOLD)) {
168                     // Look for a plausible word boundary
169                     int remaining = rangeEnd - (current + wordLength);
170                     int pc = fIter.current();
171                     int chars = 0;
172                     for (;;) {
173                         fIter.next();
174                         uc = fIter.current();
175                         chars += 1;
176                         if (--remaining <= 0) {
177                             break;
178                         }
179                         if (fEndWordSet.contains(pc) && fBeginWordSet.contains(uc)) {
180                             // Maybe. See if it's in the dictionary.
181                             int candidate = words[(wordsFound + 1) %KHMER_LOOKAHEAD].candidates(fIter, fDictionary, rangeEnd);
182                             fIter.setIndex(current + wordLength + chars);
183                             if (candidate > 0) {
184                                 break;
185                             }
186                         }
187                         pc = uc;
188                     }
189 
190                     // Bump the word count if there wasn't already one
191                     if (wordLength <= 0) {
192                         wordsFound += 1;
193                     }
194 
195                     // Update the length with the passed-over characters
196                     wordLength += chars;
197                 } else {
198                     // Backup to where we were for next iteration
199                     fIter.setIndex(current+wordLength);
200                 }
201             }
202 
203             // Never stop before a combining mark.
204             int currPos;
205             while ((currPos = fIter.getIndex()) < rangeEnd && fMarkSet.contains(fIter.current())) {
206                 fIter.next();
207                 wordLength += fIter.getIndex() - currPos;
208             }
209 
210             // Look ahead for possible suffixes if a dictionary word does not follow.
211             // We do this in code rather than using a rule so that the heuristic
212             // resynch continues to function. For example, one of the suffix characters
213             // could be a typo in the middle of a word.
214             // NOT CURRENTLY APPLICABLE TO KHMER
215 
216             // Did we find a word on this iteration? If so, push it on the break stack
217             if (wordLength > 0) {
218                 foundBreaks.push(Integer.valueOf(current + wordLength));
219             }
220         }
221 
222         // Don't return a break for the end of the dictionary range if there is one there
223         if (foundBreaks.peek() >= rangeEnd) {
224             foundBreaks.pop();
225             wordsFound -= 1;
226         }
227 
228         return wordsFound;
229     }
230 
231 }
232