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.
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.
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/>.
18 package net.ktnx.mobileledger.model;
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;
29 import androidx.annotation.NonNull;
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;
38 import org.jetbrains.annotations.Contract;
39 import org.jetbrains.annotations.NotNull;
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;
47 abstract public class TemplateDetailsItem {
48 private final Type type;
50 protected long position;
52 protected TemplateDetailsItem(Type type) {
56 public static @NotNull TemplateDetailsItem.Header createHeader() {
59 public static @NotNull TemplateDetailsItem.Header createHeader(Header origin) {
60 return new Header(origin);
63 public static @NotNull TemplateDetailsItem.AccountRow createAccountRow() {
64 return new AccountRow();
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());
75 if (ph.getTransactionDescriptionMatchGroup() == null)
76 header.setTransactionDescription(ph.getTransactionDescription());
78 header.setTransactionDescriptionMatchGroup(
79 ph.getTransactionDescriptionMatchGroup());
81 if (ph.getTransactionCommentMatchGroup() == null)
82 header.setTransactionComment(ph.getTransactionComment());
84 header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
86 if (ph.getDateDayMatchGroup() == null)
87 header.setDateDay(ph.getDateDay());
89 header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
91 if (ph.getDateMonthMatchGroup() == null)
92 header.setDateMonth(ph.getDateMonth());
94 header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
96 if (ph.getDateYearMatchGroup() == null)
97 header.setDateYear(ph.getDateYear());
99 header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
101 header.setFallback(ph.isFallback());
105 else if (p instanceof TemplateAccount) {
106 TemplateAccount pa = (TemplateAccount) p;
107 AccountRow acc = createAccountRow();
108 acc.setId(pa.getId());
109 acc.setPosition(pa.getPosition());
111 if (pa.getAccountNameMatchGroup() == null)
112 acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
114 acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
116 if (pa.getAccountCommentMatchGroup() == null)
117 acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
119 acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
121 if (pa.getCurrencyMatchGroup() == null) {
122 acc.setCurrency(pa.getCurrencyObject());
125 acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
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);
134 acc.setAmount(pa.getAmount());
139 throw new IllegalStateException("Unexpected item class " + p.getClass());
142 public Header asHeaderItem() {
143 ensureType(Type.HEADER);
144 return (Header) this;
146 public AccountRow asAccountRowItem() {
147 ensureType(Type.ACCOUNT_ITEM);
148 return (AccountRow) this;
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(),
156 void ensureTrue(boolean flag) {
158 throw new IllegalStateException(
159 "Literal value requested, but it is matched via a pattern group");
161 void ensureFalse(boolean flag) {
163 throw new IllegalStateException("Matching group requested, but the value is a literal");
165 public long getId() {
168 public void setId(Long id) {
171 public void setId(int id) {
174 public long getPosition() {
177 public void setPosition(long position) {
178 this.position = position;
180 abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
181 public Type getType() {
185 HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
195 static class PossiblyMatchedValue<T> {
196 private boolean literalValue;
198 private int matchGroup;
199 public PossiblyMatchedValue() {
203 public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
204 literalValue = origin.literalValue;
205 value = origin.value;
206 matchGroup = origin.matchGroup;
209 public static PossiblyMatchedValue<Integer> withLiteralInt(Integer initialValue) {
210 PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
211 result.setValue(initialValue);
215 public static PossiblyMatchedValue<Float> withLiteralFloat(Float initialValue) {
216 PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
217 result.setValue(initialValue);
220 public static PossiblyMatchedValue<Short> withLiteralShort(Short initialValue) {
221 PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
222 result.setValue(initialValue);
226 public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
227 PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
228 result.setValue(initialValue);
231 public void copyFrom(@NonNull PossiblyMatchedValue<T> origin) {
232 literalValue = origin.literalValue;
233 value = origin.value;
234 matchGroup = origin.matchGroup;
236 public T getValue() {
238 throw new IllegalStateException("Value is not literal");
241 public void setValue(T newValue) {
245 public boolean hasLiteralValue() {
248 public int getMatchGroup() {
250 throw new IllegalStateException("Value is literal");
253 public void setMatchGroup(int group) {
254 this.matchGroup = group;
255 literalValue = false;
257 public boolean equals(PossiblyMatchedValue<T> other) {
258 if (!other.literalValue == literalValue)
262 return other.value == null;
263 return value.equals(other.value);
266 return matchGroup == other.matchGroup;
268 public void switchToLiteral() {
271 public String toString() {
276 return value.toString();
278 return "grp:" + matchGroup;
281 public boolean isEmpty() {
283 return value == null || Misc.emptyIsNull(value.toString()) == null;
285 return matchGroup > 0;
289 public static class TYPE {
290 public static final int header = 0;
291 public static final int accountItem = 1;
294 public static class AccountRow extends TemplateDetailsItem {
295 private final PossiblyMatchedValue<String> accountName =
296 PossiblyMatchedValue.withLiteralString("");
297 private final PossiblyMatchedValue<String> accountComment =
298 PossiblyMatchedValue.withLiteralString("");
299 private final PossiblyMatchedValue<Float> amount =
300 PossiblyMatchedValue.withLiteralFloat(null);
301 private final PossiblyMatchedValue<net.ktnx.mobileledger.db.Currency> currency =
302 new PossiblyMatchedValue<>();
303 private boolean negateAmount;
304 public AccountRow() {
305 super(Type.ACCOUNT_ITEM);
307 public AccountRow(AccountRow origin) {
308 super(Type.ACCOUNT_ITEM);
310 position = origin.position;
311 accountName.copyFrom(origin.accountName);
312 accountComment.copyFrom(origin.accountComment);
313 amount.copyFrom(origin.amount);
314 currency.copyFrom(origin.currency);
315 negateAmount = origin.negateAmount;
317 public boolean isNegateAmount() {
320 public void setNegateAmount(boolean negateAmount) {
321 this.negateAmount = negateAmount;
323 public int getAccountCommentMatchGroup() {
324 return accountComment.getMatchGroup();
326 public void setAccountCommentMatchGroup(int group) {
327 accountComment.setMatchGroup(group);
329 public String getAccountComment() {
330 return accountComment.getValue();
332 public void setAccountComment(String comment) {
333 this.accountComment.setValue(comment);
335 public int getCurrencyMatchGroup() {
336 return currency.getMatchGroup();
338 public void setCurrencyMatchGroup(int group) {
339 currency.setMatchGroup(group);
341 public net.ktnx.mobileledger.db.Currency getCurrency() {
342 return currency.getValue();
344 public void setCurrency(net.ktnx.mobileledger.db.Currency currency) {
345 this.currency.setValue(currency);
347 public int getAccountNameMatchGroup() {
348 return accountName.getMatchGroup();
350 public void setAccountNameMatchGroup(int group) {
351 accountName.setMatchGroup(group);
353 public String getAccountName() {
354 return accountName.getValue();
356 public void setAccountName(String accountName) {
357 this.accountName.setValue(accountName);
359 public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
360 public boolean hasLiteralAmount() {
361 return amount.hasLiteralValue();
363 public int getAmountMatchGroup() {
364 return amount.getMatchGroup();
366 public void setAmountMatchGroup(int group) {
367 amount.setMatchGroup(group);
369 public Float getAmount() {
370 return amount.getValue();
372 public void setAmount(Float amount) {
373 this.amount.setValue(amount);
375 public String getProblem(@NonNull Resources r, int patternGroupCount) {
376 if (Misc.emptyIsNull(accountName.getValue()) == null)
377 return r.getString(R.string.account_name_is_empty);
378 if (!amount.hasLiteralValue() &&
379 (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
380 return r.getString(R.string.invalid_matching_group_number);
384 public boolean hasLiteralAccountComment() {
385 return accountComment.hasLiteralValue();
387 public boolean hasLiteralCurrency() { return currency.hasLiteralValue(); }
388 public boolean equalContents(AccountRow o) {
389 if (position != o.position) {
390 Logger.debug("cmpAcc",
391 String.format(Locale.US, "[%d] != [%d]: pos %d != pos %d", getId(),
392 o.getId(), position, o.position));
395 return amount.equals(o.amount) && accountName.equals(o.accountName) &&
396 position == o.position && accountComment.equals(o.accountComment) &&
397 negateAmount == o.negateAmount;
399 public void switchToLiteralAmount() {
400 amount.switchToLiteral();
402 public void switchToLiteralCurrency() {
403 currency.switchToLiteral();
405 public void switchToLiteralAccountName() {
406 accountName.switchToLiteral();
408 public void switchToLiteralAccountComment() {
409 accountComment.switchToLiteral();
411 public TemplateAccount toDBO(@NonNull Long patternId) {
412 TemplateAccount result = new TemplateAccount(id, patternId, position);
414 if (accountName.hasLiteralValue())
415 result.setAccountName(accountName.getValue());
417 result.setAccountNameMatchGroup(accountName.getMatchGroup());
419 if (accountComment.hasLiteralValue())
420 result.setAccountComment(accountComment.getValue());
422 result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
424 if (amount.hasLiteralValue()) {
425 result.setAmount(amount.getValue());
426 result.setNegateAmount(null);
429 result.setAmountMatchGroup(amount.getMatchGroup());
430 result.setNegateAmount(negateAmount ? true : null);
433 if (currency.hasLiteralValue()) {
434 net.ktnx.mobileledger.db.Currency c = currency.getValue();
435 result.setCurrency((c == null) ? null : c.getId());
438 result.setCurrencyMatchGroup(currency.getMatchGroup());
443 public boolean isEmpty() {
444 return accountName.isEmpty() && accountComment.isEmpty() && amount.isEmpty();
448 public static class Header extends TemplateDetailsItem {
449 private String pattern = "";
450 private String testText = "";
451 private String name = "";
452 private Pattern compiledPattern;
453 private String patternError;
454 private PossiblyMatchedValue<String> transactionDescription =
455 PossiblyMatchedValue.withLiteralString("");
456 private PossiblyMatchedValue<String> transactionComment =
457 PossiblyMatchedValue.withLiteralString("");
458 private PossiblyMatchedValue<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
459 private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
460 private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
461 private SpannableString testMatch;
462 private boolean isFallback;
466 public Header(Header origin) {
470 testText = origin.testText;
471 testMatch = origin.testMatch;
472 setPattern(origin.pattern);
474 transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
475 transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
477 dateYear = new PossiblyMatchedValue<>(origin.dateYear);
478 dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
479 dateDay = new PossiblyMatchedValue<>(origin.dateDay);
481 isFallback = origin.isFallback;
483 private static StyleSpan capturedSpan() { return new StyleSpan(Typeface.BOLD); }
484 private static UnderlineSpan matchedSpan() { return new UnderlineSpan(); }
485 private static ForegroundColorSpan notMatchedSpan() {
486 return new ForegroundColorSpan(Color.GRAY);
488 public boolean isFallback() {
491 public void setFallback(boolean fallback) {
492 this.isFallback = fallback;
494 public String getName() {
497 public void setName(String name) {
500 public String getPattern() {
503 public void setPattern(String pattern) {
504 this.pattern = pattern;
506 this.compiledPattern = Pattern.compile(pattern);
509 catch (PatternSyntaxException ex) {
510 patternError = ex.getDescription();
511 compiledPattern = null;
513 testMatch = new SpannableString(testText);
514 if (!testText.isEmpty())
515 testMatch.setSpan(notMatchedSpan(), 0, testText.length() - 1,
516 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
521 public String toString() {
522 return super.toString() +
523 String.format(" name[%s] pat[%s] test[%s] tran[%s] com[%s]", name, pattern,
524 testText, transactionDescription, transactionComment);
526 public String getTestText() {
529 public void setTestText(String testText) {
530 this.testText = testText;
534 public String getTransactionDescription() {
535 return transactionDescription.getValue();
537 public void setTransactionDescription(String transactionDescription) {
538 this.transactionDescription.setValue(transactionDescription);
540 public String getTransactionComment() {
541 return transactionComment.getValue();
543 public void setTransactionComment(String transactionComment) {
544 this.transactionComment.setValue(transactionComment);
546 public Integer getDateYear() {
547 return dateYear.getValue();
549 public void setDateYear(Integer dateYear) {
550 this.dateYear.setValue(dateYear);
552 public Integer getDateMonth() {
553 return dateMonth.getValue();
555 public void setDateMonth(Integer dateMonth) {
556 this.dateMonth.setValue(dateMonth);
558 public Integer getDateDay() {
559 return dateDay.getValue();
561 public void setDateDay(Integer dateDay) {
562 this.dateDay.setValue(dateDay);
564 public int getDateYearMatchGroup() {
565 return dateYear.getMatchGroup();
567 public void setDateYearMatchGroup(int dateYearMatchGroup) {
568 this.dateYear.setMatchGroup(dateYearMatchGroup);
570 public int getDateMonthMatchGroup() {
571 return dateMonth.getMatchGroup();
573 public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
574 this.dateMonth.setMatchGroup(dateMonthMatchGroup);
576 public int getDateDayMatchGroup() {
577 return dateDay.getMatchGroup();
579 public void setDateDayMatchGroup(int dateDayMatchGroup) {
580 this.dateDay.setMatchGroup(dateDayMatchGroup);
582 public boolean hasLiteralDateYear() {
583 return dateYear.hasLiteralValue();
585 public boolean hasLiteralDateMonth() {
586 return dateMonth.hasLiteralValue();
588 public boolean hasLiteralDateDay() {
589 return dateDay.hasLiteralValue();
591 public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
592 public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
593 public String getProblem(@NonNull Resources r, int patternGroupCount) {
594 if (patternError != null)
595 return r.getString(R.string.pattern_has_errors) + ": " + patternError;
596 if (Misc.emptyIsNull(pattern) == null)
597 return r.getString(R.string.pattern_is_empty);
599 if (!dateYear.hasLiteralValue() && compiledPattern != null &&
600 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
601 return r.getString(R.string.invalid_matching_group_number);
603 if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
604 (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
605 return r.getString(R.string.invalid_matching_group_number);
607 if (!dateDay.hasLiteralValue() && compiledPattern != null &&
608 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
609 return r.getString(R.string.invalid_matching_group_number);
614 public boolean equalContents(Header o) {
615 if (!dateDay.equals(o.dateDay))
617 if (!dateMonth.equals(o.dateMonth))
619 if (!dateYear.equals(o.dateYear))
621 if (!transactionDescription.equals(o.transactionDescription))
623 if (!transactionComment.equals(o.transactionComment))
626 return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
627 Misc.equalStrings(testText, o.testText) &&
628 Misc.equalStrings(patternError, o.patternError) &&
629 Objects.equals(testMatch, o.testMatch) && isFallback == o.isFallback;
631 public String getMatchGroupText(int group) {
632 if (compiledPattern != null && testText != null) {
633 Matcher m = compiledPattern.matcher(testText);
635 return m.group(group);
640 public Pattern getCompiledPattern() {
641 return compiledPattern;
643 public void switchToLiteralTransactionDescription() {
644 transactionDescription.switchToLiteral();
646 public void switchToLiteralTransactionComment() {
647 transactionComment.switchToLiteral();
649 public int getTransactionDescriptionMatchGroup() {
650 return transactionDescription.getMatchGroup();
652 public void setTransactionDescriptionMatchGroup(int group) {
653 transactionDescription.setMatchGroup(group);
655 public int getTransactionCommentMatchGroup() {
656 return transactionComment.getMatchGroup();
658 public void setTransactionCommentMatchGroup(int group) {
659 transactionComment.setMatchGroup(group);
661 public void switchToLiteralDateYear() {
662 dateYear.switchToLiteral();
664 public void switchToLiteralDateMonth() {
665 dateMonth.switchToLiteral();
667 public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
668 public TemplateHeader toDBO() {
669 TemplateHeader result = new TemplateHeader(id, name, pattern);
671 if (Misc.emptyIsNull(testText) != null)
672 result.setTestText(testText);
674 if (transactionDescription.hasLiteralValue())
675 result.setTransactionDescription(transactionDescription.getValue());
677 result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
679 if (transactionComment.hasLiteralValue())
680 result.setTransactionComment(transactionComment.getValue());
682 result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
684 if (dateYear.hasLiteralValue())
685 result.setDateYear(dateYear.getValue());
687 result.setDateYearMatchGroup(dateYear.getMatchGroup());
689 if (dateMonth.hasLiteralValue())
690 result.setDateMonth(dateMonth.getValue());
692 result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
694 if (dateDay.hasLiteralValue())
695 result.setDateDay(dateDay.getValue());
697 result.setDateDayMatchGroup(dateDay.getMatchGroup());
699 result.setFallback(isFallback);
703 public SpannableString getTestMatch() {
706 public void checkPatternMatch() {
710 if (pattern != null) {
712 if (Misc.emptyIsNull(testText) != null) {
713 SpannableString ss = new SpannableString(testText);
714 Matcher m = compiledPattern.matcher(testText);
717 ss.setSpan(notMatchedSpan(), 0, m.start(),
718 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
719 if (m.end() < testText.length() - 1)
720 ss.setSpan(notMatchedSpan(), m.end(), testText.length(),
721 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
723 ss.setSpan(matchedSpan(), m.start(0), m.end(0),
724 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
726 if (m.groupCount() > 0) {
727 for (int g = 1; g <= m.groupCount(); g++) {
728 ss.setSpan(capturedSpan(), m.start(g), m.end(g),
729 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
734 patternError = "Pattern does not match";
735 ss.setSpan(new ForegroundColorSpan(Color.GRAY), 0,
736 testText.length() - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
742 catch (PatternSyntaxException e) {
743 this.compiledPattern = null;
744 this.patternError = e.getMessage();
748 patternError = "Missing pattern";
751 public String getPatternError() {
754 public SpannableString testMatch() {