diff --git a/src/consts/timezone.ts b/src/consts/timezone.ts index de974714..541d6791 100644 --- a/src/consts/timezone.ts +++ b/src/consts/timezone.ts @@ -600,3 +600,6 @@ export const ALL_TIMEZONES: TimezoneInfo[] = [ timezoneName: 'Pacific/Kiritimati' } ]; + +export const WESTERNMOST_TIMEZONE_UTC_OFFSET: number = -720; // Etc/GMT+12 (UTC-12:00) +export const EASTERNMOST_TIMEZONE_UTC_OFFSET: number = 840; // Pacific/Kiritimati (UTC+14:00) diff --git a/src/core/datetime.ts b/src/core/datetime.ts index 8046b036..5a0fa92c 100644 --- a/src/core/datetime.ts +++ b/src/core/datetime.ts @@ -31,6 +31,7 @@ export interface DateTime { isLocalizedCalendarFirstDayOfMonth(options: DateTimeFormatOptions): boolean; getGregorianCalendarYearDashMonthDashDay(): TextualYearMonthDay; getGregorianCalendarYearDashMonth(): TextualYearMonth; + getMaxDayOfGregorianCalendarMonth(): number; getWeekDay(): WeekDay; getWeekDayDisplayName(options: DateTimeFormatOptions): string getWeekDayDisplayShortName(options: DateTimeFormatOptions): string; diff --git a/src/core/explorer.ts b/src/core/explorer.ts index 6519e10a..2f02e901 100644 --- a/src/core/explorer.ts +++ b/src/core/explorer.ts @@ -25,6 +25,11 @@ export const TransactionExplorerConditionRelationPriority: Record = {}; + public static readonly TransactionTimeDayOfWeek = new TransactionExplorerConditionField('Transaction Day of Week', TransactionExplorerConditionFieldType.TransactionTimeDayOfWeek); + public static readonly TransactionTimeDayOfMonth = new TransactionExplorerConditionField('Transaction Day of Month', TransactionExplorerConditionFieldType.TransactionTimeDayOfMonth) + public static readonly TransactionTimeMonthOfYear = new TransactionExplorerConditionField('Transaction Month of Year', TransactionExplorerConditionFieldType.TransactionTimeMonthOfYear); + public static readonly TransactionTimeHourOfDay = new TransactionExplorerConditionField('Transaction Hour of Day', TransactionExplorerConditionFieldType.TransactionTimeHourOfDay); + public static readonly TransactionTimezone = new TransactionExplorerConditionField('Transaction Timezone', TransactionExplorerConditionFieldType.TransactionTimezone); public static readonly TransactionType = new TransactionExplorerConditionField('Transaction Type', TransactionExplorerConditionFieldType.TransactionType); public static readonly TransactionCategory = new TransactionExplorerConditionField('Category', TransactionExplorerConditionFieldType.TransactionCategory); public static readonly SourceAccount = new TransactionExplorerConditionField('Source Account', TransactionExplorerConditionFieldType.SourceAccount); @@ -95,6 +105,8 @@ export enum TransactionExplorerConditionOperatorType { NotEndsWith = 'notEndsWith', RegexMatch = 'regexMatch', NotRegexMatch = 'notRegexMatch', + MinuteOffsetBetween = 'minuteOffsetBetween', + MinuteOffsetNotBetween = 'minuteOffsetNotBetween', LatitudeBetween = 'latitudeBetween', LatitudeNotBetween = 'latitudeNotBetween', LongitudeBetween = 'longitudeBetween', @@ -127,6 +139,8 @@ export class TransactionExplorerConditionOperator implements NameValue { public static readonly NotEndsWith = new TransactionExplorerConditionOperator('Does not end with', TransactionExplorerConditionOperatorType.NotEndsWith); public static readonly RegexMatch = new TransactionExplorerConditionOperator('Matches regex', TransactionExplorerConditionOperatorType.RegexMatch); public static readonly NotRegexMatch = new TransactionExplorerConditionOperator('Does not match regex', TransactionExplorerConditionOperatorType.NotRegexMatch); + public static readonly MinuteOffsetBetween = new TransactionExplorerConditionOperator('Minute offset is between', TransactionExplorerConditionOperatorType.MinuteOffsetBetween); + public static readonly MinuteOffsetNotBetween = new TransactionExplorerConditionOperator('Minute offset is not between', TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween); public static readonly LatitudeBetween = new TransactionExplorerConditionOperator('Latitude is between', TransactionExplorerConditionOperatorType.LatitudeBetween); public static readonly LatitudeNotBetween = new TransactionExplorerConditionOperator('Latitude is not between', TransactionExplorerConditionOperatorType.LatitudeNotBetween); public static readonly LongitudeBetween = new TransactionExplorerConditionOperator('Longitude is between', TransactionExplorerConditionOperatorType.LongitudeBetween); diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index 10eb4747..8851cd50 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -51,6 +51,11 @@ import { NumeralSystem } from '@/core/numeral.ts'; +import { + WESTERNMOST_TIMEZONE_UTC_OFFSET, + EASTERNMOST_TIMEZONE_UTC_OFFSET, +} from '@/consts/timezone.ts'; + import { isFunction, isDefined, @@ -74,15 +79,12 @@ interface DateTimeFormatResult { type DateTimeTokenFormatFunction = (d: MomentDateTime, options: DateTimeFormatOptions) => DateTimeFormatResult; -const westernmostTimezoneUtcOffset: number = -720; // Etc/GMT+12 (UTC-12:00) -const easternmostTimezoneUtcOffset: number = 840; // Pacific/Kiritimati (UTC+14:00) - function getFixedTimezoneName(utcOffset: number): string { return `Fixed/Timezone${utcOffset}`; } (function initFixedTimezone(): void { - for (let utcOffset = westernmostTimezoneUtcOffset; utcOffset <= easternmostTimezoneUtcOffset; utcOffset += 15) { + for (let utcOffset = WESTERNMOST_TIMEZONE_UTC_OFFSET; utcOffset <= EASTERNMOST_TIMEZONE_UTC_OFFSET; utcOffset += 15) { const timezoneName = getFixedTimezoneName(utcOffset); if (moment.tz.zone(timezoneName)) { @@ -254,6 +256,10 @@ class MomentDateTime implements DateTime { return (this.instance.year() + '-' + (this.instance.month() + 1).toString().padStart(2, NumeralSystem.WesternArabicNumerals.digitZero)) as TextualYearMonth; } + public getMaxDayOfGregorianCalendarMonth(): number { + return this.instance.clone().endOf('month').date(); + } + public getWeekDay(): WeekDay { return WeekDay.valueOf(this.instance.day()) as WeekDay; } diff --git a/src/locales/de.json b/src/locales/de.json index e33160a8..4e0caa98 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} Stunde(n) und {minutes} Minute(n) hinter der Standardzeitzone", "hoursMinutesAheadOfDefaultTimezone": "{hours} Stunde(n) und {minutes} Minute(n) vor der Standardzeitzone", "monthDay": "{ordinal} Tag", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} Tage", "everyMultiDaysOfWeek": "Jeden {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Letzte 5 Jahre", "Previous Billing Cycle": "Vorheriger Abrechnungszeitraum", "Current Billing Cycle": "Aktueller Abrechnungszeitraum", + "Last day": "Last day", "Custom Date": "Benutzerdefiniertes Datum", "Start Date": "Startdatum", "End Date": "Enddatum", @@ -1573,6 +1575,8 @@ "Does not end with": "Endet nicht mit", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Breitengrad zwischen", "Latitude is not between": "Breitengrad nicht zwischen", "Longitude is between": "Längengrad zwischen", diff --git a/src/locales/en.json b/src/locales/en.json index d3655cfe..de1f424d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} hour(s) and {minutes} minutes behind default timezone", "hoursMinutesAheadOfDefaultTimezone": "{hours} hour(s) and {minutes} minutes ahead of default timezone", "monthDay": "{ordinal} day", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} days", "everyMultiDaysOfWeek": "Every {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Recent 5 years", "Previous Billing Cycle": "Previous Billing Cycle", "Current Billing Cycle": "Current Billing Cycle", + "Last day": "Last day", "Custom Date": "Custom Date", "Start Date": "Start Date", "End Date": "End Date", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/es.json b/src/locales/es.json index 76d545e9..5821aab5 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} hora(s) y {minutes} minutos de retraso en la zona horaria predeterminada", "hoursMinutesAheadOfDefaultTimezone": "{hours} hora(s) y {minutes} minutos antes de la zona horaria predeterminada", "monthDay": "día {ordinal}", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} días", "everyMultiDaysOfWeek": "Cada {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Últimos 5 años", "Previous Billing Cycle": "Ciclo de facturación anterior", "Current Billing Cycle": "Ciclo de facturación actual", + "Last day": "Last day", "Custom Date": "Fecha personalizada", "Start Date": "Fecha de Inicio", "End Date": "Fecha de Finalización", @@ -1573,6 +1575,8 @@ "Does not end with": "No termina en", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/fr.json b/src/locales/fr.json index ef6a31f0..530cedd7 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} heure(s) et {minutes} minutes de retard sur le fuseau horaire par défaut", "hoursMinutesAheadOfDefaultTimezone": "{hours} heure(s) et {minutes} minutes d'avance sur le fuseau horaire par défaut", "monthDay": "{ordinal}e jour", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} jours", "everyMultiDaysOfWeek": "Tous les {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "5 dernières années", "Previous Billing Cycle": "Cycle de facturation précédent", "Current Billing Cycle": "Cycle de facturation actuel", + "Last day": "Last day", "Custom Date": "Date personnalisée", "Start Date": "Date de début", "End Date": "Date de fin", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index d01574fa..17c11316 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -195,6 +195,7 @@ import { } from '@/lib/common.ts'; import { + getCurrentDateTime, formatCurrentTime, formatGregorianCalendarYearDashMonthDashDay, formatGregorianCalendarMonthDashDay, @@ -1059,6 +1060,20 @@ export function useI18n() { return getAllWeekdayNames('min'); } + function getAllMonths(): TypeAndDisplayName[] { + const ret: TypeAndDisplayName[] = []; + const allMonths = Month.values(); + + for (const month of allMonths) { + ret.push({ + type: month.month, + displayName: t(`datetime.${month.name}.long`) + }); + } + + return ret; + } + function getAllWeekDays(firstDayOfWeek?: WeekDayValue): TypeAndDisplayName[] { const ret: TypeAndDisplayName[] = []; const allWeekDays = WeekDay.values(); @@ -1088,6 +1103,51 @@ export function useI18n() { return ret; } + function getAllHours(): TypeAndDisplayName[] { + const ret: TypeAndDisplayName[] = []; + const now: DateTime = getCurrentDateTime(); + const format: string = getLocalizedShortTimeFormat(); + const options: DateTimeFormatOptions = getDateTimeFormatOptions(); + + for (let i = 0; i < 24; i++) { + const dateTime = now.set({ + hour: i, + minute: 0, + second: 0, + millisecond: 0 + }); + + ret.push({ + type: i, + displayName: dateTime.format(format, options) + }); + } + + return ret; + } + + function getAvailableMonthDays(daysInMonth: number, lastDaysOfMonth?: number): TypeAndDisplayName[] { + const ret: TypeAndDisplayName[] = []; + + for (let i = 1; i <= daysInMonth; i++) { + ret.push({ + type: i, + displayName: getMonthdayShortName(i), + }); + } + + if (isNumber(lastDaysOfMonth) && lastDaysOfMonth > 0) { + for (let i = -lastDaysOfMonth; i < 0; i++) { + ret.push({ + type: i, + displayName: (i === -1) ? t('Last day') : getMonthLastDayShortName(-i), + }); + } + } + + return ret; + } + function getLocalizedDateTimeFormats(type: string, allFormatMap: Record, allFormatArray: T[], languageDefaultTypeNameKey: string, systemDefaultFormatType: T, numeralSystem: NumeralSystem, calendarType?: CalendarType): LocalizedDateTimeFormat[] { const defaultFormat = getLocalizedDateTimeFormat(type, allFormatMap, allFormatArray, LANGUAGE_DEFAULT_DATE_TIME_FORMAT_VALUE, languageDefaultTypeNameKey, systemDefaultFormatType); const ret: LocalizedDateTimeFormat[] = []; @@ -1606,6 +1666,12 @@ export function useI18n() { }); } + function getMonthLastDayShortName(ordinal: number): string { + return t('format.misc.lastMonthDay', { + ordinal: getMonthdayOrdinal(ordinal) + }); + } + function getWeekdayShortName(weekDay: WeekDay): string { return t(`datetime.${weekDay.name}.short`); } @@ -2411,7 +2477,10 @@ export function useI18n() { getAllLongWeekdayNames, getAllShortWeekdayNames, getAllMinWeekdayNames, + getAllMonths, getAllWeekDays, + getAllHours, + getAvailableMonthDays, getAllCalendarDisplayTypes: () => getAllLocalizedCalendarTypes(CalendarDisplayType.values(), CalendarDisplayType.parse(t('default.calendarDisplayType')), CalendarDisplayType.Default, CalendarDisplayType.LanguageDefaultType), getAllDateDisplayTypes: () => getAllLocalizedCalendarTypes(DateDisplayType.values(), DateDisplayType.parse(t('default.dateDisplayType')), DateDisplayType.Default, DateDisplayType.LanguageDefaultType), getAllLongDateFormats: (numeralSystem: NumeralSystem, calendarType: CalendarType) => getLocalizedDateTimeFormats('longDate', LongDateFormat.all(), LongDateFormat.values(), 'longDateFormat', LongDateFormat.Default, numeralSystem, calendarType), diff --git a/src/locales/it.json b/src/locales/it.json index fde88a6b..a0240d5b 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "Indietro di {hours} ore e {minutes} minuti rispetto al fuso orario standard", "hoursMinutesAheadOfDefaultTimezone": "Avanti di {hours} ore e {minutes} minuti rispetto al fuso orario standard", "monthDay": "{ordinal} giorno", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} giorni", "everyMultiDaysOfWeek": "Ogni {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Ultimi 5 anni", "Previous Billing Cycle": "Ciclo di fatturazione precedente", "Current Billing Cycle": "Ciclo di fatturazione corrente", + "Last day": "Last day", "Custom Date": "Data personalizzata", "Start Date": "Data di inizio", "End Date": "Data di fine", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/ja.json b/src/locales/ja.json index 745d9cc8..4437f663 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "デフォルトのタイムゾーンより{hours}時間{minutes}分遅れています", "hoursMinutesAheadOfDefaultTimezone": "デフォルトのタイムゾーンから{hours}時間{minutes}分進んでいます", "monthDay": "{ordinal}日", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays}日間", "everyMultiDaysOfWeek": "毎{days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "直近5年", "Previous Billing Cycle": "以前の請求サイクル", "Current Billing Cycle": "現在の請求サイクル", + "Last day": "Last day", "Custom Date": "カスタム日付", "Start Date": "開始日", "End Date": "終了日", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/kn.json b/src/locales/kn.json index a79e98d9..21cad903 100644 --- a/src/locales/kn.json +++ b/src/locales/kn.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "ಡೀಫಾಲ್ಟ್ ಸಮಯ ವಲಯಕ್ಕಿಂತ {hours} ಗಂಟೆ ಹಾಗೂ {minutes} ನಿಮಿಷಗಳು ಹಿಂದೆ", "hoursMinutesAheadOfDefaultTimezone": "ಡೀಫಾಲ್ಟ್ ಸಮಯ ವಲಯಕ್ಕಿಂತ {hours} ಗಂಟೆ ಹಾಗೂ {minutes} ನಿಮಿಷಗಳು ಮುಂದೆ", "monthDay": "{ordinal} ದಿನ", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} ದಿನಗಳು", "everyMultiDaysOfWeek": "ಪ್ರತಿ {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "ಇತ್ತೀಚಿನ 5 ವರ್ಷಗಳು", "Previous Billing Cycle": "ಹಿಂದಿನ ಬಿಲ್ಲಿಂಗ್ ಚಕ್ರ", "Current Billing Cycle": "ಪ್ರಸ್ತುತ ಬಿಲ್ಲಿಂಗ್ ಚಕ್ರ", + "Last day": "Last day", "Custom Date": "ಕಸ್ಟಮ್ ದಿನಾಂಕ", "Start Date": "ಪ್ರಾರಂಭ ದಿನಾಂಕ", "End Date": "ಅಂತ್ಯ ದಿನಾಂಕ", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/ko.json b/src/locales/ko.json index aecb8f91..0164fdbc 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "기본 시간대보다 {hours}시간 {minutes}분 느립니다", "hoursMinutesAheadOfDefaultTimezone": "기본 시간대보다 {hours}시간 {minutes}분 빠릅니다", "monthDay": "{ordinal}일", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays}일", "everyMultiDaysOfWeek": "매주 {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "최근 5년", "Previous Billing Cycle": "이전 청구 주기", "Current Billing Cycle": "현재 청구 주기", + "Last day": "Last day", "Custom Date": "사용자 지정 날짜", "Start Date": "시작 날짜", "End Date": "종료 날짜", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/nl.json b/src/locales/nl.json index 28ad4b59..62e1c323 100644 --- a/src/locales/nl.json +++ b/src/locales/nl.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} uur en {minutes} minuten achter standaardtijdzone", "hoursMinutesAheadOfDefaultTimezone": "{hours} uur en {minutes} minuten voor op standaardtijdzone", "monthDay": "{ordinal} dag", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} dagen", "everyMultiDaysOfWeek": "Elke {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Afgelopen 5 jaar", "Previous Billing Cycle": "Vorige factureringsperiode", "Current Billing Cycle": "Huidige factureringsperiode", + "Last day": "Last day", "Custom Date": "Aangepaste datum", "Start Date": "Begindatum", "End Date": "Einddatum", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index fc617095..35a4c162 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} hora(s) e {minutes} minutos atrás do fuso horário padrão", "hoursMinutesAheadOfDefaultTimezone": "{hours} hora(s) e {minutes} minutos à frente do fuso horário padrão", "monthDay": "{ordinal} dia", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} dias", "everyMultiDaysOfWeek": "A cada {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Últimos 5 anos", "Previous Billing Cycle": "Ciclo de Cobrança Anterior", "Current Billing Cycle": "Ciclo de Cobrança Atual", + "Last day": "Last day", "Custom Date": "Data Personalizada", "Start Date": "Data de Início", "End Date": "Data de Término", @@ -1573,6 +1575,8 @@ "Does not end with": "Não termina com", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude entre", "Latitude is not between": "Latitude não entre", "Longitude is between": "Longitude entre", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4fe2771f..57ffd3fb 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} час(ов) и {minutes} минут позади часового пояса по умолчанию", "hoursMinutesAheadOfDefaultTimezone": "{hours} час(ов) и {minutes} минут впереди часового пояса по умолчанию", "monthDay": "{ordinal} день", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} дней", "everyMultiDaysOfWeek": "Каждые {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Последние 5 лет", "Previous Billing Cycle": "Предыдущий расчетный период", "Current Billing Cycle": "Текущий расчетный период", + "Last day": "Last day", "Custom Date": "Выбрать дату", "Start Date": "Дата начала", "End Date": "Дата конца", @@ -1573,6 +1575,8 @@ "Does not end with": "Не заканчивается с", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/sl.json b/src/locales/sl.json index dd5faa5d..bab1261c 100644 --- a/src/locales/sl.json +++ b/src/locales/sl.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} ura(ur) in {minutes} minut za privzetim časovnim pasom", "hoursMinutesAheadOfDefaultTimezone": "{hours} ura(ur) in {minutes} minut po privzetem časovnem pasu", "monthDay": "{ordinal} dan", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} dni", "everyMultiDaysOfWeek": "Vsak {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Zadnjih 5 let", "Previous Billing Cycle": "Prejšnje obračunsko obdobje", "Current Billing Cycle": "Trenutno obračunsko obdobje", + "Last day": "Last day", "Custom Date": "Datum po meri", "Start Date": "Datum začetka", "End Date": "Datum konca", @@ -1573,6 +1575,8 @@ "Does not end with": "Se ne konča z", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/ta.json b/src/locales/ta.json index d7eae1ec..38887a11 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "இயல்பு நேர மண்டலத்தை விட {hours} மணி நேரம் {minutes} நிமிடங்கள் பின்னால்", "hoursMinutesAheadOfDefaultTimezone": "இயல்பு நேர மண்டலத்தை விட {hours} மணி நேரம் {minutes} நிமிடங்கள் முன்னால்", "monthDay": "{ordinal} நாள்", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} நாட்கள்", "everyMultiDaysOfWeek": "ஒவ்வொரு {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "சமீபத்திய 5 ஆண்டுகள்", "Previous Billing Cycle": "முந்தைய பில்லிங் சுழற்சி", "Current Billing Cycle": "தற்போதைய பில்லிங் சுழற்சி", + "Last day": "Last day", "Custom Date": "தனிப்பயன் தேதி", "Start Date": "தொடக்கம் தேதி", "End Date": "முடிவு தேதி", @@ -1573,6 +1575,8 @@ "Does not end with": "முடியவில்லை", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/th.json b/src/locales/th.json index d83b3959..932e9ef5 100644 --- a/src/locales/th.json +++ b/src/locales/th.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "ช้ากว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง {minutes} นาที", "hoursMinutesAheadOfDefaultTimezone": "เร็วกว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง {minutes} นาที", "monthDay": "วันที่ {ordinal}", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} วัน", "everyMultiDaysOfWeek": "ทุกๆ {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "5 ปีที่ผ่านมา", "Previous Billing Cycle": "รอบบิลก่อนหน้า", "Current Billing Cycle": "รอบบิลปัจจุบัน", + "Last day": "Last day", "Custom Date": "วันที่กำหนดเอง", "Start Date": "วันที่เริ่มต้น", "End Date": "วันที่สิ้นสุด", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/tr.json b/src/locales/tr.json index ce73daff..65f573bd 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "Varsayılan saat diliminin {hours} saat {minutes} dakika gerisinde", "hoursMinutesAheadOfDefaultTimezone": "Varsayılan saat diliminin {hours} saat {minutes} dakika ilerisinde", "monthDay": "{ordinal} gün", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} gün", "everyMultiDaysOfWeek": "Her {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Son 5 yıl", "Previous Billing Cycle": "Önceki Fatura Dönemi", "Current Billing Cycle": "Mevcut Fatura Dönemi", + "Last day": "Last day", "Custom Date": "Özel Tarih", "Start Date": "Başlangıç Tarihi", "End Date": "Bitiş Tarihi", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/uk.json b/src/locales/uk.json index 051d83b1..9aa7ad80 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} год і {minutes} хв позаду часового поясу за замовчуванням", "hoursMinutesAheadOfDefaultTimezone": "{hours} год і {minutes} хв попереду часового поясу за замовчуванням", "monthDay": "{ordinal} день", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} днів", "everyMultiDaysOfWeek": "Кожні {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "Останні 5 років", "Previous Billing Cycle": "Попередній розрахунковий період", "Current Billing Cycle": "Поточний розрахунковий період", + "Last day": "Last day", "Custom Date": "Обрати дату", "Start Date": "Дата початку", "End Date": "Дата завершення", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/vi.json b/src/locales/vi.json index f373f432..6abec152 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "{hours} giờ và {minutes} phút sau múi giờ mặc định", "hoursMinutesAheadOfDefaultTimezone": "{hours} giờ và {minutes} phút trước múi giờ mặc định", "monthDay": "Ngày {ordinal}", + "lastMonthDay": "Last {ordinal} day", "eachMonthDayInMonthDays": "{ordinal}", "monthDays": "{multiMonthDays} ngày", "everyMultiDaysOfWeek": "Mỗi {days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "5 năm gần đây", "Previous Billing Cycle": "Previous Billing Cycle", "Current Billing Cycle": "Current Billing Cycle", + "Last day": "Last day", "Custom Date": "Ngày tùy chỉnh", "Start Date": "Start Date", "End Date": "End Date", @@ -1573,6 +1575,8 @@ "Does not end with": "Does not end with", "Matches regex": "Matches regex", "Does not match regex": "Does not match regex", + "Minute offset is between": "Minute offset is between", + "Minute offset is not between": "Minute offset is not between", "Latitude is between": "Latitude is between", "Latitude is not between": "Latitude is not between", "Longitude is between": "Longitude is between", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 048ea43f..9d4bb2f5 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "比默认时区晚{hours}小时{minutes}分", "hoursMinutesAheadOfDefaultTimezone": "比默认时区早{hours}小时{minutes}分", "monthDay": "{ordinal}日", + "lastMonthDay": "倒数第{ordinal}日", "eachMonthDayInMonthDays": "{ordinal}日", "monthDays": "{multiMonthDays}", "everyMultiDaysOfWeek": "每{days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "最近5年", "Previous Billing Cycle": "上个账单周期", "Current Billing Cycle": "当前账单周期", + "Last day": "倒数第1日", "Custom Date": "自定义日期", "Start Date": "开始日期", "End Date": "结束日期", @@ -1573,6 +1575,8 @@ "Does not end with": "结尾不是", "Matches regex": "正则匹配", "Does not match regex": "不匹配正则", + "Minute offset is between": "分钟偏移量介于", + "Minute offset is not between": "分钟偏移量不介于", "Latitude is between": "纬度介于", "Latitude is not between": "纬度不介于", "Longitude is between": "经度介于", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 2e24f7a9..ff81e3c9 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -119,6 +119,7 @@ "hoursMinutesBehindDefaultTimezone": "比預設時區晚{hours}小時{minutes}分", "hoursMinutesAheadOfDefaultTimezone": "比預設時區早{hours}小時{minutes}分", "monthDay": "{ordinal}日", + "lastMonthDay": "倒數第{ordinal}日", "eachMonthDayInMonthDays": "{ordinal}日", "monthDays": "{multiMonthDays}", "everyMultiDaysOfWeek": "每{days}", @@ -1540,6 +1541,7 @@ "Recent 5 years": "最近5年", "Previous Billing Cycle": "上個帳單週期", "Current Billing Cycle": "當前帳單週期", + "Last day": "倒數第1日", "Custom Date": "自訂日期", "Start Date": "開始日期", "End Date": "結束日期", @@ -1573,6 +1575,8 @@ "Does not end with": "結尾不是", "Matches regex": "正則匹配", "Does not match regex": "不匹配正則", + "Minute offset is between": "分钟偏移量介於", + "Minute offset is not between": "分钟偏移量不介於", "Latitude is between": "緯度介於", "Latitude is not between": "緯度不介於", "Longitude is between": "經度介於", diff --git a/src/models/explorer.ts b/src/models/explorer.ts index 1317a1fa..5fd8672b 100644 --- a/src/models/explorer.ts +++ b/src/models/explorer.ts @@ -1,4 +1,5 @@ import { type PartialRecord, itemAndIndex, keysIfValueEquals } from '@/core/base.ts'; +import { type DateTime } from '@/core/datetime.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { AccountType } from '@/core/account.ts'; import { TransactionType } from '@/core/transaction.ts'; @@ -300,6 +301,21 @@ export class TransactionExplorerQuery { let condition: TransactionExplorerCondition; switch (field) { + case TransactionExplorerConditionField.TransactionTimeDayOfWeek: + condition = new TransactionExplorerTransactionTimeDayOfWeekCondition(TransactionExplorerConditionOperatorType.In, []); + break; + case TransactionExplorerConditionField.TransactionTimeDayOfMonth: + condition = new TransactionExplorerTransactionTimeDayOfMonthCondition(TransactionExplorerConditionOperatorType.In, []); + break; + case TransactionExplorerConditionField.TransactionTimeMonthOfYear: + condition = new TransactionExplorerTransactionTimeMonthOfYearCondition(TransactionExplorerConditionOperatorType.In, []); + break; + case TransactionExplorerConditionField.TransactionTimeHourOfDay: + condition = new TransactionExplorerTransactionTimeHourOfDayCondition(TransactionExplorerConditionOperatorType.In, []); + break; + case TransactionExplorerConditionField.TransactionTimezone: + condition = new TransactionExplorerTransactionTimezoneCondition(TransactionExplorerConditionOperatorType.MinuteOffsetBetween, [ 0, 0 ]); + break; case TransactionExplorerConditionField.TransactionType: condition = new TransactionExplorerTransactionTypeCondition(TransactionExplorerConditionOperatorType.In, [ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]); break; @@ -345,7 +361,7 @@ export class TransactionExplorerQuery { return new TransactionExplorerConditionWithRelation(new TransactionExplorerUndefinedCondition(), TransactionExplorerConditionRelation.SubEnd); } - public match(transaction: TransactionInsightDataItem): boolean { + public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean { if (!this.conditions || this.conditions.length < 1) { return true; } @@ -370,7 +386,7 @@ export class TransactionExplorerQuery { throw new Error('invalid postfix expression'); } } else { - stack.push(token.match(transaction)); + stack.push(token.match(transaction, context)); } } @@ -646,6 +662,21 @@ export class TransactionExplorerConditionWithRelation { let operatorTypes: PartialRecord = {}; switch (this.condition.field) { + case TransactionExplorerConditionField.TransactionTimeDayOfWeek.value: + operatorTypes = TransactionExplorerTransactionTimeDayOfWeekCondition.supportedOperators; + break; + case TransactionExplorerConditionField.TransactionTimeDayOfMonth.value: + operatorTypes = TransactionExplorerTransactionTimeDayOfMonthCondition.supportedOperators; + break; + case TransactionExplorerConditionField.TransactionTimeMonthOfYear.value: + operatorTypes = TransactionExplorerTransactionTimeMonthOfYearCondition.supportedOperators; + break; + case TransactionExplorerConditionField.TransactionTimeHourOfDay.value: + operatorTypes = TransactionExplorerTransactionTimeHourOfDayCondition.supportedOperators; + break; + case TransactionExplorerConditionField.TransactionTimezone.value: + operatorTypes = TransactionExplorerTransactionTimezoneCondition.supportedOperators; + break; case TransactionExplorerConditionField.TransactionType.value: operatorTypes = TransactionExplorerTransactionTypeCondition.supportedOperators; break; @@ -736,6 +767,31 @@ export class TransactionExplorerConditionWithRelation { const conditionValue = conditionObject['value']; switch (conditionField) { + case TransactionExplorerConditionField.TransactionTimeDayOfWeek.value: + if (TransactionExplorerTransactionTimeDayOfWeekCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerTransactionTimeDayOfWeekCondition(conditionOperator as TransactionTimeDayOfWeekConditionOperator, conditionValue as number[]); + } + break; + case TransactionExplorerConditionField.TransactionTimeDayOfMonth.value: + if (TransactionExplorerTransactionTimeDayOfMonthCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerTransactionTimeDayOfMonthCondition(conditionOperator as TransactionTimeDayOfMonthConditionOperator, conditionValue as number[]); + } + break; + case TransactionExplorerConditionField.TransactionTimeMonthOfYear.value: + if (TransactionExplorerTransactionTimeMonthOfYearCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerTransactionTimeMonthOfYearCondition(conditionOperator as TransactionTimeMonthOfYearConditionOperator, conditionValue as number[]); + } + break; + case TransactionExplorerConditionField.TransactionTimeHourOfDay.value: + if (TransactionExplorerTransactionTimeHourOfDayCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { + condition = new TransactionExplorerTransactionTimeHourOfDayCondition(conditionOperator as TransactionTimeHourOfDayConditionOperator, conditionValue as number[]); + } + break; + case TransactionExplorerConditionField.TransactionTimezone.value: + if (TransactionExplorerTransactionTimezoneCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) { + condition = new TransactionExplorerTransactionTimezoneCondition(conditionOperator as TransactionTimezoneConditionOperator, conditionValue as [number, number]); + } + break; case TransactionExplorerConditionField.TransactionType.value: if (TransactionExplorerTransactionTypeCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) { condition = new TransactionExplorerTransactionTypeCondition(conditionOperator as TransactionTypeConditionOperator, conditionValue as number[]); @@ -803,13 +859,17 @@ export class TransactionExplorerConditionWithRelation { } } +export interface InsightsExplorerMatchContext { + getTransactionDateTime(): DateTime; +} + export interface TransactionExplorerCondition { readonly field: T; readonly operator: TransactionExplorerConditionOperatorType; value: V; getValueForStore(): V; - match(transaction: TransactionInsightDataItem): boolean; + match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean; toExpression(allCategoriesMap: Record, allAccountsMap: Record, allTagsMap: Record): string; } @@ -831,6 +891,233 @@ export class TransactionExplorerUndefinedCondition implements TransactionExplore } } +type TransactionTimeDayOfWeekConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + +export class TransactionExplorerTransactionTimeDayOfWeekCondition implements TransactionExplorerCondition { + public static readonly supportedOperators: PartialRecord = { + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true + }; + public readonly field = TransactionExplorerConditionFieldType.TransactionTimeDayOfWeek; + public readonly operator: TransactionTimeDayOfWeekConditionOperator = TransactionExplorerConditionOperatorType.In; + public value: number[]; + + constructor(operator: TransactionTimeDayOfWeekConditionOperator, value: number[]) { + this.operator = operator; + this.value = value; + } + + public getValueForStore(): number[] { + return this.value; + } + + public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean { + const transactionDateTime = context.getTransactionDateTime(); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transactionDateTime.getWeekDay().type); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transactionDateTime.getWeekDay().type); + } + + return false; + } + + public toExpression(): string { + const textualDayOfWeeks = this.value.join(', '); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `DAY_OF_WEEK(transaction_time) IN (${textualDayOfWeeks})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `DAY_OF_WEEK(transaction_time) NOT IN (${textualDayOfWeeks})`; + } else { + return ''; + } + } +} + +type TransactionTimeDayOfMonthConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + +export class TransactionExplorerTransactionTimeDayOfMonthCondition implements TransactionExplorerCondition { + public static readonly supportedOperators: PartialRecord = { + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true + }; + public readonly field = TransactionExplorerConditionFieldType.TransactionTimeDayOfMonth; + public readonly operator: TransactionTimeDayOfMonthConditionOperator = TransactionExplorerConditionOperatorType.In; + public value: number[]; + + constructor(operator: TransactionTimeDayOfMonthConditionOperator, value: number[]) { + this.operator = operator; + this.value = value; + } + + public getValueForStore(): number[] { + return this.value; + } + + public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean { + const transactionDateTime = context.getTransactionDateTime(); + const normalizedValue: number[] = this.value.map(day => day >= 0 ? day : transactionDateTime.getMaxDayOfGregorianCalendarMonth() + day + 1); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return normalizedValue.includes(transactionDateTime.getGregorianCalendarDay()); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !normalizedValue.includes(transactionDateTime.getGregorianCalendarDay()); + } + + return false; + } + + public toExpression(): string { + const textualDayOfMonths = this.value.join(', '); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `DAY(transaction_time) IN (${textualDayOfMonths})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `DAY(transaction_time) NOT IN (${textualDayOfMonths})`; + } else { + return ''; + } + } +} + +type TransactionTimeMonthOfYearConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + +export class TransactionExplorerTransactionTimeMonthOfYearCondition implements TransactionExplorerCondition { + public static readonly supportedOperators: PartialRecord = { + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true + }; + public readonly field = TransactionExplorerConditionFieldType.TransactionTimeMonthOfYear; + public readonly operator: TransactionTimeMonthOfYearConditionOperator = TransactionExplorerConditionOperatorType.In; + public value: number[]; + + constructor(operator: TransactionTimeMonthOfYearConditionOperator, value: number[]) { + this.operator = operator; + this.value = value; + } + + public getValueForStore(): number[] { + return this.value; + } + + public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean { + const transactionDateTime = context.getTransactionDateTime(); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transactionDateTime.getGregorianCalendarMonth()); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transactionDateTime.getGregorianCalendarMonth()); + } + + return false; + } + + public toExpression(): string { + const textualMonthOfYears = this.value.join(', '); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `MONTH(transaction_time) IN (${textualMonthOfYears})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `MONTH(transaction_time) NOT IN (${textualMonthOfYears})`; + } else { + return ''; + } + } +} + +type TransactionTimeHourOfDayConditionOperator = TransactionExplorerConditionOperatorType.In | + TransactionExplorerConditionOperatorType.NotIn; + +export class TransactionExplorerTransactionTimeHourOfDayCondition implements TransactionExplorerCondition { + public static readonly supportedOperators: PartialRecord = { + [TransactionExplorerConditionOperatorType.In]: true, + [TransactionExplorerConditionOperatorType.NotIn]: true + }; + public readonly field = TransactionExplorerConditionFieldType.TransactionTimeHourOfDay; + public readonly operator: TransactionTimeHourOfDayConditionOperator = TransactionExplorerConditionOperatorType.In; + public value: number[]; + + constructor(operator: TransactionTimeHourOfDayConditionOperator, value: number[]) { + this.operator = operator; + this.value = value; + } + + public getValueForStore(): number[] { + return this.value; + } + + public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean { + const transactionDateTime = context.getTransactionDateTime(); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return this.value.includes(transactionDateTime.getHour()); + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return !this.value.includes(transactionDateTime.getHour()); + } + + return false; + } + + public toExpression(): string { + const textualHourOfDays = this.value.join(', '); + + if (this.operator === TransactionExplorerConditionOperatorType.In) { + return `HOUR(transaction_time) IN (${textualHourOfDays})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) { + return `HOUR(transaction_time) NOT IN (${textualHourOfDays})`; + } else { + return ''; + } + } +} + +type TransactionTimezoneConditionOperator = TransactionExplorerConditionOperatorType.MinuteOffsetBetween | + TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween; + +export class TransactionExplorerTransactionTimezoneCondition implements TransactionExplorerCondition { + public static readonly supportedOperators: PartialRecord = { + [TransactionExplorerConditionOperatorType.MinuteOffsetBetween]: true, + [TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween]: true + }; + public readonly field = TransactionExplorerConditionFieldType.TransactionTimezone; + public readonly operator: TransactionTimezoneConditionOperator = TransactionExplorerConditionOperatorType.MinuteOffsetBetween; + public value: [number, number]; + + constructor(operator: TransactionTimezoneConditionOperator, value: [number, number]) { + this.operator = operator; + this.value = value; + } + + public getValueForStore(): [number, number] { + return this.value; + } + + public match(transaction: TransactionInsightDataItem): boolean { + if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetBetween) { + return transaction.utcOffset >= this.value[0] && transaction.utcOffset <= this.value[1]; + } else if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween) { + return transaction.utcOffset < this.value[0] || transaction.utcOffset > this.value[1]; + } + + return false; + } + + public toExpression(): string { + if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetBetween) { + return `(UTC_OFFSET(timezone) >= ${this.value[0]} AND UTC_OFFSET(timezone) <= ${this.value[1]})`; + } else if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween) { + return `(UTC_OFFSET(timezone) < ${this.value[0]} OR UTC_OFFSET(timezone) > ${this.value[1]})`; + } + + return ''; + } +} + type TransactionTypeConditionOperator = TransactionExplorerConditionOperatorType.In | TransactionExplorerConditionOperatorType.NotIn; diff --git a/src/stores/explorer.ts b/src/stores/explorer.ts index 34882cef..a316feb3 100644 --- a/src/stores/explorer.ts +++ b/src/stores/explorer.ts @@ -10,7 +10,7 @@ import { useExchangeRatesStore } from './exchangeRates.ts'; import { type BeforeResolveFunction, itemAndIndex, reversed, keys, values } from '@/core/base.ts'; import { NumeralSystem, AmountFilterType } from '@/core/numeral.ts'; -import { DateRangeScene, DateRange } from '@/core/datetime.ts'; +import { type DateTime, DateRangeScene, DateRange } from '@/core/datetime.ts'; import { TimezoneTypeForStatistics } from '@/core/timezone.ts'; import { AccountCategory } from '@/core/account.ts'; import { TransactionType } from '@/core/transaction.ts'; @@ -32,6 +32,7 @@ import { import { type InsightsExplorerNewDisplayOrderRequest, type InsightsExplorerInfoResponse, + type InsightsExplorerMatchContext, InsightsExplorer, InsightsExplorerBasicInfo } from '@/models/explorer.ts'; @@ -151,6 +152,20 @@ export const useExplorersStore = defineStore('explorers', () => { return result; })(); + function buildInsightsExplorerMatchContext(insightsExplorer: InsightsExplorer, transaction: TransactionInsightDataItem): InsightsExplorerMatchContext { + return { + getTransactionDateTime(): DateTime { + let transactionTimeUtfOffset: number | undefined = undefined; + + if (insightsExplorer.timezoneUsedForDateRange === TimezoneTypeForStatistics.TransactionTimezone.type) { + transactionTimeUtfOffset = transaction.utcOffset; + } + + return isDefined(transactionTimeUtfOffset) ? parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transactionTimeUtfOffset) : parseDateTimeFromUnixTime(transaction.time); + } + }; + } + function getDataCategoryInfo(timezoneUsedForDateRange: number, dimension: TransactionExplorerDataDimension, queryName: string, queryIndex: number, transaction: TransactionInsightDataItem): CategoriedInfo { let transactionTimeUtfOffset: number | undefined = undefined; @@ -600,12 +615,14 @@ export const useExplorersStore = defineStore('explorers', () => { const result: TransactionInsightDataItem[] = []; for (const transaction of allTransactions.value) { + const matchOptions: InsightsExplorerMatchContext = buildInsightsExplorerMatchContext(currentInsightsExplorer.value, transaction); + for (const query of currentInsightsExplorer.value.queries) { if (currentInsightsExplorer.value.datatableQuerySource && currentInsightsExplorer.value.datatableQuerySource !== query.id) { continue; } - if (query.match(transaction)) { + if (query.match(transaction, matchOptions)) { result.push(transaction); break; } @@ -751,8 +768,10 @@ export const useExplorersStore = defineStore('explorers', () => { continue; } + const matchContext: InsightsExplorerMatchContext = buildInsightsExplorerMatchContext(currentInsightsExplorer.value, transaction); + for (const [query, index] of itemAndIndex(currentInsightsExplorer.value.queries)) { - if (query.match(transaction)) { + if (query.match(transaction, matchContext)) { addTransactionToCategoriedDataMap(currentInsightsExplorer.value.timezoneUsedForDateRange, categoriedDataMap, categoryDimension, seriesDimension, query.name, index, transaction); if (categoryDimension !== TransactionExplorerDataDimension.Query) { diff --git a/src/views/desktop/insights/tabs/ExplorerQueryTab.vue b/src/views/desktop/insights/tabs/ExplorerQueryTab.vue index 1b81ded7..61dea964 100644 --- a/src/views/desktop/insights/tabs/ExplorerQueryTab.vue +++ b/src/views/desktop/insights/tabs/ExplorerQueryTab.vue @@ -153,6 +153,119 @@ />
+ + + + + + + + + + + + + + + + +
+ + ~ + +
+