mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 15:07:33 +08:00
insights explorer supports sub condition
This commit is contained in:
+198
-81
@@ -4,7 +4,9 @@ import { AccountType } from '@/core/account.ts';
|
||||
import { TransactionType } from '@/core/transaction.ts';
|
||||
import { ChartSortingType } from '@/core/statistics.ts';
|
||||
import {
|
||||
type TransactionExplorerSubConditionStartRelation,
|
||||
TransactionExplorerConditionRelation,
|
||||
TransactionExplorerSubConditionStartRelationPlaceholder,
|
||||
TransactionExplorerConditionRelationPriority,
|
||||
TransactionExplorerConditionFieldType,
|
||||
TransactionExplorerConditionField,
|
||||
@@ -339,6 +341,10 @@ export class TransactionExplorerQuery {
|
||||
);
|
||||
}
|
||||
|
||||
public addSubConditionEnd(): TransactionExplorerConditionWithRelation {
|
||||
return new TransactionExplorerConditionWithRelation(new TransactionExplorerUndefinedCondition(), TransactionExplorerConditionRelation.SubEnd);
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
if (!this.conditions || this.conditions.length < 1) {
|
||||
return true;
|
||||
@@ -434,7 +440,7 @@ export class TransactionExplorerQuery {
|
||||
return finalTokens;
|
||||
}
|
||||
|
||||
const operatorStack: TransactionExplorerConditionRelation[] = [];
|
||||
const operatorStack: (TransactionExplorerConditionRelation | TransactionExplorerSubConditionStartRelation)[] = [];
|
||||
const firstCondition = this.conditions[0] as TransactionExplorerConditionWithRelation;
|
||||
|
||||
if (firstCondition.relation !== TransactionExplorerConditionRelation.First) {
|
||||
@@ -452,22 +458,70 @@ export class TransactionExplorerQuery {
|
||||
throw new Error('only the first condition can have relation "first"');
|
||||
}
|
||||
|
||||
const currentOperator = item.relation;
|
||||
if (item.relation === TransactionExplorerConditionRelation.SubEnd) {
|
||||
while (operatorStack.length > 0) {
|
||||
const topOperator = operatorStack.pop();
|
||||
|
||||
while (operatorStack.length > 0) {
|
||||
const topOperator = operatorStack[operatorStack.length - 1];
|
||||
const isAndOrOperator = topOperator === TransactionExplorerConditionRelation.And || topOperator === TransactionExplorerConditionRelation.Or;
|
||||
if (topOperator === TransactionExplorerSubConditionStartRelationPlaceholder) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isAndOrOperator && TransactionExplorerConditionRelationPriority[topOperator] >= TransactionExplorerConditionRelationPriority[currentOperator]) {
|
||||
finalTokens.push(topOperator);
|
||||
operatorStack.pop();
|
||||
} else {
|
||||
break;
|
||||
const isAndOrOperator = topOperator === TransactionExplorerConditionRelation.And || topOperator === TransactionExplorerConditionRelation.Or;
|
||||
|
||||
if (isAndOrOperator) {
|
||||
finalTokens.push(topOperator);
|
||||
} else {
|
||||
throw new Error('invalid operator in stack');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // And, Or, AndSub, OrSub
|
||||
let currentOperator: TransactionExplorerConditionRelation.And | TransactionExplorerConditionRelation.Or;
|
||||
let startNewSubCondition = false;
|
||||
|
||||
operatorStack.push(currentOperator);
|
||||
finalTokens.push(item.condition);
|
||||
switch (item.relation) {
|
||||
case TransactionExplorerConditionRelation.AndSub:
|
||||
currentOperator = TransactionExplorerConditionRelation.And;
|
||||
startNewSubCondition = true;
|
||||
break;
|
||||
case TransactionExplorerConditionRelation.OrSub:
|
||||
currentOperator = TransactionExplorerConditionRelation.Or;
|
||||
startNewSubCondition = true;
|
||||
break;
|
||||
case TransactionExplorerConditionRelation.And:
|
||||
currentOperator = item.relation;
|
||||
break;
|
||||
case TransactionExplorerConditionRelation.Or:
|
||||
currentOperator = item.relation;
|
||||
break;
|
||||
default:
|
||||
throw new Error('invalid operator in stack');
|
||||
}
|
||||
|
||||
while (operatorStack.length > 0) {
|
||||
const topOperator = operatorStack[operatorStack.length - 1];
|
||||
|
||||
if (topOperator === TransactionExplorerSubConditionStartRelationPlaceholder) {
|
||||
break;
|
||||
}
|
||||
|
||||
const isAndOrOperator = topOperator === TransactionExplorerConditionRelation.And || topOperator === TransactionExplorerConditionRelation.Or;
|
||||
|
||||
if (isAndOrOperator && TransactionExplorerConditionRelationPriority[topOperator] >= TransactionExplorerConditionRelationPriority[currentOperator]) {
|
||||
finalTokens.push(topOperator);
|
||||
operatorStack.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
operatorStack.push(currentOperator);
|
||||
|
||||
if (startNewSubCondition) {
|
||||
operatorStack.push(TransactionExplorerSubConditionStartRelationPlaceholder);
|
||||
}
|
||||
|
||||
finalTokens.push(item.condition);
|
||||
}
|
||||
}
|
||||
|
||||
while (operatorStack.length > 0) {
|
||||
@@ -483,6 +537,25 @@ export class TransactionExplorerQuery {
|
||||
return finalTokens;
|
||||
}
|
||||
|
||||
public getConditionNestingDepths(): number[] {
|
||||
const depths: number[] = [];
|
||||
let depth = 0;
|
||||
|
||||
for (const item of this.conditions) {
|
||||
if (item.relation === TransactionExplorerConditionRelation.SubEnd) {
|
||||
depth--;
|
||||
depths.push(depth);
|
||||
} else if (item.relation === TransactionExplorerConditionRelation.AndSub || item.relation === TransactionExplorerConditionRelation.OrSub) {
|
||||
depth++;
|
||||
depths.push(depth);
|
||||
} else {
|
||||
depths.push(depth);
|
||||
}
|
||||
}
|
||||
|
||||
return depths;
|
||||
}
|
||||
|
||||
public clone(newId: string): TransactionExplorerQuery {
|
||||
const clonedConditions: TransactionExplorerConditionWithRelation[] = [];
|
||||
|
||||
@@ -527,6 +600,7 @@ export class TransactionExplorerQuery {
|
||||
const id: string = idFieldValue;
|
||||
const name: string = nameFieldValue;
|
||||
const conditions: TransactionExplorerConditionWithRelation[] = [];
|
||||
let conditionDepth = 0;
|
||||
|
||||
for (const [item, index] of itemAndIndex(conditionsFieldValue)) {
|
||||
const condition = TransactionExplorerConditionWithRelation.parse(item);
|
||||
@@ -541,9 +615,20 @@ export class TransactionExplorerQuery {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (condition.relation === TransactionExplorerConditionRelation.AndSub ||
|
||||
condition.relation === TransactionExplorerConditionRelation.OrSub) {
|
||||
conditionDepth++;
|
||||
} else if (condition.relation === TransactionExplorerConditionRelation.SubEnd) {
|
||||
conditionDepth--;
|
||||
}
|
||||
|
||||
conditions.push(condition);
|
||||
}
|
||||
|
||||
if (conditionDepth !== 0) {
|
||||
return null; // unbalanced parentheses
|
||||
}
|
||||
|
||||
return new TransactionExplorerQuery(id, name, conditions);
|
||||
}
|
||||
}
|
||||
@@ -609,6 +694,12 @@ export class TransactionExplorerConditionWithRelation {
|
||||
}
|
||||
|
||||
public toJsonObject(): unknown {
|
||||
if (this.relation === TransactionExplorerConditionRelation.SubEnd) {
|
||||
return {
|
||||
relation: this.relation
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
condition: {
|
||||
field: this.condition.field,
|
||||
@@ -620,86 +711,94 @@ export class TransactionExplorerConditionWithRelation {
|
||||
}
|
||||
|
||||
public static parse(data: unknown): TransactionExplorerConditionWithRelation | null {
|
||||
if (typeof data !== 'object' || !data || !('condition' in data) || !('relation' in data)) {
|
||||
if (typeof data !== 'object' || !data || !('relation' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionObject = data['condition'];
|
||||
const relation = data['relation'];
|
||||
|
||||
if (typeof conditionObject !== 'object' || !conditionObject || !('field' in conditionObject) || !('operator' in conditionObject) || !('value' in conditionObject) || typeof relation !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionField = conditionObject['field'];
|
||||
const conditionOperator = conditionObject['operator'] as TransactionExplorerConditionOperatorType;
|
||||
const conditionValue = conditionObject['value'];
|
||||
|
||||
let condition: TransactionExplorerCondition | null = null;
|
||||
|
||||
switch (conditionField) {
|
||||
case TransactionExplorerConditionField.TransactionType.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerTransactionTypeCondition(conditionValue as number[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.TransactionCategory.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerTransactionCategoryCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.SourceAccount.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerSourceAccountCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.DestinationAccount.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerDestinationAccountCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.SourceAmount.value:
|
||||
if (TransactionExplorerSourceAmountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
|
||||
condition = new TransactionExplorerSourceAmountCondition(conditionOperator as AmountConditionOperator, conditionValue as [number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.DestinationAmount.value:
|
||||
if (TransactionExplorerDestinationAmountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
|
||||
condition = new TransactionExplorerDestinationAmountCondition(conditionOperator as AmountConditionOperator, conditionValue as [number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.GeoLocation.value:
|
||||
if (TransactionExplorerGeoLocationCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerGeoLocationCondition(conditionOperator as GeoLocationConditionOperator, conditionValue as [number, number, number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.TransactionTag.value:
|
||||
if (TransactionExplorerTransactionTagCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerTransactionTagCondition(conditionOperator as TransactionTagConditionOperator, conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.Pictures.value:
|
||||
if (TransactionExplorerPicturesCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerPicturesCondition(conditionOperator as PicturesConditionOperator, conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.Description.value:
|
||||
if (TransactionExplorerDescriptionCondition.supportedOperators[conditionOperator] && typeof conditionValue === 'string') {
|
||||
condition = new TransactionExplorerDescriptionCondition(conditionOperator as DescriptionConditionOperator, conditionValue);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (relation === TransactionExplorerConditionRelation.First ||
|
||||
relation === TransactionExplorerConditionRelation.And || relation === TransactionExplorerConditionRelation.Or ||
|
||||
relation === TransactionExplorerConditionRelation.AndSub || relation === TransactionExplorerConditionRelation.OrSub) {
|
||||
if (!('condition' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionObject = data['condition'];
|
||||
|
||||
if (typeof conditionObject !== 'object' || !conditionObject || !('field' in conditionObject) || !('operator' in conditionObject) || !('value' in conditionObject) || typeof relation !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conditionField = conditionObject['field'];
|
||||
const conditionOperator = conditionObject['operator'] as TransactionExplorerConditionOperatorType;
|
||||
const conditionValue = conditionObject['value'];
|
||||
|
||||
switch (conditionField) {
|
||||
case TransactionExplorerConditionField.TransactionType.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerTransactionTypeCondition(conditionValue as number[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.TransactionCategory.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerTransactionCategoryCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.SourceAccount.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerSourceAccountCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.DestinationAccount.value:
|
||||
if (conditionOperator === TransactionExplorerConditionOperatorType.In && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerDestinationAccountCondition(conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.SourceAmount.value:
|
||||
if (TransactionExplorerSourceAmountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
|
||||
condition = new TransactionExplorerSourceAmountCondition(conditionOperator as AmountConditionOperator, conditionValue as [number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.DestinationAmount.value:
|
||||
if (TransactionExplorerDestinationAmountCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
|
||||
condition = new TransactionExplorerDestinationAmountCondition(conditionOperator as AmountConditionOperator, conditionValue as [number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.GeoLocation.value:
|
||||
if (TransactionExplorerGeoLocationCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerGeoLocationCondition(conditionOperator as GeoLocationConditionOperator, conditionValue as [number, number, number, number]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.TransactionTag.value:
|
||||
if (TransactionExplorerTransactionTagCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerTransactionTagCondition(conditionOperator as TransactionTagConditionOperator, conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.Pictures.value:
|
||||
if (TransactionExplorerPicturesCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
|
||||
condition = new TransactionExplorerPicturesCondition(conditionOperator as PicturesConditionOperator, conditionValue as string[]);
|
||||
}
|
||||
break;
|
||||
case TransactionExplorerConditionField.Description.value:
|
||||
if (TransactionExplorerDescriptionCondition.supportedOperators[conditionOperator] && typeof conditionValue === 'string') {
|
||||
condition = new TransactionExplorerDescriptionCondition(conditionOperator as DescriptionConditionOperator, conditionValue);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (relation === TransactionExplorerConditionRelation.SubEnd) {
|
||||
condition = new TransactionExplorerUndefinedCondition();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (condition === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (relation !== TransactionExplorerConditionRelation.First && relation !== TransactionExplorerConditionRelation.And && relation !== TransactionExplorerConditionRelation.Or) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TransactionExplorerConditionWithRelation(condition, relation);
|
||||
}
|
||||
}
|
||||
@@ -714,6 +813,24 @@ export interface TransactionExplorerCondition<T = TransactionExplorerConditionFi
|
||||
toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>): string;
|
||||
}
|
||||
|
||||
export class TransactionExplorerUndefinedCondition implements TransactionExplorerCondition {
|
||||
public readonly field = TransactionExplorerConditionFieldType.Undefined;
|
||||
public readonly operator = TransactionExplorerConditionOperatorType.Equals;
|
||||
public value = '';
|
||||
|
||||
public getValueForStore(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public match(transaction: TransactionInsightDataItem): boolean {
|
||||
return !!transaction;
|
||||
}
|
||||
|
||||
public toExpression(): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionExplorerTransactionTypeCondition implements TransactionExplorerCondition<TransactionExplorerConditionFieldType.TransactionType, number[]> {
|
||||
public static readonly supportedOperators: PartialRecord<TransactionExplorerConditionOperatorType, true> = {
|
||||
[TransactionExplorerConditionOperatorType.In]: true
|
||||
|
||||
Reference in New Issue
Block a user