diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php index 1b45b4defb..67a1bbf6f9 100644 --- a/app/Support/ParseDateString.php +++ b/app/Support/ParseDateString.php @@ -35,6 +35,7 @@ class ParseDateString */ public function parseDate(string $date): Carbon { + $date = strtolower($date); // parse keywords: if (in_array($date, $this->keywords, true)) { return $this->parseKeyword($date); @@ -48,18 +49,189 @@ class ParseDateString // if + or -: if (0 === strpos($date, '+') || 0 === strpos($date, '-')) { + return $this->parseRelativeDate($date); } + if ('xxxx-xx-xx' === strtolower($date)) { + throw new FireflyException(sprintf('[a]Not a recognised date format: "%s"', $date)); + } + // can't do a partial year: + $substrCount = substr_count(substr($date, 0, 4), 'x', 0); + if (10 === strlen($date) && $substrCount > 0 && $substrCount < 4) { + throw new FireflyException(sprintf('[b]Not a recognised date format: "%s"', $date)); + } - throw new FireflyException('Not recognised.'); + // maybe a date range + if (10 === strlen($date) && (false !== strpos($date, 'xx') || false !== strpos($date, 'xxxx'))) { + Log::debug(sprintf('[c]Detected a date range ("%s"), return a fake date.', $date)); + // very lazy way to parse the date without parsing it, because this specific function + // cant handle date ranges. + return new Carbon('1984-09-17'); + } + + throw new FireflyException(sprintf('[d]Not a recognised date format: "%s"', $date)); } + /** + * @param string $date + * + * @return bool + */ + public function isDateRange(string $date): bool + { + $date = strtolower($date); + // not 10 chars: + if (10 !== strlen($date)) { + return false; + } + // all x'es + if ('xxxx-xx-xx' === strtolower($date)) { + return false; + } + // no x'es + if (false === strpos($date, 'xx') && false === strpos($date, 'xxxx')) { + return false; + } + + return true; + } + + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + public function parseRange(string $date, Carbon $journalDate): array + { + // several types of range can be submitted + switch (true) { + default: + break; + case $this->isDayRange($date): + return $this->parseDayRange($date, $journalDate); + case $this->isMonthRange($date): + return $this->parseMonthRange($date, $journalDate); + case $this->isYearRange($date): + return $this->parseYearRange($date, $journalDate); + case $this->isMonthDayRange($date): + return $this->parseMonthDayRange($date, $journalDate); + case $this->isDayYearRange($date): + return $this->parseDayYearRange($date, $journalDate); + case $this->isMonthYearRange($date): + return $this->parseMonthYearRange($date, $journalDate); + } + + return [ + 'start' => new Carbon('1984-09-17'), + 'end' => new Carbon('1984-09-17'), + ]; + } + + /** + * @param string $date + * + * @return bool + */ + protected function isDayRange(string $date): bool + { + // if regex for xxxx-xx-DD: + $pattern = '/^xxxx-xx-(0[1-9]|[12][\d]|3[01])$/'; + if (preg_match($pattern, $date)) { + Log::debug(sprintf('"%s" is a day range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a day range.', $date)); + + return false; + } + + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + protected function parseDayRange(string $date, Carbon $journalDate): array + { + // format of string is xxxx-xx-DD + $validDate = str_replace(['xxxx'], [$journalDate->year], $date); + $validDate = str_replace(['xx'], [$journalDate->format('m')], $validDate); + Log::debug(sprintf('parseDayRange: Parsed "%s" into "%s"', $date, $validDate)); + $start = Carbon::createFromFormat('Y-m-d', $validDate)->startOfDay(); + $end = Carbon::createFromFormat('Y-m-d', $validDate)->endOfDay(); + + return [ + 'start' => $start, + 'end' => $end, + ]; + } + + /** + * @param string $date + * + * @return bool + */ + protected function isMonthRange(string $date): bool + { + // if regex for xxxx-MM-xx: + $pattern = '/^xxxx-(0[1-9]|1[012])-xx$/'; + if (preg_match($pattern, $date)) { + Log::debug(sprintf('"%s" is a month range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a month range.', $date)); + + return false; + } + + /** + * @param string $date + * + * @return bool + */ + protected function isMonthYearRange(string $date): bool + { + // if regex for YYYY-MM-xx: + $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/'; + if (preg_match($pattern, $date)) { + Log::debug(sprintf('"%s" is a month/year range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a month/year range.', $date)); + + return false; + } + + /** + * @param string $date + * + * @return bool + */ + protected function isYearRange(string $date): bool + { + // if regex for YYYY-xx-xx: + $pattern = '/^(19|20)\d\d-xx-xx$/'; + if (preg_match($pattern, $date)) { + Log::debug(sprintf('"%s" is a year range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a year range.', $date)); + + return false; + } + + /** * @param string $date * * @return Carbon */ - private function parseDefaultDate(string $date): Carbon + protected function parseDefaultDate(string $date): Carbon { return Carbon::createFromFormat('Y-m-d', $date); } @@ -69,7 +241,7 @@ class ParseDateString * * @return Carbon */ - private function parseKeyword(string $keyword): Carbon + protected function parseKeyword(string $keyword): Carbon { $today = Carbon::today()->startOfDay(); switch ($keyword) { @@ -99,12 +271,65 @@ class ParseDateString } } + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + protected function parseMonthRange(string $date, Carbon $journalDate): array + { + // because 31 would turn February into March unexpectedly and the exact day is irrelevant here. + $day = $journalDate->format('d'); + if ((int) $day > 28) { + $day = '28'; + } + + // format of string is xxxx-MM-xx + $validDate = str_replace(['xxxx'], [$journalDate->year], $date); + $validDate = str_replace(['xx'], [$day], $validDate); + Log::debug(sprintf('parseMonthRange: Parsed "%s" into "%s"', $date, $validDate)); + $start = Carbon::createFromFormat('Y-m-d', $validDate)->startOfMonth(); + $end = Carbon::createFromFormat('Y-m-d', $validDate)->endOfMonth(); + + return [ + 'start' => $start, + 'end' => $end, + ]; + } + + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + protected function parseMonthYearRange(string $date, Carbon $journalDate): array + { + // because 31 would turn February into March unexpectedly and the exact day is irrelevant here. + $day = $journalDate->format('d'); + if ((int) $day > 28) { + $day = '28'; + } + + // format of string is YYYY-MM-xx + $validDate = str_replace(['xx'], [$day], $date); + Log::debug(sprintf('parseMonthYearRange: Parsed "%s" into "%s"', $date, $validDate)); + $start = Carbon::createFromFormat('Y-m-d', $validDate)->startOfMonth(); + $end = Carbon::createFromFormat('Y-m-d', $validDate)->endOfMonth(); + + return [ + 'start' => $start, + 'end' => $end, + ]; + } + /** * @param string $date * * @return Carbon */ - private function parseRelativeDate(string $date): Carbon + protected function parseRelativeDate(string $date): Carbon { Log::debug(sprintf('Now in parseRelativeDate("%s")', $date)); $parts = explode(' ', $date); @@ -154,4 +379,106 @@ class ParseDateString return $today; } + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + protected function parseYearRange(string $date, Carbon $journalDate): array + { + // format of string is YYYY-xx-xx + // kind of a convulted way of replacing variables but I'm lazy. + $validDate = str_replace(['xx-xx'], [sprintf('%s-xx', $journalDate->format('m'))], $date); + $validDate = str_replace(['xx'], [$journalDate->format('d')], $validDate); + Log::debug(sprintf('parseYearRange: Parsed "%s" into "%s"', $date, $validDate)); + $start = Carbon::createFromFormat('Y-m-d', $validDate)->startOfYear(); + $end = Carbon::createFromFormat('Y-m-d', $validDate)->endOfYear(); + + return [ + 'start' => $start, + 'end' => $end, + ]; + } + + /** + * @param string $date + * + * @return bool + */ + protected function isMonthDayRange(string $date): bool + { + // if regex for xxxx-MM-DD: + $pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12][\d]|3[01])$/'; + if (preg_match($pattern, $date)) { + Log::debug(sprintf('"%s" is a month/day range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a month/day range.', $date)); + + return false; + } + + /** + * @param string $date + * + * @return bool + */ + protected function isDayYearRange(string $date): bool + { + // if regex for YYYY-xx-DD: + $pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12][\d]|3[01])$/'; + if (preg_match($pattern, $date)) { + Log::debug(sprintf('"%s" is a day/year range.', $date)); + + return true; + } + Log::debug(sprintf('"%s" is not a day/year range.', $date)); + + return false; + } + + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + private function parseMonthDayRange(string $date, Carbon $journalDate): array + { + // Any year. + // format of string is xxxx-MM-DD + $validDate = str_replace(['xxxx'], [$journalDate->year], $date); + Log::debug(sprintf('parseMonthDayRange: Parsed "%s" into "%s"', $date, $validDate)); + $start = Carbon::createFromFormat('Y-m-d', $validDate)->startOfDay(); + $end = Carbon::createFromFormat('Y-m-d', $validDate)->endOfDay(); + + return [ + 'start' => $start, + 'end' => $end, + ]; + } + + /** + * @param string $date + * @param Carbon $journalDate + * + * @return array + */ + private function parseDayYearRange(string $date, Carbon $journalDate): array + { + // Any year. + // format of string is YYYY-xx-DD + $validDate = str_replace(['xx'], [$journalDate->format('m')], $date); + Log::debug(sprintf('parseDayYearRange: Parsed "%s" into "%s"', $date, $validDate)); + $start = Carbon::createFromFormat('Y-m-d', $validDate)->startOfDay(); + $end = Carbon::createFromFormat('Y-m-d', $validDate)->endOfDay(); + + return [ + 'start' => $start, + 'end' => $end, + ]; + } + } diff --git a/app/TransactionRules/Triggers/DateAfter.php b/app/TransactionRules/Triggers/DateAfter.php index 71ac2cdbcf..a702bf3668 100644 --- a/app/TransactionRules/Triggers/DateAfter.php +++ b/app/TransactionRules/Triggers/DateAfter.php @@ -82,7 +82,8 @@ final class DateAfter extends AbstractTrigger implements TriggerInterface return false; } - if ($date->isAfter($ruleDate)) { + $isDateRange = $dateParser->isDateRange($this->triggerValue); + if (false === $isDateRange && $date->isAfter($ruleDate)) { Log::debug( sprintf( '%s is after %s, so return true.', @@ -93,6 +94,32 @@ final class DateAfter extends AbstractTrigger implements TriggerInterface return true; } + // could be a date range. + if ($isDateRange) { + Log::debug(sprintf('Date value is "%s", representing a range.', $this->triggerValue)); + $range = $dateParser->parseRange($this->triggerValue, $date); + if ($date->isAfter($range['end'])) { + Log::debug( + sprintf( + '%s is after [%s/%s], so return true.', + $date->format('Y-m-d H:i:s'), + $range['start']->format('Y-m-d H:i:s'), + $range['end']->format('Y-m-d H:i:s'), + ) + ); + + return true; + } + Log::debug( + sprintf( + '%s is NOT after [%s/%s], so return false.', + $date->format('Y-m-d H:i:s'), + $range['start']->format('Y-m-d H:i:s'), + $range['end']->format('Y-m-d H:i:s'), + ) + ); + return false; + } Log::debug( sprintf( diff --git a/app/TransactionRules/Triggers/DateBefore.php b/app/TransactionRules/Triggers/DateBefore.php index 0b6a6e7c40..de41f6da67 100644 --- a/app/TransactionRules/Triggers/DateBefore.php +++ b/app/TransactionRules/Triggers/DateBefore.php @@ -82,7 +82,9 @@ final class DateBefore extends AbstractTrigger implements TriggerInterface return false; } - if ($date->isBefore($ruleDate)) { + $isDateRange = $dateParser->isDateRange($this->triggerValue); + + if (false === $isDateRange && $date->isBefore($ruleDate)) { Log::debug( sprintf( '%s is before %s, so return true.', @@ -94,6 +96,35 @@ final class DateBefore extends AbstractTrigger implements TriggerInterface return true; } + // could be a date range. + if ($isDateRange) { + Log::debug(sprintf('Date value is "%s", representing a range.', $this->triggerValue)); + $range = $dateParser->parseRange($this->triggerValue, $date); + if ($date->isBefore($range['start'])) { + Log::debug( + sprintf( + '%s is before [%s/%s], so return true.', + $date->format('Y-m-d H:i:s'), + $range['start']->format('Y-m-d H:i:s'), + $range['end']->format('Y-m-d H:i:s'), + ) + ); + + return true; + } + Log::debug( + sprintf( + '%s is NOT before [%s/%s], so return false.', + $date->format('Y-m-d H:i:s'), + $range['start']->format('Y-m-d H:i:s'), + $range['end']->format('Y-m-d H:i:s'), + ) + ); + return false; + } + + + Log::debug( sprintf( '%s is NOT before %s, so return true.', diff --git a/app/TransactionRules/Triggers/DateIs.php b/app/TransactionRules/Triggers/DateIs.php index 6661b4e441..62cdeda575 100644 --- a/app/TransactionRules/Triggers/DateIs.php +++ b/app/TransactionRules/Triggers/DateIs.php @@ -82,7 +82,9 @@ final class DateIs extends AbstractTrigger implements TriggerInterface return false; } - if ($ruleDate->isSameDay($date)) { + $isDateRange = $dateParser->isDateRange($this->triggerValue); + + if (false === $isDateRange && $ruleDate->isSameDay($date)) { Log::debug( sprintf( '%s is on the same day as %s, so return true.', @@ -94,6 +96,34 @@ final class DateIs extends AbstractTrigger implements TriggerInterface return true; } + // could be a date range. + if ($isDateRange) { + Log::debug(sprintf('Date value is "%s", representing a range.', $this->triggerValue)); + $range = $dateParser->parseRange($this->triggerValue, $date); + if ($date->isAfter($range['start']) && $date->isBefore($range['end'])) { + Log::debug( + sprintf( + '%s is between [%s/%s], so return true.', + $date->format('Y-m-d H:i:s'), + $range['start']->format('Y-m-d H:i:s'), + $range['end']->format('Y-m-d H:i:s'), + ) + ); + + return true; + } + Log::debug( + sprintf( + '%s is NOT between [%s/%s], so return false.', + $date->format('Y-m-d H:i:s'), + $range['start']->format('Y-m-d H:i:s'), + $range['end']->format('Y-m-d H:i:s'), + ) + ); + + return false; + } + Log::debug( sprintf( '%s is NOT on the same day as %s, so return true.',