]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java
AccountRow.equalContents: compare positions too
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / model / TemplateDetailsItem.java
1 /*
2  * Copyright © 2021 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.model;
19
20 import android.content.res.Resources;
21 import android.graphics.Color;
22 import android.graphics.Typeface;
23 import android.text.SpannableString;
24 import android.text.Spanned;
25 import android.text.style.ForegroundColorSpan;
26 import android.text.style.StyleSpan;
27 import android.text.style.UnderlineSpan;
28
29 import androidx.annotation.NonNull;
30
31 import net.ktnx.mobileledger.R;
32 import net.ktnx.mobileledger.db.TemplateAccount;
33 import net.ktnx.mobileledger.db.TemplateBase;
34 import net.ktnx.mobileledger.db.TemplateHeader;
35 import net.ktnx.mobileledger.utils.Logger;
36 import net.ktnx.mobileledger.utils.Misc;
37
38 import org.jetbrains.annotations.Contract;
39 import org.jetbrains.annotations.NotNull;
40
41 import java.util.Locale;
42 import java.util.Objects;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 import java.util.regex.PatternSyntaxException;
46
47 abstract public class TemplateDetailsItem {
48     private final Type type;
49     protected Long id;
50     protected Long position;
51
52     protected TemplateDetailsItem(Type type) {
53         this.type = type;
54     }
55     @Contract(" -> new")
56     public static @NotNull TemplateDetailsItem.Header createHeader() {
57         return new Header();
58     }
59     public static @NotNull TemplateDetailsItem.Header createHeader(Header origin) {
60         return new Header(origin);
61     }
62     @Contract("-> new")
63     public static @NotNull TemplateDetailsItem.AccountRow createAccountRow() {
64         return new AccountRow();
65     }
66     public static TemplateDetailsItem fromRoomObject(TemplateBase p) {
67         if (p instanceof TemplateHeader) {
68             TemplateHeader ph = (TemplateHeader) p;
69             Header header = createHeader();
70             header.setId(ph.getId());
71             header.setName(ph.getName());
72             header.setPattern(ph.getRegularExpression());
73             header.setTestText(ph.getTestText());
74
75             if (ph.getTransactionDescriptionMatchGroup() == null)
76                 header.setTransactionDescription(ph.getTransactionDescription());
77             else
78                 header.setTransactionDescriptionMatchGroup(
79                         ph.getTransactionDescriptionMatchGroup());
80
81             if (ph.getTransactionCommentMatchGroup() == null)
82                 header.setTransactionComment(ph.getTransactionComment());
83             else
84                 header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
85
86             if (ph.getDateDayMatchGroup() == null)
87                 header.setDateDay(ph.getDateDay());
88             else
89                 header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
90
91             if (ph.getDateMonthMatchGroup() == null)
92                 header.setDateMonth(ph.getDateMonth());
93             else
94                 header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
95
96             if (ph.getDateYearMatchGroup() == null)
97                 header.setDateYear(ph.getDateYear());
98             else
99                 header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
100
101             return header;
102         }
103         else if (p instanceof TemplateAccount) {
104             TemplateAccount pa = (TemplateAccount) p;
105             AccountRow acc = createAccountRow();
106             acc.setId(pa.getId());
107             acc.setPosition(pa.getPosition());
108
109             if (pa.getAccountNameMatchGroup() == null)
110                 acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
111             else
112                 acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
113
114             if (pa.getAccountCommentMatchGroup() == null)
115                 acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
116             else
117                 acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
118
119             if (pa.getCurrencyMatchGroup() == null) {
120                 final Integer currencyId = pa.getCurrency();
121                 if (currencyId != null && currencyId > 0)
122                     acc.setCurrency(Currency.loadById(currencyId));
123             }
124             else
125                 acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
126
127             final Integer amountMatchGroup = pa.getAmountMatchGroup();
128             if (amountMatchGroup != null && amountMatchGroup > 0) {
129                 acc.setAmountMatchGroup(amountMatchGroup);
130                 final Boolean negateAmount = pa.getNegateAmount();
131                 acc.setNegateAmount(negateAmount != null && negateAmount);
132             }
133             else
134                 acc.setAmount(pa.getAmount());
135
136             return acc;
137         }
138         else {
139             throw new IllegalStateException("Unexpected item class " + p.getClass());
140         }
141     }
142     public Header asHeaderItem() {
143         ensureType(Type.HEADER);
144         return (Header) this;
145     }
146     public AccountRow asAccountRowItem() {
147         ensureType(Type.ACCOUNT_ITEM);
148         return (AccountRow) this;
149     }
150     private void ensureType(Type type) {
151         if (this.type != type)
152             throw new IllegalStateException(
153                     String.format("Type is %s, but %s is required", this.type.toString(),
154                             type.toString()));
155     }
156     void ensureTrue(boolean flag) {
157         if (!flag)
158             throw new IllegalStateException(
159                     "Literal value requested, but it is matched via a pattern group");
160     }
161     void ensureFalse(boolean flag) {
162         if (flag)
163             throw new IllegalStateException("Matching group requested, but the value is a literal");
164     }
165     public long getId() {
166         return id;
167     }
168     public void setId(Long id) {
169         this.id = id;
170     }
171     public void setId(int id) {
172         this.id = (long) id;
173     }
174     public long getPosition() {
175         return position;
176     }
177     public void setPosition(Long position) {
178         this.position = position;
179     }
180     abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
181     public Type getType() {
182         return type;
183     }
184     public enum Type {
185         HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
186         final int index;
187         Type(int i) {
188             index = i;
189         }
190         public int toInt() {
191             return index;
192         }
193     }
194
195     static class PossiblyMatchedValue<T> {
196         private boolean literalValue;
197         private T value;
198         private int matchGroup;
199         public PossiblyMatchedValue() {
200             literalValue = true;
201             value = null;
202         }
203         public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
204             literalValue = origin.literalValue;
205             value = origin.value;
206             matchGroup = origin.matchGroup;
207         }
208         @NonNull
209         public static PossiblyMatchedValue<Integer> withLiteralInt(Integer initialValue) {
210             PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
211             result.setValue(initialValue);
212             return result;
213         }
214         @NonNull
215         public static PossiblyMatchedValue<Float> withLiteralFloat(Float initialValue) {
216             PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
217             result.setValue(initialValue);
218             return result;
219         }
220         public static PossiblyMatchedValue<Short> withLiteralShort(Short initialValue) {
221             PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
222             result.setValue(initialValue);
223             return result;
224         }
225         @NonNull
226         public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
227             PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
228             result.setValue(initialValue);
229             return result;
230         }
231         public void copyFrom(@NonNull PossiblyMatchedValue<T> origin) {
232             literalValue = origin.literalValue;
233             value = origin.value;
234             matchGroup = origin.matchGroup;
235         }
236         public T getValue() {
237             if (!literalValue)
238                 throw new IllegalStateException("Value is not literal");
239             return value;
240         }
241         public void setValue(T newValue) {
242             value = newValue;
243             literalValue = true;
244         }
245         public boolean hasLiteralValue() {
246             return literalValue;
247         }
248         public int getMatchGroup() {
249             if (literalValue)
250                 throw new IllegalStateException("Value is literal");
251             return matchGroup;
252         }
253         public void setMatchGroup(int group) {
254             this.matchGroup = group;
255             literalValue = false;
256         }
257         public boolean equals(PossiblyMatchedValue<T> other) {
258             if (!other.literalValue == literalValue)
259                 return false;
260             if (literalValue) {
261                 if (value == null)
262                     return other.value == null;
263                 return value.equals(other.value);
264             }
265             else
266                 return matchGroup == other.matchGroup;
267         }
268         public void switchToLiteral() {
269             literalValue = true;
270         }
271         public String toString() {
272             if (literalValue)
273                 if (value == null)
274                     return "<null>";
275                 else
276                     return value.toString();
277             if (matchGroup > 0)
278                 return "grp:" + matchGroup;
279             return "<null>";
280         }
281     }
282
283     public static class TYPE {
284         public static final int header = 0;
285         public static final int accountItem = 1;
286     }
287
288     public static class AccountRow extends TemplateDetailsItem {
289         private final PossiblyMatchedValue<String> accountName =
290                 PossiblyMatchedValue.withLiteralString("");
291         private final PossiblyMatchedValue<String> accountComment =
292                 PossiblyMatchedValue.withLiteralString("");
293         private final PossiblyMatchedValue<Float> amount =
294                 PossiblyMatchedValue.withLiteralFloat(null);
295         private final PossiblyMatchedValue<Currency> currency = new PossiblyMatchedValue<>();
296         private boolean negateAmount;
297         public AccountRow() {
298             super(Type.ACCOUNT_ITEM);
299         }
300         public AccountRow(AccountRow origin) {
301             super(Type.ACCOUNT_ITEM);
302             id = origin.id;
303             position = origin.position;
304             accountName.copyFrom(origin.accountName);
305             accountComment.copyFrom(origin.accountComment);
306             amount.copyFrom(origin.amount);
307             currency.copyFrom(origin.currency);
308             negateAmount = origin.negateAmount;
309         }
310         public boolean isNegateAmount() {
311             return negateAmount;
312         }
313         public void setNegateAmount(boolean negateAmount) {
314             this.negateAmount = negateAmount;
315         }
316         public int getAccountCommentMatchGroup() {
317             return accountComment.getMatchGroup();
318         }
319         public void setAccountCommentMatchGroup(int group) {
320             accountComment.setMatchGroup(group);
321         }
322         public String getAccountComment() {
323             return accountComment.getValue();
324         }
325         public void setAccountComment(String comment) {
326             this.accountComment.setValue(comment);
327         }
328         public int getCurrencyMatchGroup() {
329             return currency.getMatchGroup();
330         }
331         public void setCurrencyMatchGroup(int group) {
332             currency.setMatchGroup(group);
333         }
334         public Currency getCurrency() {
335             return currency.getValue();
336         }
337         public void setCurrency(Currency currency) {
338             this.currency.setValue(currency);
339         }
340         public int getAccountNameMatchGroup() {
341             return accountName.getMatchGroup();
342         }
343         public void setAccountNameMatchGroup(int group) {
344             accountName.setMatchGroup(group);
345         }
346         public String getAccountName() {
347             return accountName.getValue();
348         }
349         public void setAccountName(String accountName) {
350             this.accountName.setValue(accountName);
351         }
352         public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
353         public boolean hasLiteralAmount() {
354             return amount.hasLiteralValue();
355         }
356         public int getAmountMatchGroup() {
357             return amount.getMatchGroup();
358         }
359         public void setAmountMatchGroup(int group) {
360             amount.setMatchGroup(group);
361         }
362         public Float getAmount() {
363             return amount.getValue();
364         }
365         public void setAmount(Float amount) {
366             this.amount.setValue(amount);
367         }
368         public String getProblem(@NonNull Resources r, int patternGroupCount) {
369             if (Misc.emptyIsNull(accountName.getValue()) == null)
370                 return r.getString(R.string.account_name_is_empty);
371             if (!amount.hasLiteralValue() &&
372                 (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
373                 return r.getString(R.string.invalid_matching_group_number);
374
375             return null;
376         }
377         public boolean hasLiteralAccountComment() {
378             return accountComment.hasLiteralValue();
379         }
380         public boolean equalContents(AccountRow o) {
381             if (position != o.position) {
382                 Logger.debug("cmpAcc",
383                         String.format(Locale.US, "[%d] != [%d]: pos %d != pos %d", getId(),
384                                 o.getId(), position, o.position));
385                 return false;
386             }
387             return amount.equals(o.amount) && accountName.equals(o.accountName) &&
388                    position == o.position && accountComment.equals(o.accountComment) &&
389                    negateAmount == o.negateAmount;
390         }
391         public void switchToLiteralAmount() {
392             amount.switchToLiteral();
393         }
394         public void switchToLiteralAccountName() {
395             accountName.switchToLiteral();
396         }
397         public void switchToLiteralAccountComment() {
398             accountComment.switchToLiteral();
399         }
400         public TemplateAccount toDBO(@NonNull Long patternId) {
401             TemplateAccount result = new TemplateAccount(id, patternId, position);
402
403             if (accountName.hasLiteralValue())
404                 result.setAccountName(accountName.getValue());
405             else
406                 result.setAccountNameMatchGroup(accountName.getMatchGroup());
407
408             if (accountComment.hasLiteralValue())
409                 result.setAccountComment(accountComment.getValue());
410             else
411                 result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
412
413             if (amount.hasLiteralValue()) {
414                 result.setAmount(amount.getValue());
415                 result.setNegateAmount(null);
416             }
417             else {
418                 result.setAmountMatchGroup(amount.getMatchGroup());
419                 result.setNegateAmount(negateAmount ? true : null);
420             }
421
422             return result;
423         }
424     }
425
426     public static class Header extends TemplateDetailsItem {
427         private String pattern = "";
428         private String testText = "";
429         private String name = "";
430         private Pattern compiledPattern;
431         private String patternError;
432         private PossiblyMatchedValue<String> transactionDescription =
433                 PossiblyMatchedValue.withLiteralString("");
434         private PossiblyMatchedValue<String> transactionComment =
435                 PossiblyMatchedValue.withLiteralString("");
436         private PossiblyMatchedValue<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
437         private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
438         private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
439         private SpannableString testMatch;
440         private Header() {
441             super(Type.HEADER);
442         }
443         public Header(Header origin) {
444             this();
445             id = origin.id;
446             name = origin.name;
447             testText = origin.testText;
448             testMatch = origin.testMatch;
449             setPattern(origin.pattern);
450
451             transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
452             transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
453
454             dateYear = new PossiblyMatchedValue<>(origin.dateYear);
455             dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
456             dateDay = new PossiblyMatchedValue<>(origin.dateDay);
457         }
458         private static StyleSpan capturedSpan() { return new StyleSpan(Typeface.BOLD); }
459         private static UnderlineSpan matchedSpan() { return new UnderlineSpan(); }
460         private static ForegroundColorSpan notMatchedSpan() {
461             return new ForegroundColorSpan(Color.GRAY);
462         }
463         public String getName() {
464             return name;
465         }
466         public void setName(String name) {
467             this.name = name;
468         }
469         public String getPattern() {
470             return pattern;
471         }
472         public void setPattern(String pattern) {
473             this.pattern = pattern;
474             try {
475                 this.compiledPattern = Pattern.compile(pattern);
476                 checkPatternMatch();
477             }
478             catch (PatternSyntaxException ex) {
479                 patternError = ex.getDescription();
480                 compiledPattern = null;
481
482                 testMatch = new SpannableString(testText);
483                 testMatch.setSpan(notMatchedSpan(), 0, testText.length() - 1,
484                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
485             }
486         }
487         @NonNull
488         @Override
489         public String toString() {
490             return super.toString() +
491                    String.format(" name[%s] pat[%s] test[%s] tran[%s] com[%s]", name, pattern,
492                            testText, transactionDescription, transactionComment);
493         }
494         public String getTestText() {
495             return testText;
496         }
497         public void setTestText(String testText) {
498             this.testText = testText;
499
500             checkPatternMatch();
501         }
502         public String getTransactionDescription() {
503             return transactionDescription.getValue();
504         }
505         public void setTransactionDescription(String transactionDescription) {
506             this.transactionDescription.setValue(transactionDescription);
507         }
508         public String getTransactionComment() {
509             return transactionComment.getValue();
510         }
511         public void setTransactionComment(String transactionComment) {
512             this.transactionComment.setValue(transactionComment);
513         }
514         public Integer getDateYear() {
515             return dateYear.getValue();
516         }
517         public void setDateYear(Integer dateYear) {
518             this.dateYear.setValue(dateYear);
519         }
520         public Integer getDateMonth() {
521             return dateMonth.getValue();
522         }
523         public void setDateMonth(Integer dateMonth) {
524             this.dateMonth.setValue(dateMonth);
525         }
526         public Integer getDateDay() {
527             return dateDay.getValue();
528         }
529         public void setDateDay(Integer dateDay) {
530             this.dateDay.setValue(dateDay);
531         }
532         public int getDateYearMatchGroup() {
533             return dateYear.getMatchGroup();
534         }
535         public void setDateYearMatchGroup(int dateYearMatchGroup) {
536             this.dateYear.setMatchGroup(dateYearMatchGroup);
537         }
538         public int getDateMonthMatchGroup() {
539             return dateMonth.getMatchGroup();
540         }
541         public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
542             this.dateMonth.setMatchGroup(dateMonthMatchGroup);
543         }
544         public int getDateDayMatchGroup() {
545             return dateDay.getMatchGroup();
546         }
547         public void setDateDayMatchGroup(int dateDayMatchGroup) {
548             this.dateDay.setMatchGroup(dateDayMatchGroup);
549         }
550         public boolean hasLiteralDateYear() {
551             return dateYear.hasLiteralValue();
552         }
553         public boolean hasLiteralDateMonth() {
554             return dateMonth.hasLiteralValue();
555         }
556         public boolean hasLiteralDateDay() {
557             return dateDay.hasLiteralValue();
558         }
559         public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
560         public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
561         public String getProblem(@NonNull Resources r, int patternGroupCount) {
562             if (patternError != null)
563                 return r.getString(R.string.pattern_has_errors) + ": " + patternError;
564             if (Misc.emptyIsNull(pattern) == null)
565                 return r.getString(R.string.pattern_is_empty);
566
567             if (!dateYear.hasLiteralValue() && compiledPattern != null &&
568                 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
569                 return r.getString(R.string.invalid_matching_group_number);
570
571             if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
572                 (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
573                 return r.getString(R.string.invalid_matching_group_number);
574
575             if (!dateDay.hasLiteralValue() && compiledPattern != null &&
576                 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
577                 return r.getString(R.string.invalid_matching_group_number);
578
579             return null;
580         }
581
582         public boolean equalContents(Header o) {
583             if (!dateDay.equals(o.dateDay))
584                 return false;
585             if (!dateMonth.equals(o.dateMonth))
586                 return false;
587             if (!dateYear.equals(o.dateYear))
588                 return false;
589             if (!transactionDescription.equals(o.transactionDescription))
590                 return false;
591             if (!transactionComment.equals(o.transactionComment))
592                 return true;
593
594             return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
595                    Misc.equalStrings(testText, o.testText) &&
596                    Misc.equalStrings(patternError, o.patternError) &&
597                    Objects.equals(testMatch, o.testMatch);
598         }
599         public String getMatchGroupText(int group) {
600             if (compiledPattern != null && testText != null) {
601                 Matcher m = compiledPattern.matcher(testText);
602                 if (m.matches())
603                     return m.group(group);
604             }
605
606             return "ø";
607         }
608         public Pattern getCompiledPattern() {
609             return compiledPattern;
610         }
611         public void switchToLiteralTransactionDescription() {
612             transactionDescription.switchToLiteral();
613         }
614         public void switchToLiteralTransactionComment() {
615             transactionComment.switchToLiteral();
616         }
617         public int getTransactionDescriptionMatchGroup() {
618             return transactionDescription.getMatchGroup();
619         }
620         public void setTransactionDescriptionMatchGroup(int group) {
621             transactionDescription.setMatchGroup(group);
622         }
623         public int getTransactionCommentMatchGroup() {
624             return transactionComment.getMatchGroup();
625         }
626         public void setTransactionCommentMatchGroup(int group) {
627             transactionComment.setMatchGroup(group);
628         }
629         public void switchToLiteralDateYear() {
630             dateYear.switchToLiteral();
631         }
632         public void switchToLiteralDateMonth() {
633             dateMonth.switchToLiteral();
634         }
635         public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
636         public TemplateHeader toDBO() {
637             TemplateHeader result = new TemplateHeader(id, name, pattern);
638
639             if (Misc.emptyIsNull(testText) != null)
640                 result.setTestText(testText);
641
642             if (transactionDescription.hasLiteralValue())
643                 result.setTransactionDescription(transactionDescription.getValue());
644             else
645                 result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
646
647             if (transactionComment.hasLiteralValue())
648                 result.setTransactionComment(transactionComment.getValue());
649             else
650                 result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
651
652             if (dateYear.hasLiteralValue())
653                 result.setDateYear(dateYear.getValue());
654             else
655                 result.setDateYearMatchGroup(dateYear.getMatchGroup());
656
657             if (dateMonth.hasLiteralValue())
658                 result.setDateMonth(dateMonth.getValue());
659             else
660                 result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
661
662             if (dateDay.hasLiteralValue())
663                 result.setDateDay(dateDay.getValue());
664             else
665                 result.setDateDayMatchGroup(dateDay.getMatchGroup());
666
667             return result;
668         }
669         public SpannableString getTestMatch() {
670             return testMatch;
671         }
672         public void checkPatternMatch() {
673             patternError = null;
674             testMatch = null;
675
676             if (pattern != null) {
677                 try {
678                     if (Misc.emptyIsNull(testText) != null) {
679                         SpannableString ss = new SpannableString(testText);
680                         Matcher m = compiledPattern.matcher(testText);
681                         if (m.find()) {
682                             if (m.start() > 0)
683                                 ss.setSpan(notMatchedSpan(), 0, m.start(),
684                                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
685                             if (m.end() < testText.length() - 1)
686                                 ss.setSpan(notMatchedSpan(), m.end(), testText.length(),
687                                         Spanned.SPAN_INCLUSIVE_INCLUSIVE);
688
689                             ss.setSpan(matchedSpan(), m.start(0), m.end(0),
690                                     Spanned.SPAN_INCLUSIVE_INCLUSIVE);
691
692                             if (m.groupCount() > 0) {
693                                 for (int g = 1; g <= m.groupCount(); g++) {
694                                     ss.setSpan(capturedSpan(), m.start(g), m.end(g),
695                                             Spanned.SPAN_INCLUSIVE_INCLUSIVE);
696                                 }
697                             }
698                         }
699                         else {
700                             patternError = "Pattern does not match";
701                             ss.setSpan(new ForegroundColorSpan(Color.GRAY), 0,
702                                     testText.length() - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
703                         }
704
705                         testMatch = ss;
706                     }
707                 }
708                 catch (PatternSyntaxException e) {
709                     this.compiledPattern = null;
710                     this.patternError = e.getMessage();
711                 }
712             }
713             else {
714                 patternError = "Missing pattern";
715             }
716         }
717         public String getPatternError() {
718             return patternError;
719         }
720         public SpannableString testMatch() {
721             return testMatch;
722         }
723     }
724 }