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.Misc;
37 import org.jetbrains.annotations.Contract;
38 import org.jetbrains.annotations.NotNull;
40 import java.util.Objects;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 import java.util.regex.PatternSyntaxException;
45 abstract public class TemplateDetailsItem {
46 private final Type type;
48 protected Long position;
50 protected TemplateDetailsItem(Type type) {
54 public static @NotNull TemplateDetailsItem.Header createHeader() {
57 public static @NotNull TemplateDetailsItem.Header createHeader(Header origin) {
58 return new Header(origin);
61 public static @NotNull TemplateDetailsItem.AccountRow createAccountRow() {
62 return new AccountRow();
64 public static TemplateDetailsItem fromRoomObject(TemplateBase p) {
65 if (p instanceof TemplateHeader) {
66 TemplateHeader ph = (TemplateHeader) p;
67 Header header = createHeader();
68 header.setId(ph.getId());
69 header.setName(ph.getName());
70 header.setPattern(ph.getRegularExpression());
71 header.setTestText(ph.getTestText());
73 if (ph.getTransactionDescriptionMatchGroup() == null)
74 header.setTransactionDescription(ph.getTransactionDescription());
76 header.setTransactionDescriptionMatchGroup(
77 ph.getTransactionDescriptionMatchGroup());
79 if (ph.getTransactionCommentMatchGroup() == null)
80 header.setTransactionComment(ph.getTransactionComment());
82 header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
84 if (ph.getDateDayMatchGroup() == null)
85 header.setDateDay(ph.getDateDay());
87 header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
89 if (ph.getDateMonthMatchGroup() == null)
90 header.setDateMonth(ph.getDateMonth());
92 header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
94 if (ph.getDateYearMatchGroup() == null)
95 header.setDateYear(ph.getDateYear());
97 header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
101 else if (p instanceof TemplateAccount) {
102 TemplateAccount pa = (TemplateAccount) p;
103 AccountRow acc = createAccountRow();
104 acc.setId(pa.getId());
106 if (pa.getAccountNameMatchGroup() == null)
107 acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
109 acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
111 if (pa.getAccountCommentMatchGroup() == null)
112 acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
114 acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
116 if (pa.getCurrencyMatchGroup() == null) {
117 final Integer currencyId = pa.getCurrency();
118 if (currencyId != null && currencyId > 0)
119 acc.setCurrency(Currency.loadById(currencyId));
122 acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
124 final Integer amountMatchGroup = pa.getAmountMatchGroup();
125 if (amountMatchGroup != null && amountMatchGroup > 0) {
126 acc.setAmountMatchGroup(amountMatchGroup);
127 final Boolean negateAmount = pa.getNegateAmount();
128 acc.setNegateAmount(negateAmount != null && negateAmount);
131 acc.setAmount(pa.getAmount());
136 throw new IllegalStateException("Unexpected item class " + p.getClass());
139 public Header asHeaderItem() {
140 ensureType(Type.HEADER);
141 return (Header) this;
143 public AccountRow asAccountRowItem() {
144 ensureType(Type.ACCOUNT_ITEM);
145 return (AccountRow) this;
147 private void ensureType(Type type) {
148 if (this.type != type)
149 throw new IllegalStateException(
150 String.format("Type is %s, but %s is required", this.type.toString(),
153 void ensureTrue(boolean flag) {
155 throw new IllegalStateException(
156 "Literal value requested, but it is matched via a pattern group");
158 void ensureFalse(boolean flag) {
160 throw new IllegalStateException("Matching group requested, but the value is a literal");
162 public long getId() {
165 public void setId(Long id) {
168 public void setId(int id) {
171 public long getPosition() {
174 public void setPosition(Long position) {
175 this.position = position;
177 abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
178 public Type getType() {
182 HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
192 static class PossiblyMatchedValue<T> {
193 private boolean literalValue;
195 private int matchGroup;
196 public PossiblyMatchedValue() {
200 public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
201 literalValue = origin.literalValue;
202 value = origin.value;
203 matchGroup = origin.matchGroup;
206 public static PossiblyMatchedValue<Integer> withLiteralInt(Integer initialValue) {
207 PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
208 result.setValue(initialValue);
212 public static PossiblyMatchedValue<Float> withLiteralFloat(Float initialValue) {
213 PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
214 result.setValue(initialValue);
217 public static PossiblyMatchedValue<Short> withLiteralShort(Short initialValue) {
218 PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
219 result.setValue(initialValue);
223 public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
224 PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
225 result.setValue(initialValue);
228 public T getValue() {
230 throw new IllegalStateException("Value is not literal");
233 public void setValue(T newValue) {
237 public boolean hasLiteralValue() {
240 public int getMatchGroup() {
242 throw new IllegalStateException("Value is literal");
245 public void setMatchGroup(int group) {
246 this.matchGroup = group;
247 literalValue = false;
249 public boolean equals(PossiblyMatchedValue<T> other) {
250 if (!other.literalValue == literalValue)
254 return other.value == null;
255 return value.equals(other.value);
258 return matchGroup == other.matchGroup;
260 public void switchToLiteral() {
263 public String toString() {
268 return value.toString();
270 return "grp:" + matchGroup;
275 public static class TYPE {
276 public static final int header = 0;
277 public static final int accountItem = 1;
280 public static class AccountRow extends TemplateDetailsItem {
281 private final PossiblyMatchedValue<String> accountName =
282 PossiblyMatchedValue.withLiteralString("");
283 private final PossiblyMatchedValue<String> accountComment =
284 PossiblyMatchedValue.withLiteralString("");
285 private final PossiblyMatchedValue<Float> amount =
286 PossiblyMatchedValue.withLiteralFloat(null);
287 private final PossiblyMatchedValue<Currency> currency = new PossiblyMatchedValue<>();
288 private boolean negateAmount;
289 private AccountRow() {
290 super(Type.ACCOUNT_ITEM);
292 public boolean isNegateAmount() {
295 public void setNegateAmount(boolean negateAmount) {
296 this.negateAmount = negateAmount;
298 public int getAccountCommentMatchGroup() {
299 return accountComment.getMatchGroup();
301 public void setAccountCommentMatchGroup(int group) {
302 accountComment.setMatchGroup(group);
304 public String getAccountComment() {
305 return accountComment.getValue();
307 public void setAccountComment(String comment) {
308 this.accountComment.setValue(comment);
310 public int getCurrencyMatchGroup() {
311 return currency.getMatchGroup();
313 public void setCurrencyMatchGroup(int group) {
314 currency.setMatchGroup(group);
316 public Currency getCurrency() {
317 return currency.getValue();
319 public void setCurrency(Currency currency) {
320 this.currency.setValue(currency);
322 public int getAccountNameMatchGroup() {
323 return accountName.getMatchGroup();
325 public void setAccountNameMatchGroup(int group) {
326 accountName.setMatchGroup(group);
328 public String getAccountName() {
329 return accountName.getValue();
331 public void setAccountName(String accountName) {
332 this.accountName.setValue(accountName);
334 public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
335 public boolean hasLiteralAmount() {
336 return amount.hasLiteralValue();
338 public int getAmountMatchGroup() {
339 return amount.getMatchGroup();
341 public void setAmountMatchGroup(int group) {
342 amount.setMatchGroup(group);
344 public Float getAmount() {
345 return amount.getValue();
347 public void setAmount(Float amount) {
348 this.amount.setValue(amount);
350 public String getProblem(@NonNull Resources r, int patternGroupCount) {
351 if (Misc.emptyIsNull(accountName.getValue()) == null)
352 return r.getString(R.string.account_name_is_empty);
353 if (!amount.hasLiteralValue() &&
354 (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
355 return r.getString(R.string.invalid_matching_group_number);
359 public boolean hasLiteralAccountComment() {
360 return accountComment.hasLiteralValue();
362 public boolean equalContents(AccountRow o) {
363 return amount.equals(o.amount) && accountName.equals(o.accountName) &&
364 accountComment.equals(o.accountComment) && negateAmount == o.negateAmount;
366 public void switchToLiteralAmount() {
367 amount.switchToLiteral();
369 public void switchToLiteralAccountName() {
370 accountName.switchToLiteral();
372 public void switchToLiteralAccountComment() {
373 accountComment.switchToLiteral();
375 public TemplateAccount toDBO(@NonNull Long patternId) {
376 TemplateAccount result = new TemplateAccount(id, patternId, position);
378 if (accountName.hasLiteralValue())
379 result.setAccountName(accountName.getValue());
381 result.setAccountNameMatchGroup(accountName.getMatchGroup());
383 if (accountComment.hasLiteralValue())
384 result.setAccountComment(accountComment.getValue());
386 result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
388 if (amount.hasLiteralValue()) {
389 result.setAmount(amount.getValue());
390 result.setNegateAmount(null);
393 result.setAmountMatchGroup(amount.getMatchGroup());
394 result.setNegateAmount(negateAmount ? true : null);
401 public static class Header extends TemplateDetailsItem {
402 private String pattern = "";
403 private String testText = "";
404 private String name = "";
405 private Pattern compiledPattern;
406 private String patternError;
407 private PossiblyMatchedValue<String> transactionDescription =
408 PossiblyMatchedValue.withLiteralString("");
409 private PossiblyMatchedValue<String> transactionComment =
410 PossiblyMatchedValue.withLiteralString("");
411 private PossiblyMatchedValue<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
412 private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
413 private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
414 private SpannableString testMatch;
418 public Header(Header origin) {
422 testText = origin.testText;
423 testMatch = origin.testMatch;
424 setPattern(origin.pattern);
426 transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
427 transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
429 dateYear = new PossiblyMatchedValue<>(origin.dateYear);
430 dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
431 dateDay = new PossiblyMatchedValue<>(origin.dateDay);
433 private static StyleSpan capturedSpan() { return new StyleSpan(Typeface.BOLD); }
434 private static UnderlineSpan matchedSpan() { return new UnderlineSpan(); }
435 private static ForegroundColorSpan notMatchedSpan() {
436 return new ForegroundColorSpan(Color.GRAY);
438 public String getName() {
441 public void setName(String name) {
444 public String getPattern() {
447 public void setPattern(String pattern) {
448 this.pattern = pattern;
450 this.compiledPattern = Pattern.compile(pattern);
453 catch (PatternSyntaxException ex) {
454 patternError = ex.getDescription();
455 compiledPattern = null;
457 testMatch = new SpannableString(testText);
458 testMatch.setSpan(notMatchedSpan(), 0, testText.length() - 1,
459 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
464 public String toString() {
465 return super.toString() +
466 String.format(" name[%s] pat[%s] test[%s] tran[%s] com[%s]", name, pattern,
467 testText, transactionDescription, transactionComment);
469 public String getTestText() {
472 public void setTestText(String testText) {
473 this.testText = testText;
477 public String getTransactionDescription() {
478 return transactionDescription.getValue();
480 public void setTransactionDescription(String transactionDescription) {
481 this.transactionDescription.setValue(transactionDescription);
483 public String getTransactionComment() {
484 return transactionComment.getValue();
486 public void setTransactionComment(String transactionComment) {
487 this.transactionComment.setValue(transactionComment);
489 public Integer getDateYear() {
490 return dateYear.getValue();
492 public void setDateYear(Integer dateYear) {
493 this.dateYear.setValue(dateYear);
495 public Integer getDateMonth() {
496 return dateMonth.getValue();
498 public void setDateMonth(Integer dateMonth) {
499 this.dateMonth.setValue(dateMonth);
501 public Integer getDateDay() {
502 return dateDay.getValue();
504 public void setDateDay(Integer dateDay) {
505 this.dateDay.setValue(dateDay);
507 public int getDateYearMatchGroup() {
508 return dateYear.getMatchGroup();
510 public void setDateYearMatchGroup(int dateYearMatchGroup) {
511 this.dateYear.setMatchGroup(dateYearMatchGroup);
513 public int getDateMonthMatchGroup() {
514 return dateMonth.getMatchGroup();
516 public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
517 this.dateMonth.setMatchGroup(dateMonthMatchGroup);
519 public int getDateDayMatchGroup() {
520 return dateDay.getMatchGroup();
522 public void setDateDayMatchGroup(int dateDayMatchGroup) {
523 this.dateDay.setMatchGroup(dateDayMatchGroup);
525 public boolean hasLiteralDateYear() {
526 return dateYear.hasLiteralValue();
528 public boolean hasLiteralDateMonth() {
529 return dateMonth.hasLiteralValue();
531 public boolean hasLiteralDateDay() {
532 return dateDay.hasLiteralValue();
534 public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
535 public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
536 public String getProblem(@NonNull Resources r, int patternGroupCount) {
537 if (patternError != null)
538 return r.getString(R.string.pattern_has_errors) + ": " + patternError;
539 if (Misc.emptyIsNull(pattern) == null)
540 return r.getString(R.string.pattern_is_empty);
542 if (!dateYear.hasLiteralValue() && compiledPattern != null &&
543 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
544 return r.getString(R.string.invalid_matching_group_number);
546 if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
547 (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
548 return r.getString(R.string.invalid_matching_group_number);
550 if (!dateDay.hasLiteralValue() && compiledPattern != null &&
551 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
552 return r.getString(R.string.invalid_matching_group_number);
557 public boolean equalContents(Header o) {
558 if (!dateDay.equals(o.dateDay))
560 if (!dateMonth.equals(o.dateMonth))
562 if (!dateYear.equals(o.dateYear))
564 if (!transactionDescription.equals(o.transactionDescription))
566 if (!transactionComment.equals(o.transactionComment))
569 return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
570 Misc.equalStrings(testText, o.testText) &&
571 Misc.equalStrings(patternError, o.patternError) &&
572 Objects.equals(testMatch, o.testMatch);
574 public String getMatchGroupText(int group) {
575 if (compiledPattern != null && testText != null) {
576 Matcher m = compiledPattern.matcher(testText);
578 return m.group(group);
583 public Pattern getCompiledPattern() {
584 return compiledPattern;
586 public void switchToLiteralTransactionDescription() {
587 transactionDescription.switchToLiteral();
589 public void switchToLiteralTransactionComment() {
590 transactionComment.switchToLiteral();
592 public int getTransactionDescriptionMatchGroup() {
593 return transactionDescription.getMatchGroup();
595 public void setTransactionDescriptionMatchGroup(int group) {
596 transactionDescription.setMatchGroup(group);
598 public int getTransactionCommentMatchGroup() {
599 return transactionComment.getMatchGroup();
601 public void setTransactionCommentMatchGroup(int group) {
602 transactionComment.setMatchGroup(group);
604 public void switchToLiteralDateYear() {
605 dateYear.switchToLiteral();
607 public void switchToLiteralDateMonth() {
608 dateMonth.switchToLiteral();
610 public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
611 public TemplateHeader toDBO() {
612 TemplateHeader result = new TemplateHeader(id, name, pattern);
614 if (Misc.emptyIsNull(testText) != null)
615 result.setTestText(testText);
617 if (transactionDescription.hasLiteralValue())
618 result.setTransactionDescription(transactionDescription.getValue());
620 result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
622 if (transactionComment.hasLiteralValue())
623 result.setTransactionComment(transactionComment.getValue());
625 result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
627 if (dateYear.hasLiteralValue())
628 result.setDateYear(dateYear.getValue());
630 result.setDateYearMatchGroup(dateYear.getMatchGroup());
632 if (dateMonth.hasLiteralValue())
633 result.setDateMonth(dateMonth.getValue());
635 result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
637 if (dateDay.hasLiteralValue())
638 result.setDateDay(dateDay.getValue());
640 result.setDateDayMatchGroup(dateDay.getMatchGroup());
644 public SpannableString getTestMatch() {
647 public void checkPatternMatch() {
651 if (pattern != null) {
653 if (Misc.emptyIsNull(testText) != null) {
654 SpannableString ss = new SpannableString(testText);
655 Matcher m = compiledPattern.matcher(testText);
658 ss.setSpan(notMatchedSpan(), 0, m.start(),
659 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
660 if (m.end() < testText.length() - 1)
661 ss.setSpan(notMatchedSpan(), m.end(), testText.length(),
662 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
664 ss.setSpan(matchedSpan(), m.start(0), m.end(0),
665 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
667 if (m.groupCount() > 0) {
668 for (int g = 1; g <= m.groupCount(); g++) {
669 ss.setSpan(capturedSpan(), m.start(g), m.end(g),
670 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
675 patternError = "Pattern does not match";
676 ss.setSpan(new ForegroundColorSpan(Color.GRAY), 0,
677 testText.length() - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
683 catch (PatternSyntaxException e) {
684 this.compiledPattern = null;
685 this.patternError = e.getMessage();
689 patternError = "Missing pattern";
692 public String getPatternError() {
695 public SpannableString testMatch() {