import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
public class RetrieveTransactionsTask
extends AsyncTask<Void, RetrieveTransactionsTask.Progress, String> {
- private static final int MATCHING_TRANSACTIONS_LIMIT = 50;
+ private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
private static final Pattern reComment = Pattern.compile("^\\s*;");
private static final Pattern reTransactionStart = Pattern.compile("<tr class=\"title\" " +
"id=\"transaction-(\\d+)\"><td class=\"date\"[^\"]*>([\\d.-]+)</td>");
this.contextRef = contextRef;
}
private static void L(String msg) {
- //Log.d("transaction-parser", msg);
+ //debug("transaction-parser", msg);
}
@Override
protected void onProgressUpdate(Progress... values) {
int maxTransactionId = Progress.INDETERMINATE;
ArrayList<LedgerAccount> accountList = new ArrayList<>();
HashMap<String, Void> accountNames = new HashMap<>();
+ HashMap<String, LedgerAccount> syntheticAccounts = new HashMap<>();
LedgerAccount lastAccount = null, prevAccount = null;
boolean onlyStarred = Data.optShowOnlyStarred.get();
.setHasSubAccounts(prevAccount.isParentOf(lastAccount));
// make sure the parent account(s) are present,
// synthesising them if necessary
+ // this happens when the (missing-in-HTML) parent account has
+ // only one child so we create a synthetic parent account record,
+ // copying the amounts when child's amounts are parsed
String parentName = lastAccount.getParentName();
if (parentName != null) {
Stack<String> toAppend = new Stack<>();
parentName =
new LedgerAccount(parentName).getParentName();
}
+ syntheticAccounts.clear();
while (!toAppend.isEmpty()) {
String aName = toAppend.pop();
- LedgerAccount acc = new LedgerAccount(aName);
- acc.setHiddenByStar(lastAccount.isHiddenByStar());
+ LedgerAccount acc = profile.tryLoadAccount(db, aName);
+ if (acc == null) {
+ acc = new LedgerAccount(aName);
+ acc.setHiddenByStar(lastAccount.isHiddenByStar());
+ acc.setExpanded(!lastAccount.hasSubAccounts() ||
+ lastAccount.isExpanded());
+ }
acc.setHasSubAccounts(true);
+ acc.removeAmounts(); // filled below when amounts are parsed
if ((!onlyStarred || !acc.isHiddenByStar()) &&
acc.isVisible(accountList)) accountList.add(acc);
L(String.format("gap-filling with %s", aName));
accountNames.put(aName, null);
profile.storeAccount(db, acc);
+ syntheticAccounts.put(aName, acc);
}
}
if (currency == null) currency = "";
value = value.replace(',', '.');
L("curr=" + currency + ", value=" + value);
+ final float val = Float.parseFloat(value);
profile.storeAccountValue(db, lastAccount.getName(), currency,
- Float.valueOf(value));
- lastAccount.addAmount(Float.parseFloat(value), currency);
+ val);
+ lastAccount.addAmount(val, currency);
+ for (LedgerAccount syn : syntheticAccounts.values()) {
+ syn.addAmount(val, currency);
+ profile.storeAccountValue(db, syn.getName(), currency, val);
+ }
}
if (match_found) {
else acc.removeAmounts();
profile.storeAccount(db, acc);
- for (ParsedBalance b : parsedAccount.getAebalance()) {
- profile.storeAccountValue(db, acc.getName(), b.getAcommodity(),
- b.getAquantity().asFloat());
+ String lastCurrency = null;
+ float lastCurrencyAmount = 0;
+ for (ParsedBalance b : parsedAccount.getAibalance()) {
+ final String currency = b.getAcommodity();
+ final float amount = b.getAquantity().asFloat();
+ if (currency.equals(lastCurrency)) lastCurrencyAmount += amount;
+ else {
+ if (lastCurrency != null) {
+ profile.storeAccountValue(db, acc.getName(), lastCurrency,
+ lastCurrencyAmount);
+ acc.addAmount(lastCurrencyAmount, lastCurrency);
+ }
+ lastCurrency = currency;
+ lastCurrencyAmount = amount;
+ }
+ }
+ if (lastCurrency != null) {
+ profile.storeAccountValue(db, acc.getName(), lastCurrency,
+ lastCurrencyAmount);
+ acc.addAmount(lastCurrencyAmount, lastCurrency);
}
if (acc.isVisible(accountList)) accountList.add(acc);
int processedTransactionCount = 0;
+ DetectedTransactionOrder transactionOrder = DetectedTransactionOrder.UNKNOWN;
+ int orderAccumulator = 0;
+ int lastTransactionId = 0;
+
while (true) {
throwIfCancelled();
ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
throwIfCancelled();
if (parsedTransaction == null) break;
+
LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
+ if (transaction.getId() > lastTransactionId) orderAccumulator++;
+ else orderAccumulator--;
+ lastTransactionId = transaction.getId();
+ if (transactionOrder == DetectedTransactionOrder.UNKNOWN) {
+ if (orderAccumulator > 30) {
+ transactionOrder = DetectedTransactionOrder.FILE;
+ debug("rtt", String.format(
+ "Detected native file order after %d transactions (factor %d)",
+ processedTransactionCount, orderAccumulator));
+ progress.setTotal(Data.transactions.size());
+ }
+ else if (orderAccumulator < -30) {
+ transactionOrder = DetectedTransactionOrder.REVERSE_CHRONOLOGICAL;
+ debug("rtt", String.format(
+ "Detected reverse chronological order after %d transactions (factor %d)",
+ processedTransactionCount, orderAccumulator));
+ }
+ }
+
if (transaction.existsInDb(db)) {
profile.markTransactionAsPresent(db, transaction);
matchedTransactionsCount++;
- if (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT) {
+ if ((transactionOrder == DetectedTransactionOrder.REVERSE_CHRONOLOGICAL) &&
+ (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT))
+ {
profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
progress.setTotal(progress.getProgress());
publishProgress(progress);
progress.setTotal(maxTransactionId);
}
- if ((progress.getTotal() == Progress.INDETERMINATE) ||
- (progress.getTotal() < transaction.getId()))
+
+ if ((transactionOrder != DetectedTransactionOrder.UNKNOWN) &&
+ ((progress.getTotal() == Progress.INDETERMINATE) ||
+ (progress.getTotal() < transaction.getId())))
progress.setTotal(transaction.getId());
progress.setProgress(++processedTransactionCount);
return true;
}
+
@SuppressLint("DefaultLocale")
@Override
protected String doInBackground(Void... params) {
MobileLedgerProfile profile = Data.profile.get();
- Data.backgroundTaskCount.incrementAndGet();
+ Data.backgroundTaskStarted();
try {
if (!retrieveAccountList(profile) || !retrieveTransactionList(profile))
return retrieveTransactionListLegacy(profile);
return "Operation cancelled";
}
finally {
- Data.backgroundTaskCount.decrementAndGet();
+ Data.backgroundTaskFinished();
}
}
private MainActivity getContext() {
private void throwIfCancelled() {
if (isCancelled()) throw new OperationCanceledException(null);
}
+ enum DetectedTransactionOrder {UNKNOWN, REVERSE_CHRONOLOGICAL, FILE}
private enum ParserState {
EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION,