-
Notifications
You must be signed in to change notification settings - Fork 12
/
ReplaceCommand.java
491 lines (442 loc) · 18.4 KB
/
ReplaceCommand.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/*************************************************************************\
* Copyright (C) 2009-2015 Mennē Software Solutions, LLC
*
* This code is released as open source under the Apache 2.0 License:<br/>
* <a href="http://www.apache.org/licenses/LICENSE-2.0">
* http://www.apache.org/licenses/LICENSE-2.0</a><br />
\*************************************************************************/
package com.moneydance.modules.features.findandreplace;
import com.infinitekind.moneydance.model.*;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <p>Replace data according to the user's wishes.</p>
*
* @author Kevin Menningen
* @version Build 94
* @since 1.0
*/
public class ReplaceCommand implements IFarCommand
{
// the input is immutable
private final Account _replaceCategory;
private final Long _replaceAmount; // stored as object so we can use null
private final CurrencyType _replaceCurrency;
private final boolean _parentsOnly;
private final String _replaceDescription;
private final boolean _replaceFoundDescriptionOnly;
private final String _replaceMemo;
private final boolean _replaceFoundMemoOnly;
private final String _replaceCheckNum;
private final boolean _replaceFoundCheckOnly;
private final Pattern _findPattern;
private final ReplaceTagCommandType _replaceTagType;
private final List<String> _replaceTagSet;
private final List<String> _userTagSet;
// the transaction changes as the command is applied to all selected transactions in the list
private FindResultsTableEntry _transaction;
ReplaceCommand(final Account category, final Long amount, final CurrencyType amountCurrency,
final boolean parentsOnly,
final String description, final boolean replaceFoundDescriptionOnly,
final String memo, final boolean replaceFoundMemoOnly,
final String check, final boolean replaceFoundCheckOnly,
final Pattern findPattern,
final ReplaceTagCommandType tagCommand, final List<String> tags,
final List<String> userTagSet)
{
_replaceCategory = category;
_replaceAmount = amount;
_replaceCurrency = amountCurrency;
_parentsOnly = parentsOnly;
_replaceDescription = description;
_replaceFoundDescriptionOnly = replaceFoundDescriptionOnly;
_replaceMemo = memo;
_replaceFoundMemoOnly = replaceFoundMemoOnly;
_replaceCheckNum = check;
_replaceFoundCheckOnly = replaceFoundCheckOnly;
_findPattern = findPattern;
_replaceTagType = tagCommand;
_replaceTagSet = tags;
_userTagSet = userTagSet;
_transaction = null;
}
public void setTransactionEntry(final FindResultsTableEntry entry)
{
_transaction = entry;
}
public Account getPreviewCategory()
{
if (_transaction == null)
{
// not applied to a transaction, no preview
return null;
}
if (_parentsOnly && (_transaction.getParentTxn().getSplitCount() > 1))
{
// this is a parent transaction with more than one split -- no category
return null;
}
// the default is to make no changes. this check is to see if any category should be
// returned at all for the transaction type -- if not, then return nothing
final Account category = FarUtil.getTransactionCategory(_transaction.getSplitTxn());
if (category != null)
{
if (_transaction.getSplitTxn().getAccount().equals(_replaceCategory) ||
_transaction.getParentTxn().getAccount().equals(_replaceCategory))
{
// The transaction cannot be replaced because either the category is already set
// to that account, or it's the same as the other side and you shouldn't have the
// account on both sides
return null;
}
return _replaceCategory;
}
// something else is not right, skip the category
return null;
}
public Long getPreviewAmount()
{
if (_transaction == null)
{
// not applied to a transaction, no preview
return null;
}
if (_parentsOnly && (_transaction.getParentTxn().getSplitCount() > 1))
{
// this is a parent transaction with more than one split -- can't change amount
return null;
}
return _replaceAmount;
}
public CurrencyType getAmountCurrency()
{
if (_replaceCurrency == null)
{
return (_transaction == null) ? null : _transaction.getCurrencyType();
}
return _replaceCurrency;
}
public String getPreviewDescription(final boolean useParent)
{
if (_transaction == null)
{
// not applied to a transaction, no preview
return null;
}
final String originalText;
if (useParent || _parentsOnly)
{
originalText = _transaction.getParentTxn().getDescription();
}
else
{
originalText = _transaction.getSplitTxn().getDescription();
}
final String replacedText = applyTextReplace(originalText, _replaceDescription,
_replaceFoundDescriptionOnly);
if ((originalText != null) && originalText.equals(replacedText))
{
// no change in the text, therefore no preview
return null;
}
return replacedText;
}
public String getPreviewMemo()
{
if (_transaction == null)
{
// not applied to a transaction, no preview
return null;
}
return applyTextReplace(_transaction.getParentTxn().getMemo(), _replaceMemo, _replaceFoundMemoOnly);
}
public String getPreviewCheckNumber()
{
if (_transaction == null)
{
// not applied to a transaction, no preview
return null;
}
return applyTextReplace(_transaction.getParentTxn().getCheckNumber(), _replaceCheckNum, _replaceFoundCheckOnly);
}
public List<String> getPreviewTags()
{
if ((_transaction == null) || (_replaceTagSet == null))
{
// not applied to a transaction, no preview, or no tags replaced
return null;
}
// There can be tags on both sides: split and parent. However the parent side is hard to
// get to (Show Other Side, then edit tags) and has some UI issues for the user. Currently
// we only support tags on the split.
final List<String> baseTags = FarUtil.getTransactionTags(_transaction.getSplitTxn(), _userTagSet);
return getChangedTagSet(baseTags);
}
/**
* Actually change the transaction.
* @return True if a change was made, false otherwise.
*/
public boolean execute()
{
if (_transaction == null)
{
// nothing to do
return false;
}
boolean changed = false;
Account previousCategory = null;
// the amount or category should not be changed if Consolidate Splits is turned on and the parent transaction
// has more than one split
final boolean skipAmount = _parentsOnly && (_transaction.getParentTxn().getSplitCount() > 1);
if (!skipAmount && (_replaceCategory != null))
{
// Only apply category replacement to splits. Do not replace if the category is already
// the same, or if the transaction's other side is the same category (you can't have
// the same account on both sides of a split)
if (!_transaction.getSplitTxn().getAccount().equals(_replaceCategory) &&
!_transaction.getParentTxn().getAccount().equals(_replaceCategory))
{
changed = true;
previousCategory = _transaction.getSplitTxn().getAccount();
_transaction.getSplitTxn().setAccount(_replaceCategory);
}
}
final boolean newCategory = (previousCategory != null) && (_replaceCategory != null);
if (!skipAmount && (_replaceAmount != null))
{
// only apply amount changes to splits because it is too complicated to change the
// amount for a parent transaction with multiple splits
// If we replace both the category AND the amount, we have to assume the user put the amount
// in the target currency, and no conversion takes place
final SplitTxn split = _transaction.getSplitTxn();
// check if the target category has changed to == new rate
long value = _replaceAmount.longValue();
CurrencyType targetCurr = split.getAccount().getCurrencyType();
CurrencyType parentCurr = _transaction.getParentTxn().getAccount().getCurrencyType();
changed = true;
if (newCategory) {
targetCurr = _replaceCategory.getCurrencyType();
}
// convert the replacement amount into the category's currency
long splitAmount = Math.abs(CurrencyUtil.convertValue(value, _replaceCurrency, targetCurr, split.getDateInt()));
long parentAmount = Math.abs(CurrencyUtil.convertValue(value, _replaceCurrency, parentCurr, split.getDateInt()));
split.setAmount(-splitAmount, -parentAmount);
}
else if (newCategory)
{
// we have not changed the amount, but the category has been changed, so do a currency conversion
CurrencyType sourceCurr = previousCategory.getCurrencyType();
CurrencyType targetCurr = _replaceCategory.getCurrencyType();
if (!sourceCurr.equals(targetCurr))
{
final SplitTxn split = _transaction.getSplitTxn();
CurrencyType parentCurr = _transaction.getParentTxn().getAccount().getCurrencyType();
long parentAmount = -split.getParentAmount();
long newSplitAmount = CurrencyUtil.convertValue(parentAmount, parentCurr, targetCurr, _transaction.getParentTxn().getDateInt());
split.setAmount(newSplitAmount, parentAmount);
}
}
if (_replaceDescription != null)
{
// independently replace the split and the parent description, ss long as consolidate splits is off
if (!_parentsOnly)
{
final String splitDescription = _transaction.getSplitTxn().getDescription();
if (splitDescription != null)
{
String newDescription = applyTextReplace(splitDescription, _replaceDescription,
_replaceFoundDescriptionOnly);
if (!splitDescription.equals(newDescription))
{
changed = true;
_transaction.getSplitTxn().setDescription(newDescription);
}
}
else if (!_replaceFoundDescriptionOnly)
{
// blow in the new description since it is blank
changed = true;
_transaction.getSplitTxn().setDescription(_replaceDescription);
}
}
// now check the parent
final ParentTxn parent = _transaction.getParentTxn();
if ((parent != null) && (_parentsOnly || (parent.getSplitCount() == 1)))
{
final String parentDescription = _transaction.getParentTxn().getDescription();
if (parentDescription != null)
{
String newDescription = applyTextReplace(parentDescription,
_replaceDescription,
_replaceFoundDescriptionOnly);
if (!parentDescription.equals(newDescription))
{
changed = true;
_transaction.getParentTxn().setDescription(newDescription);
}
}
else if (!_replaceFoundDescriptionOnly)
{
// blow in the new description since it is blank
changed = true;
_transaction.getParentTxn().setDescription(_replaceDescription);
}
}
}
if (_replaceMemo != null)
{
// memo only applies to parent transactions
final ParentTxn parent = _transaction.getParentTxn();
if (parent != null)
{
final String replacementText = applyTextReplace(parent.getMemo(), _replaceMemo,
_replaceFoundMemoOnly);
if (parent.getMemo() == null)
{
changed = true;
}
else if (!parent.getMemo().equals(replacementText))
{
changed = true;
}
parent.setMemo(replacementText);
}
}
if (_replaceCheckNum != null)
{
// check number only applies to parent transactions
final ParentTxn parent = _transaction.getParentTxn();
if (parent != null)
{
final String replacementText = applyTextReplace(parent.getCheckNumber(),
_replaceCheckNum,
_replaceFoundCheckOnly);
if (parent.getCheckNumber() == null)
{
changed = true;
}
else if (!parent.getCheckNumber().equals(replacementText))
{
changed = true;
}
parent.setCheckNumber(replacementText);
}
}
if (_replaceTagSet != null)
{
// There can be tags on both sides: split and parent. However the parent side is hard to
// get to (Show Other Side, then edit tags) and has some UI issues for the user. Currently
// we only support tags on the split.
final SplitTxn split = _transaction.getSplitTxn();
final List<String> existingTags = FarUtil.getTransactionTags(split, _userTagSet);
final List<String> newTags = getChangedTagSet(existingTags);
if (newTags != null)
{
if (!existingTags.equals(newTags))
{
changed = true;
}
split.setKeywords(newTags);
}
}
return changed;
} // execute()
///////////////////////////////////////////////////////////////////////////////////////////////
// Package Private Methods
///////////////////////////////////////////////////////////////////////////////////////////////
AbstractTxn getParentTransaction() {
return _transaction.getParentTxn();
}
///////////////////////////////////////////////////////////////////////////////////////////////
// Private Methods
///////////////////////////////////////////////////////////////////////////////////////////////
private String applyTextReplace(final String originalText, final String replacementText,
final boolean replaceFoundOnly)
{
if (replaceFoundOnly)
{
Matcher result = _findPattern.matcher(originalText);
if (result.find())
{
return result.replaceAll(replacementText);
}
return originalText;
}
// replace the entire text
return replacementText;
}
private List<String> getChangedTagSet(final List<String> baseTags)
{
if (_replaceTagSet == null)
{
// invalid
return null;
}
final List<String> newTags;
if (ReplaceTagCommandType.ADD.equals(_replaceTagType))
{
if (baseTags != null)
{
newTags = new ArrayList<String>(baseTags);
}
else
{
newTags = new ArrayList<String>();
}
for (final String addTag : _replaceTagSet)
{
boolean found = false;
for (final String existing : newTags) {
if (existing.equals(addTag)) {
found = true;
break;
}
}
if (!found)
{
// safe to add
newTags.add(addTag);
}
} // for addTag
}
else if (ReplaceTagCommandType.REMOVE.equals(_replaceTagType))
{
if (baseTags != null)
{
newTags = new ArrayList<String>(baseTags);
}
else
{
newTags = new ArrayList<String>();
}
for (final String removeTag : _replaceTagSet)
{
String tagToDelete = null;
for (final String existing : newTags)
{
if (existing.equals(removeTag))
{
tagToDelete = existing;
break;
}
}
if (tagToDelete != null)
{
newTags.remove(tagToDelete);
}
} // for removeTag
}
else if (ReplaceTagCommandType.REPLACE.equals(_replaceTagType))
{
newTags = new ArrayList<String>(_replaceTagSet);
}
else
{
// invalid
return null;
}
return newTags;
}
}