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());
105 acc.setPosition(pa.getPosition());
107 if (pa.getAccountNameMatchGroup() == null)
108 acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
110 acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
112 if (pa.getAccountCommentMatchGroup() == null)
113 acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
115 acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
117 if (pa.getCurrencyMatchGroup() == null) {
118 final Integer currencyId = pa.getCurrency();
119 if (currencyId != null && currencyId > 0)
120 acc.setCurrency(Currency.loadById(currencyId));
123 acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
125 final Integer amountMatchGroup = pa.getAmountMatchGroup();
126 if (amountMatchGroup != null && amountMatchGroup > 0) {
127 acc.setAmountMatchGroup(amountMatchGroup);
128 final Boolean negateAmount = pa.getNegateAmount();
129 acc.setNegateAmount(negateAmount != null && negateAmount);
132 acc.setAmount(pa.getAmount());
137 throw new IllegalStateException("Unexpected item class " + p.getClass());
140 public Header asHeaderItem() {
141 ensureType(Type.HEADER);
142 return (Header) this;
144 public AccountRow asAccountRowItem() {
145 ensureType(Type.ACCOUNT_ITEM);
146 return (AccountRow) this;
148 private void ensureType(Type type) {
149 if (this.type != type)
150 throw new IllegalStateException(
151 String.format("Type is %s, but %s is required", this.type.toString(),
154 void ensureTrue(boolean flag) {
156 throw new IllegalStateException(
157 "Literal value requested, but it is matched via a pattern group");
159 void ensureFalse(boolean flag) {
161 throw new IllegalStateException("Matching group requested, but the value is a literal");
163 public long getId() {
166 public void setId(Long id) {
169 public void setId(int id) {
172 public long getPosition() {
175 public void setPosition(Long position) {
176 this.position = position;
178 abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
179 public Type getType() {
183 HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
193 static class PossiblyMatchedValue<T> {
194 private boolean literalValue;
196 private int matchGroup;
197 public PossiblyMatchedValue() {
201 public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
202 literalValue = origin.literalValue;
203 value = origin.value;
204 matchGroup = origin.matchGroup;
207 public static PossiblyMatchedValue<Integer> withLiteralInt(Integer initialValue) {
208 PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
209 result.setValue(initialValue);
213 public static PossiblyMatchedValue<Float> withLiteralFloat(Float initialValue) {
214 PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
215 result.setValue(initialValue);
218 public static PossiblyMatchedValue<Short> withLiteralShort(Short initialValue) {
219 PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
220 result.setValue(initialValue);
224 public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
225 PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
226 result.setValue(initialValue);
229 public void copyFrom(@NonNull PossiblyMatchedValue<T> origin) {
230 literalValue = origin.literalValue;
231 value = origin.value;
232 matchGroup = origin.matchGroup;
234 public T getValue() {
236 throw new IllegalStateException("Value is not literal");
239 public void setValue(T newValue) {
243 public boolean hasLiteralValue() {
246 public int getMatchGroup() {
248 throw new IllegalStateException("Value is literal");
251 public void setMatchGroup(int group) {
252 this.matchGroup = group;
253 literalValue = false;
255 public boolean equals(PossiblyMatchedValue<T> other) {
256 if (!other.literalValue == literalValue)
260 return other.value == null;
261 return value.equals(other.value);
264 return matchGroup == other.matchGroup;
266 public void switchToLiteral() {
269 public String toString() {
274 return value.toString();
276 return "grp:" + matchGroup;
281 public static class TYPE {
282 public static final int header = 0;
283 public static final int accountItem = 1;
286 public static class AccountRow extends TemplateDetailsItem {
287 private final PossiblyMatchedValue<String> accountName =
288 PossiblyMatchedValue.withLiteralString("");
289 private final PossiblyMatchedValue<String> accountComment =
290 PossiblyMatchedValue.withLiteralString("");
291 private final PossiblyMatchedValue<Float> amount =
292 PossiblyMatchedValue.withLiteralFloat(null);
293 private final PossiblyMatchedValue<Currency> currency = new PossiblyMatchedValue<>();
294 private boolean negateAmount;
295 public AccountRow() {
296 super(Type.ACCOUNT_ITEM);
298 public AccountRow(AccountRow origin) {
299 super(Type.ACCOUNT_ITEM);
301 position = origin.position;
302 accountName.copyFrom(origin.accountName);
303 accountComment.copyFrom(origin.accountComment);
304 amount.copyFrom(origin.amount);
305 currency.copyFrom(origin.currency);
306 negateAmount = origin.negateAmount;
308 public boolean isNegateAmount() {
311 public void setNegateAmount(boolean negateAmount) {
312 this.negateAmount = negateAmount;
314 public int getAccountCommentMatchGroup() {
315 return accountComment.getMatchGroup();
317 public void setAccountCommentMatchGroup(int group) {
318 accountComment.setMatchGroup(group);
320 public String getAccountComment() {
321 return accountComment.getValue();
323 public void setAccountComment(String comment) {
324 this.accountComment.setValue(comment);
326 public int getCurrencyMatchGroup() {
327 return currency.getMatchGroup();
329 public void setCurrencyMatchGroup(int group) {
330 currency.setMatchGroup(group);
332 public Currency getCurrency() {
333 return currency.getValue();
335 public void setCurrency(Currency currency) {
336 this.currency.setValue(currency);
338 public int getAccountNameMatchGroup() {
339 return accountName.getMatchGroup();
341 public void setAccountNameMatchGroup(int group) {
342 accountName.setMatchGroup(group);
344 public String getAccountName() {
345 return accountName.getValue();
347 public void setAccountName(String accountName) {
348 this.accountName.setValue(accountName);
350 public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
351 public boolean hasLiteralAmount() {
352 return amount.hasLiteralValue();
354 public int getAmountMatchGroup() {
355 return amount.getMatchGroup();
357 public void setAmountMatchGroup(int group) {
358 amount.setMatchGroup(group);
360 public Float getAmount() {
361 return amount.getValue();
363 public void setAmount(Float amount) {
364 this.amount.setValue(amount);
366 public String getProblem(@NonNull Resources r, int patternGroupCount) {
367 if (Misc.emptyIsNull(accountName.getValue()) == null)
368 return r.getString(R.string.account_name_is_empty);
369 if (!amount.hasLiteralValue() &&
370 (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
371 return r.getString(R.string.invalid_matching_group_number);
375 public boolean hasLiteralAccountComment() {
376 return accountComment.hasLiteralValue();
378 public boolean equalContents(AccountRow o) {
379 return amount.equals(o.amount) && accountName.equals(o.accountName) &&
380 accountComment.equals(o.accountComment) && negateAmount == o.negateAmount;
382 public void switchToLiteralAmount() {
383 amount.switchToLiteral();
385 public void switchToLiteralAccountName() {
386 accountName.switchToLiteral();
388 public void switchToLiteralAccountComment() {
389 accountComment.switchToLiteral();
391 public TemplateAccount toDBO(@NonNull Long patternId) {
392 TemplateAccount result = new TemplateAccount(id, patternId, position);
394 if (accountName.hasLiteralValue())
395 result.setAccountName(accountName.getValue());
397 result.setAccountNameMatchGroup(accountName.getMatchGroup());
399 if (accountComment.hasLiteralValue())
400 result.setAccountComment(accountComment.getValue());
402 result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
404 if (amount.hasLiteralValue()) {
405 result.setAmount(amount.getValue());
406 result.setNegateAmount(null);
409 result.setAmountMatchGroup(amount.getMatchGroup());
410 result.setNegateAmount(negateAmount ? true : null);
417 public static class Header extends TemplateDetailsItem {
418 private String pattern = "";
419 private String testText = "";
420 private String name = "";
421 private Pattern compiledPattern;
422 private String patternError;
423 private PossiblyMatchedValue<String> transactionDescription =
424 PossiblyMatchedValue.withLiteralString("");
425 private PossiblyMatchedValue<String> transactionComment =
426 PossiblyMatchedValue.withLiteralString("");
427 private PossiblyMatchedValue<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
428 private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
429 private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
430 private SpannableString testMatch;
434 public Header(Header origin) {
438 testText = origin.testText;
439 testMatch = origin.testMatch;
440 setPattern(origin.pattern);
442 transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
443 transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
445 dateYear = new PossiblyMatchedValue<>(origin.dateYear);
446 dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
447 dateDay = new PossiblyMatchedValue<>(origin.dateDay);
449 private static StyleSpan capturedSpan() { return new StyleSpan(Typeface.BOLD); }
450 private static UnderlineSpan matchedSpan() { return new UnderlineSpan(); }
451 private static ForegroundColorSpan notMatchedSpan() {
452 return new ForegroundColorSpan(Color.GRAY);
454 public String getName() {
457 public void setName(String name) {
460 public String getPattern() {
463 public void setPattern(String pattern) {
464 this.pattern = pattern;
466 this.compiledPattern = Pattern.compile(pattern);
469 catch (PatternSyntaxException ex) {
470 patternError = ex.getDescription();
471 compiledPattern = null;
473 testMatch = new SpannableString(testText);
474 testMatch.setSpan(notMatchedSpan(), 0, testText.length() - 1,
475 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
480 public String toString() {
481 return super.toString() +
482 String.format(" name[%s] pat[%s] test[%s] tran[%s] com[%s]", name, pattern,
483 testText, transactionDescription, transactionComment);
485 public String getTestText() {
488 public void setTestText(String testText) {
489 this.testText = testText;
493 public String getTransactionDescription() {
494 return transactionDescription.getValue();
496 public void setTransactionDescription(String transactionDescription) {
497 this.transactionDescription.setValue(transactionDescription);
499 public String getTransactionComment() {
500 return transactionComment.getValue();
502 public void setTransactionComment(String transactionComment) {
503 this.transactionComment.setValue(transactionComment);
505 public Integer getDateYear() {
506 return dateYear.getValue();
508 public void setDateYear(Integer dateYear) {
509 this.dateYear.setValue(dateYear);
511 public Integer getDateMonth() {
512 return dateMonth.getValue();
514 public void setDateMonth(Integer dateMonth) {
515 this.dateMonth.setValue(dateMonth);
517 public Integer getDateDay() {
518 return dateDay.getValue();
520 public void setDateDay(Integer dateDay) {
521 this.dateDay.setValue(dateDay);
523 public int getDateYearMatchGroup() {
524 return dateYear.getMatchGroup();
526 public void setDateYearMatchGroup(int dateYearMatchGroup) {
527 this.dateYear.setMatchGroup(dateYearMatchGroup);
529 public int getDateMonthMatchGroup() {
530 return dateMonth.getMatchGroup();
532 public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
533 this.dateMonth.setMatchGroup(dateMonthMatchGroup);
535 public int getDateDayMatchGroup() {
536 return dateDay.getMatchGroup();
538 public void setDateDayMatchGroup(int dateDayMatchGroup) {
539 this.dateDay.setMatchGroup(dateDayMatchGroup);
541 public boolean hasLiteralDateYear() {
542 return dateYear.hasLiteralValue();
544 public boolean hasLiteralDateMonth() {
545 return dateMonth.hasLiteralValue();
547 public boolean hasLiteralDateDay() {
548 return dateDay.hasLiteralValue();
550 public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
551 public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
552 public String getProblem(@NonNull Resources r, int patternGroupCount) {
553 if (patternError != null)
554 return r.getString(R.string.pattern_has_errors) + ": " + patternError;
555 if (Misc.emptyIsNull(pattern) == null)
556 return r.getString(R.string.pattern_is_empty);
558 if (!dateYear.hasLiteralValue() && compiledPattern != null &&
559 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
560 return r.getString(R.string.invalid_matching_group_number);
562 if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
563 (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
564 return r.getString(R.string.invalid_matching_group_number);
566 if (!dateDay.hasLiteralValue() && compiledPattern != null &&
567 (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
568 return r.getString(R.string.invalid_matching_group_number);
573 public boolean equalContents(Header o) {
574 if (!dateDay.equals(o.dateDay))
576 if (!dateMonth.equals(o.dateMonth))
578 if (!dateYear.equals(o.dateYear))
580 if (!transactionDescription.equals(o.transactionDescription))
582 if (!transactionComment.equals(o.transactionComment))
585 return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
586 Misc.equalStrings(testText, o.testText) &&
587 Misc.equalStrings(patternError, o.patternError) &&
588 Objects.equals(testMatch, o.testMatch);
590 public String getMatchGroupText(int group) {
591 if (compiledPattern != null && testText != null) {
592 Matcher m = compiledPattern.matcher(testText);
594 return m.group(group);
599 public Pattern getCompiledPattern() {
600 return compiledPattern;
602 public void switchToLiteralTransactionDescription() {
603 transactionDescription.switchToLiteral();
605 public void switchToLiteralTransactionComment() {
606 transactionComment.switchToLiteral();
608 public int getTransactionDescriptionMatchGroup() {
609 return transactionDescription.getMatchGroup();
611 public void setTransactionDescriptionMatchGroup(int group) {
612 transactionDescription.setMatchGroup(group);
614 public int getTransactionCommentMatchGroup() {
615 return transactionComment.getMatchGroup();
617 public void setTransactionCommentMatchGroup(int group) {
618 transactionComment.setMatchGroup(group);
620 public void switchToLiteralDateYear() {
621 dateYear.switchToLiteral();
623 public void switchToLiteralDateMonth() {
624 dateMonth.switchToLiteral();
626 public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
627 public TemplateHeader toDBO() {
628 TemplateHeader result = new TemplateHeader(id, name, pattern);
630 if (Misc.emptyIsNull(testText) != null)
631 result.setTestText(testText);
633 if (transactionDescription.hasLiteralValue())
634 result.setTransactionDescription(transactionDescription.getValue());
636 result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
638 if (transactionComment.hasLiteralValue())
639 result.setTransactionComment(transactionComment.getValue());
641 result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
643 if (dateYear.hasLiteralValue())
644 result.setDateYear(dateYear.getValue());
646 result.setDateYearMatchGroup(dateYear.getMatchGroup());
648 if (dateMonth.hasLiteralValue())
649 result.setDateMonth(dateMonth.getValue());
651 result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
653 if (dateDay.hasLiteralValue())
654 result.setDateDay(dateDay.getValue());
656 result.setDateDayMatchGroup(dateDay.getMatchGroup());
660 public SpannableString getTestMatch() {
663 public void checkPatternMatch() {
667 if (pattern != null) {
669 if (Misc.emptyIsNull(testText) != null) {
670 SpannableString ss = new SpannableString(testText);
671 Matcher m = compiledPattern.matcher(testText);
674 ss.setSpan(notMatchedSpan(), 0, m.start(),
675 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
676 if (m.end() < testText.length() - 1)
677 ss.setSpan(notMatchedSpan(), m.end(), testText.length(),
678 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
680 ss.setSpan(matchedSpan(), m.start(0), m.end(0),
681 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
683 if (m.groupCount() > 0) {
684 for (int g = 1; g <= m.groupCount(); g++) {
685 ss.setSpan(capturedSpan(), m.start(g), m.end(g),
686 Spanned.SPAN_INCLUSIVE_INCLUSIVE);
691 patternError = "Pattern does not match";
692 ss.setSpan(new ForegroundColorSpan(Color.GRAY), 0,
693 testText.length() - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
699 catch (PatternSyntaxException e) {
700 this.compiledPattern = null;
701 this.patternError = e.getMessage();
705 patternError = "Missing pattern";
708 public String getPatternError() {
711 public SpannableString testMatch() {