2 * Copyright © 2019 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.ui.activity;
20 import android.annotation.SuppressLint;
22 import androidx.annotation.NonNull;
23 import androidx.lifecycle.LiveData;
24 import androidx.lifecycle.MutableLiveData;
25 import androidx.lifecycle.Observer;
26 import androidx.lifecycle.ViewModel;
28 import net.ktnx.mobileledger.BuildConfig;
29 import net.ktnx.mobileledger.model.LedgerTransactionAccount;
30 import net.ktnx.mobileledger.utils.Logger;
31 import net.ktnx.mobileledger.utils.Misc;
33 import org.jetbrains.annotations.NotNull;
35 import java.util.ArrayList;
36 import java.util.Calendar;
37 import java.util.Date;
38 import java.util.GregorianCalendar;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
44 import static net.ktnx.mobileledger.utils.Logger.debug;
46 public class NewTransactionModel extends ViewModel {
47 static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
48 static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
49 static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
50 private final Item header = new Item(this, null, "");
51 private final Item trailer = new Item(this);
52 private final ArrayList<Item> items = new ArrayList<>();
53 private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
54 private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
55 private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
56 public int getAccountCount() {
59 public Date getDate() {
60 return header.date.getValue();
62 public String getDescription() {
63 return header.description.getValue();
65 public LiveData<Boolean> isSubmittable() {
66 return this.isSubmittable;
69 header.date.setValue(null);
70 header.description.setValue(null);
72 items.add(new Item(this, new LedgerTransactionAccount("")));
73 items.add(new Item(this, new LedgerTransactionAccount("")));
74 focusedItem.setValue(0);
76 public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
77 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
78 this.focusedItem.observe(owner, observer);
80 public void stopObservingFocusedItem(
81 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
82 this.focusedItem.removeObserver(observer);
84 public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
86 androidx.lifecycle.Observer<? super Integer> observer) {
87 this.accountCount.observe(owner, observer);
89 public void stopObservingAccountCount(
90 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
91 this.accountCount.removeObserver(observer);
93 public int getFocusedItem() { return focusedItem.getValue(); }
94 public void setFocusedItem(int position) {
95 focusedItem.setValue(position);
97 public int addAccount(LedgerTransactionAccount acc) {
98 items.add(new Item(this, acc));
99 accountCount.setValue(getAccountCount());
102 boolean accountsInInitialState() {
103 for (Item item : items) {
104 LedgerTransactionAccount acc = item.getAccount();
105 if (acc.isAmountSet())
107 if (!acc.getAccountName()
115 LedgerTransactionAccount getAccount(int index) {
116 return items.get(index)
119 public Item getItem(int index) {
124 if (index <= items.size())
125 return items.get(index - 1);
130 A transaction is submittable if:
132 1) has at least two account names
133 2) each amount has account name
134 3) amounts must balance to 0, or
135 3a) there must be exactly one empty amount (with account)
136 4) empty accounts with empty amounts are ignored
137 5) a row with an empty account name or empty amount is guaranteed to exist
139 @SuppressLint("DefaultLocale")
140 public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
145 final String descriptionText = getDescription();
146 boolean submittable = true;
147 List<Item> itemsWithEmptyAmount = new ArrayList<>();
148 List<Item> itemsWithAccountAndEmptyAmount = new ArrayList<>();
151 if ((descriptionText == null) || descriptionText.trim()
154 Logger.debug("submittable", "Transaction not submittable: missing description");
158 for (int i = 0; i < this.items.size(); i++) {
159 Item item = this.items.get(i);
161 LedgerTransactionAccount acc = item.getAccount();
162 String acc_name = acc.getAccountName()
164 if (acc_name.isEmpty()) {
167 if (acc.isAmountSet()) {
168 // 2) each amount has account name
169 Logger.debug("submittable", String.format(
170 "Transaction not submittable: row %d has no account name, but has" +
171 " amount %1.2f", i + 1, acc.getAmount()));
179 if (acc.isAmountSet()) {
181 balance += acc.getAmount();
184 itemsWithEmptyAmount.add(item);
186 if (!acc_name.isEmpty()) {
187 itemsWithAccountAndEmptyAmount.add(item);
192 // 1) has at least two account names
194 Logger.debug("submittable",
195 String.format("Transaction not submittable: only %d account names",
200 // 3) amount must balance to 0, or
201 // 3a) there must be exactly one empty amount (with account)
202 if (Misc.isZero(balance)) {
203 for (Item item : items) {
204 item.setAmountHint(null);
208 int balanceReceiversCount = itemsWithAccountAndEmptyAmount.size();
209 if (balanceReceiversCount != 1) {
210 Logger.debug("submittable", (balanceReceiversCount == 0) ?
211 "Transaction not submittable: non-zero balance " +
212 "with no empty amounts with accounts" :
213 "Transaction not submittable: non-zero balance " +
214 "with multiple empty amounts with accounts");
218 // suggest off-balance amount to a row and remove hints on other rows
219 Item receiver = null;
220 if (!itemsWithAccountAndEmptyAmount.isEmpty())
221 receiver = itemsWithAccountAndEmptyAmount.get(0);
222 else if (!itemsWithEmptyAmount.isEmpty())
223 receiver = itemsWithEmptyAmount.get(0);
225 for (Item item : items) {
226 if (item.equals(receiver)) {
227 Logger.debug("submittable",
228 String.format("Setting amount hint to %1.2f", -balance));
229 item.setAmountHint(String.format("%1.2f", -balance));
232 item.setAmountHint(null);
236 // 5) a row with an empty account name or empty amount is guaranteed to exist
237 if ((empty_rows == 0) &&
238 ((this.items.size() == accounts) || (this.items.size() == amounts)))
244 debug("submittable", submittable ? "YES" : "NO");
245 isSubmittable.setValue(submittable);
247 if (BuildConfig.DEBUG) {
248 debug("submittable", "== Dump of all items");
249 for (int i = 0; i < items.size(); i++) {
250 Item item = items.get(i);
251 LedgerTransactionAccount acc = item.getAccount();
252 debug("submittable", String.format("Item %2d: [%4.2f] %s", i,
253 acc.isAmountSet() ? acc.getAmount() : 0, acc.getAccountName()));
257 catch (NumberFormatException e) {
258 debug("submittable", "NO (because of NumberFormatException)");
259 isSubmittable.setValue(false);
261 catch (Exception e) {
263 debug("submittable", "NO (because of an Exception)");
264 isSubmittable.setValue(false);
267 public void removeItem(int pos) {
269 accountCount.setValue(getAccountCount());
271 public void sendCountNotifications() {
272 accountCount.setValue(getAccountCount());
274 public void sendFocusedNotification() {
275 focusedItem.setValue(focusedItem.getValue());
277 public void updateFocusedItem(int position) {
278 focusedItem.setValue(position);
280 enum ItemType {generalData, transactionRow, bottomFiller}
282 //==========================================================================================
284 class Item extends Object {
285 private ItemType type;
286 private MutableLiveData<Date> date = new MutableLiveData<>();
287 private MutableLiveData<String> description = new MutableLiveData<>();
288 private LedgerTransactionAccount account;
289 private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
290 private NewTransactionModel model;
291 private MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
292 public Item(NewTransactionModel model) {
294 type = ItemType.bottomFiller;
295 editable.setValue(false);
297 public Item(NewTransactionModel model, Date date, String description) {
299 this.type = ItemType.generalData;
300 this.date.setValue(date);
301 this.description.setValue(description);
302 this.editable.setValue(true);
304 public Item(NewTransactionModel model, LedgerTransactionAccount account) {
306 this.type = ItemType.transactionRow;
307 this.account = account;
308 this.editable.setValue(true);
310 public NewTransactionModel getModel() {
313 public void setEditable(boolean editable) {
314 ensureType(ItemType.generalData, ItemType.transactionRow);
315 this.editable.setValue(editable);
317 private void ensureType(ItemType type1, ItemType type2) {
318 if ((type != type1) && (type != type2)) {
319 throw new RuntimeException(
320 String.format("Actual type (%s) differs from wanted (%s or %s)", type,
324 public String getAmountHint() {
325 ensureType(ItemType.transactionRow);
326 return amountHint.getValue();
328 public void setAmountHint(String amountHint) {
329 ensureType(ItemType.transactionRow);
331 // avoid unnecessary triggers
332 if (amountHint == null) {
333 if (this.amountHint.getValue() == null)
337 if (amountHint.equals(this.amountHint.getValue()))
341 this.amountHint.setValue(amountHint);
343 public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
345 androidx.lifecycle.Observer<? super String> observer) {
346 this.amountHint.observe(owner, observer);
348 public void stopObservingAmountHint(
349 @NonNull androidx.lifecycle.Observer<? super String> observer) {
350 this.amountHint.removeObserver(observer);
352 public ItemType getType() {
355 public void ensureType(ItemType wantedType) {
356 if (type != wantedType) {
357 throw new RuntimeException(
358 String.format("Actual type (%s) differs from wanted (%s)", type,
362 public Date getDate() {
363 ensureType(ItemType.generalData);
364 return date.getValue();
366 public void setDate(Date date) {
367 ensureType(ItemType.generalData);
368 this.date.setValue(date);
370 public void setDate(String text) {
371 int year, month, day;
372 final Calendar c = GregorianCalendar.getInstance();
373 Matcher m = reYMD.matcher(text);
375 year = Integer.parseInt(m.group(1));
376 month = Integer.parseInt(m.group(2)) - 1; // month is 0-based
377 day = Integer.parseInt(m.group(3));
380 year = c.get(Calendar.YEAR);
381 m = reMD.matcher(text);
383 month = Integer.parseInt(m.group(1)) - 1;
384 day = Integer.parseInt(m.group(2));
387 month = c.get(Calendar.MONTH);
388 m = reD.matcher(text);
390 day = Integer.parseInt(m.group(1));
393 day = c.get(Calendar.DAY_OF_MONTH);
398 c.set(year, month, day);
400 this.setDate(c.getTime());
402 public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
403 @NonNull androidx.lifecycle.Observer<? super Date> observer) {
404 this.date.observe(owner, observer);
406 public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
407 this.date.removeObserver(observer);
409 public String getDescription() {
410 ensureType(ItemType.generalData);
411 return description.getValue();
413 public void setDescription(String description) {
414 ensureType(ItemType.generalData);
415 this.description.setValue(description);
417 public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
419 androidx.lifecycle.Observer<? super String> observer) {
420 this.description.observe(owner, observer);
422 public void stopObservingDescription(
423 @NonNull androidx.lifecycle.Observer<? super String> observer) {
424 this.description.removeObserver(observer);
426 public LedgerTransactionAccount getAccount() {
427 ensureType(ItemType.transactionRow);
430 public void setAccountName(String name) {
431 account.setAccountName(name);
436 * @return nicely formatted, shortest available date representation
438 public String getFormattedDate() {
441 Date time = date.getValue();
445 Calendar c = GregorianCalendar.getInstance();
447 Calendar today = GregorianCalendar.getInstance();
449 final int myYear = c.get(Calendar.YEAR);
450 final int myMonth = c.get(Calendar.MONTH);
451 final int myDay = c.get(Calendar.DAY_OF_MONTH);
453 if (today.get(Calendar.YEAR) != myYear) {
454 return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay);
457 if (today.get(Calendar.MONTH) != myMonth) {
458 return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay);
461 return String.valueOf(myDay);
463 public void observeEditableFlag(NewTransactionActivity activity,
464 Observer<Boolean> observer) {
465 editable.observe(activity, observer);
467 public void stopObservingEditableFlag(Observer<Boolean> observer) {
468 editable.removeObserver(observer);