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.model.LedgerTransactionAccount;
29 import net.ktnx.mobileledger.utils.Misc;
31 import org.jetbrains.annotations.NotNull;
33 import java.util.ArrayList;
34 import java.util.Calendar;
35 import java.util.Date;
36 import java.util.GregorianCalendar;
37 import java.util.Locale;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
41 import static net.ktnx.mobileledger.utils.Logger.debug;
42 import static net.ktnx.mobileledger.utils.Misc.isZero;
44 public class NewTransactionModel extends ViewModel {
45 static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
46 static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
47 static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
48 private final Item header = new Item(this, null, "");
49 private final Item trailer = new Item(this);
50 private final ArrayList<Item> items = new ArrayList<>();
51 private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
52 private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(null);
53 private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
54 public int getAccountCount() {
57 public Date getDate() {
58 return header.date.getValue();
60 public String getDescription() {
61 return header.description.getValue();
63 public LiveData<Boolean> isSubmittable() {
64 return this.isSubmittable;
67 header.date.setValue(null);
68 header.description.setValue(null);
70 items.add(new Item(this, new LedgerTransactionAccount("")));
71 items.add(new Item(this, new LedgerTransactionAccount("")));
72 focusedItem.setValue(0);
74 public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
75 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
76 this.focusedItem.observe(owner, observer);
78 public void stopObservingFocusedItem(
79 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
80 this.focusedItem.removeObserver(observer);
82 public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
84 androidx.lifecycle.Observer<? super Integer> observer) {
85 this.accountCount.observe(owner, observer);
87 public void stopObservingAccountCount(
88 @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
89 this.accountCount.removeObserver(observer);
91 public void setFocusedItem(int position) {
92 focusedItem.setValue(position);
94 public int addAccount(LedgerTransactionAccount acc) {
95 items.add(new Item(this, acc));
96 accountCount.setValue(getAccountCount());
99 boolean accountsInInitialState() {
100 for (Item item : items) {
101 LedgerTransactionAccount acc = item.getAccount();
102 if (acc.isAmountSet())
104 if (!acc.getAccountName()
112 LedgerTransactionAccount getAccount(int index) {
113 return items.get(index)
116 public Item getItem(int index) {
121 if (index <= items.size())
122 return items.get(index - 1);
127 // 1) at least two account names
128 // 2) each amount must have account name
129 // 3) amounts must balance to 0, or
130 // 3a) there must be exactly one empty amount
131 // 4) empty accounts with empty amounts are ignored
132 // 5) a row with an empty account name or empty amount is guaranteed to exist
133 @SuppressLint("DefaultLocale")
134 public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
136 int accounts_with_values = 0;
138 int amounts_with_accounts = 0;
140 Item empty_amount = null;
141 boolean single_empty_amount = false;
142 boolean single_empty_amount_has_account = false;
143 float running_total = 0f;
144 final String descriptionText = getDescription();
145 final boolean have_description = ((descriptionText != null) && !descriptionText.isEmpty());
148 for (int i = 0; i < this.items.size(); i++) {
149 Item item = this.items.get(i);
151 LedgerTransactionAccount acc = item.getAccount();
152 String acc_name = acc.getAccountName()
154 if (acc_name.isEmpty()) {
160 if (acc.isAmountSet()) {
161 accounts_with_values++;
165 if (acc.isAmountSet()) {
167 if (!acc_name.isEmpty())
168 amounts_with_accounts++;
169 running_total += acc.getAmount();
172 if (empty_amount == null) {
174 single_empty_amount = true;
175 single_empty_amount_has_account = !acc_name.isEmpty();
177 else if (!acc_name.isEmpty())
178 single_empty_amount = false;
182 if ((empty_rows == 0) &&
183 ((this.items.size() == accounts) || (this.items.size() == amounts)))
188 for (NewTransactionModel.Item item : items) {
190 final LedgerTransactionAccount acc = item.getAccount();
191 if (acc.isAmountSet())
194 if (single_empty_amount) {
195 if (item.equals(empty_amount)) {
196 empty_amount.setAmountHint(Misc.isZero(running_total) ? null
204 // no single empty account and this account's amount is not set
205 // => hint should be '0.00'
206 item.setAmountHint(null);
211 debug("submittable", String.format(Locale.US,
212 "%s, accounts=%d, accounts_with_values=%s, " +
213 "amounts_with_accounts=%d, amounts=%d, running_total=%1.2f, " +
214 "single_empty_with_acc=%s", have_description ? "description" : "NO description",
215 accounts, accounts_with_values, amounts_with_accounts, amounts, running_total,
216 (single_empty_amount && single_empty_amount_has_account) ? "true" : "false"));
218 if (have_description && (accounts >= 2) && (accounts_with_values >= (accounts - 1)) &&
219 (amounts_with_accounts == amounts) &&
220 (single_empty_amount && single_empty_amount_has_account || isZero(running_total)))
222 debug("submittable", "YES");
223 isSubmittable.setValue(true);
226 debug("submittable", "NO");
227 isSubmittable.setValue(false);
231 catch (NumberFormatException e) {
232 debug("submittable", "NO (because of NumberFormatException)");
233 isSubmittable.setValue(false);
235 catch (Exception e) {
237 debug("submittable", "NO (because of an Exception)");
238 isSubmittable.setValue(false);
241 public void removeItem(int pos) {
243 accountCount.setValue(getAccountCount());
245 public void sendCountNotifications() {
246 accountCount.setValue(getAccountCount());
248 enum ItemType {generalData, transactionRow, bottomFiller}
250 class Item extends Object {
251 private ItemType type;
252 private MutableLiveData<Date> date = new MutableLiveData<>();
253 private MutableLiveData<String> description = new MutableLiveData<>();
254 private LedgerTransactionAccount account;
255 private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
256 private NewTransactionModel model;
257 private MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
258 public Item(NewTransactionModel model) {
260 type = ItemType.bottomFiller;
261 editable.setValue(false);
263 public Item(NewTransactionModel model, Date date, String description) {
265 this.type = ItemType.generalData;
266 this.date.setValue(date);
267 this.description.setValue(description);
268 this.editable.setValue(true);
270 public Item(NewTransactionModel model, LedgerTransactionAccount account) {
272 this.type = ItemType.transactionRow;
273 this.account = account;
274 this.editable.setValue(true);
276 public NewTransactionModel getModel() {
279 public void setEditable(boolean editable) {
280 ensureType(ItemType.generalData, ItemType.transactionRow);
281 this.editable.setValue(editable);
283 private void ensureType(ItemType type1, ItemType type2) {
284 if ((type != type1) && (type != type2)) {
285 throw new RuntimeException(
286 String.format("Actual type (%s) differs from wanted (%s or %s)", type,
290 public String getAmountHint() {
291 ensureType(ItemType.transactionRow);
292 return amountHint.getValue();
294 public void setAmountHint(String amountHint) {
295 ensureType(ItemType.transactionRow);
297 // avoid unnecessary triggers
298 if (amountHint == null) {
299 if (this.amountHint.getValue() == null)
303 if (amountHint.equals(this.amountHint.getValue()))
307 this.amountHint.setValue(amountHint);
309 public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
311 androidx.lifecycle.Observer<? super String> observer) {
312 this.amountHint.observe(owner, observer);
314 public void stopObservingAmountHint(
315 @NonNull androidx.lifecycle.Observer<? super String> observer) {
316 this.amountHint.removeObserver(observer);
318 public ItemType getType() {
321 public void ensureType(ItemType wantedType) {
322 if (type != wantedType) {
323 throw new RuntimeException(
324 String.format("Actual type (%s) differs from wanted (%s)", type,
328 public Date getDate() {
329 ensureType(ItemType.generalData);
330 return date.getValue();
332 public void setDate(Date date) {
333 ensureType(ItemType.generalData);
334 this.date.setValue(date);
336 public void setDate(String text) {
337 int year, month, day;
338 final Calendar c = GregorianCalendar.getInstance();
339 Matcher m = reYMD.matcher(text);
341 year = Integer.parseInt(m.group(1));
342 month = Integer.parseInt(m.group(2)) - 1; // month is 0-based
343 day = Integer.parseInt(m.group(3));
346 year = c.get(Calendar.YEAR);
347 m = reMD.matcher(text);
349 month = Integer.parseInt(m.group(1)) - 1;
350 day = Integer.parseInt(m.group(2));
353 month = c.get(Calendar.MONTH);
354 m = reD.matcher(text);
356 day = Integer.parseInt(m.group(1));
359 day = c.get(Calendar.DAY_OF_MONTH);
364 c.set(year, month, day);
366 this.setDate(c.getTime());
368 public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
369 @NonNull androidx.lifecycle.Observer<? super Date> observer) {
370 this.date.observe(owner, observer);
372 public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> observer) {
373 this.date.removeObserver(observer);
375 public String getDescription() {
376 ensureType(ItemType.generalData);
377 return description.getValue();
379 public void setDescription(String description) {
380 ensureType(ItemType.generalData);
381 this.description.setValue(description);
383 public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
385 androidx.lifecycle.Observer<? super String> observer) {
386 this.description.observe(owner, observer);
388 public void stopObservingDescription(
389 @NonNull androidx.lifecycle.Observer<? super String> observer) {
390 this.description.removeObserver(observer);
392 public LedgerTransactionAccount getAccount() {
393 ensureType(ItemType.transactionRow);
396 public void setAccountName(String name) {
397 account.setAccountName(name);
402 * @return nicely formatted, shortest available date representation
404 public String getFormattedDate() {
407 Date time = date.getValue();
411 Calendar c = GregorianCalendar.getInstance();
413 Calendar today = GregorianCalendar.getInstance();
415 final int myYear = c.get(Calendar.YEAR);
416 final int myMonth = c.get(Calendar.MONTH);
417 final int myDay = c.get(Calendar.DAY_OF_MONTH);
419 if (today.get(Calendar.YEAR) != myYear) {
420 return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth, myDay);
423 if (today.get(Calendar.MONTH) != myMonth) {
424 return String.format(Locale.US, "%d/%02d", myMonth, myDay);
427 return String.valueOf(myDay);
429 public void observeEditableFlag(NewTransactionActivity activity,
430 Observer<Boolean> observer) {
431 editable.observe(activity, observer);
433 public void stopObservingEditableFlag(Observer<Boolean> observer) {
434 editable.removeObserver(observer);