Files
firefly-iii/app/Support/ParseDateString.php
T

396 lines
12 KiB
PHP
Raw Normal View History

2020-05-16 12:11:06 +02:00
<?php
2020-06-30 19:05:35 +02:00
/**
* ParseDateString.php
* Copyright (c) 2020 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
2020-05-16 12:11:06 +02:00
declare(strict_types=1);
2020-05-16 12:11:06 +02:00
namespace FireflyIII\Support;
use Carbon\Carbon;
use Carbon\CarbonInterface;
2023-12-09 20:12:34 +01:00
use Carbon\Exceptions\InvalidFormatException;
2020-05-16 12:11:06 +02:00
use FireflyIII\Exceptions\FireflyException;
2023-12-09 20:12:34 +01:00
use Illuminate\Support\Facades\Log;
2025-10-05 12:57:58 +02:00
use Safe\Exceptions\PcreException;
2025-10-05 13:03:51 +02:00
2025-05-27 17:06:15 +02:00
use function Safe\preg_match;
2020-05-16 12:11:06 +02:00
/**
* Class ParseDateString
*/
class ParseDateString
{
2026-01-23 15:09:50 +01:00
private array $keywords = [
'today',
'yesterday',
'tomorrow',
'start of this week',
'end of this week',
'start of this month',
'end of this month',
'start of this quarter',
'end of this quarter',
'start of this year',
2026-01-23 15:14:29 +01:00
'end of this year',
2026-01-23 15:09:50 +01:00
];
2020-05-16 12:11:06 +02:00
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;
}
2025-11-09 09:11:55 +01:00
// no x'es
2025-11-09 09:08:03 +01:00
return !(!str_contains($date, 'xx') && !str_contains($date, 'xxxx'));
}
2020-05-16 12:11:06 +02:00
/**
2025-10-05 12:57:58 +02:00
* @throws FireflyException
* @throws PcreException
2025-10-05 13:03:51 +02:00
*
2025-01-03 15:53:10 +01:00
* @SuppressWarnings("PHPMD.NPathComplexity")
2020-05-16 12:11:06 +02:00
*/
public function parseDate(string $date): Carbon
{
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('parseDate("%s")', $date));
2026-01-23 15:14:29 +01:00
$date = strtolower($date);
2020-05-16 12:11:06 +02:00
// parse keywords:
if (in_array($date, $this->keywords, true)) {
return $this->parseKeyword($date);
}
// if regex for YYYY-MM-DD:
2026-01-23 15:14:29 +01:00
$pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
2025-09-07 07:43:04 +02:00
if (0 !== $result) {
2020-05-16 12:11:06 +02:00
return $this->parseDefaultDate($date);
}
// if + or -:
2021-09-18 10:20:19 +02:00
if (str_starts_with($date, '+') || str_starts_with($date, '-')) {
2020-05-16 12:11:06 +02:00
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:
2023-02-22 18:03:31 +01:00
$substrCount = substr_count(substr($date, 0, 4), 'x');
if (10 === strlen($date) && $substrCount > 0 && $substrCount < 4) {
throw new FireflyException(sprintf('[b] Not a recognised date format: "%s"', $date));
}
// maybe a date range
2021-09-18 10:20:19 +02:00
if (10 === strlen($date) && (str_contains($date, 'xx') || str_contains($date, 'xxxx'))) {
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('[c] Detected a date range ("%s"), return a fake date.', $date));
2023-12-20 19:35:52 +01:00
// very lazy way to parse the date without parsing it, because this specific function
// cant handle date ranges.
return new Carbon('1984-09-17');
}
2020-09-14 20:01:33 +02:00
// maybe a year, nothing else?
2026-01-23 15:09:50 +01:00
if (4 === strlen($date) && is_numeric($date) && (int) $date > 1000 && (int) $date <= 3000) {
2020-09-14 20:01:33 +02:00
return new Carbon(sprintf('%d-01-01', $date));
}
2022-09-28 07:35:57 +02:00
throw new FireflyException(sprintf('[d] Not a recognised date format: "%s"', $date));
}
public function parseRange(string $date): array
{
// several types of range can be submitted
2026-01-23 15:09:50 +01:00
$result = ['exact' => new Carbon('1984-09-17')];
switch (true) {
default:
break;
case $this->isDayRange($date):
$result = $this->parseDayRange($date);
break;
case $this->isMonthRange($date):
$result = $this->parseMonthRange($date);
break;
case $this->isYearRange($date):
$result = $this->parseYearRange($date);
break;
case $this->isMonthDayRange($date):
$result = $this->parseMonthDayRange($date);
break;
case $this->isDayYearRange($date):
$result = $this->parseDayYearRange($date);
break;
case $this->isMonthYearRange($date):
$result = $this->parseMonthYearRange($date);
break;
}
return $result;
}
2023-12-21 05:06:51 +01:00
/**
* Returns true if this matches regex for xxxx-xx-DD:
*/
protected function isDayRange(string $date): bool
{
2023-02-22 18:03:31 +01:00
$pattern = '/^xxxx-xx-(0[1-9]|[12]\d|3[01])$/';
2025-05-27 17:06:15 +02:00
$result = preg_match($pattern, $date);
2025-09-07 07:43:04 +02:00
if (0 !== $result) {
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('"%s" is a day range.', $date));
return true;
}
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('"%s" is not a day range.', $date));
return false;
}
2025-09-26 06:05:37 +02:00
protected function isDayYearRange(string $date): bool
{
2025-09-26 06:05:37 +02:00
// if regex for YYYY-xx-DD:
$pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a day/year range.', $date));
2023-05-29 13:56:55 +02:00
2025-09-26 06:05:37 +02:00
return true;
}
Log::debug(sprintf('"%s" is not a day/year range.', $date));
return false;
}
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])$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
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;
}
protected function isMonthRange(string $date): bool
{
// if regex for xxxx-MM-xx:
$pattern = '/^xxxx-(0[1-9]|1[012])-xx$/';
2025-05-27 17:06:15 +02:00
$result = preg_match($pattern, $date);
2025-09-07 07:43:04 +02:00
if (0 !== $result) {
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('"%s" is a month range.', $date));
return true;
}
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('"%s" is not a month range.', $date));
return false;
}
2025-09-26 06:05:37 +02:00
protected function isMonthYearRange(string $date): bool
{
2025-09-26 06:05:37 +02:00
// if regex for YYYY-MM-xx:
$pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/';
$result = preg_match($pattern, $date);
if (0 !== $result) {
Log::debug(sprintf('"%s" is a month/year range.', $date));
2023-05-29 13:56:55 +02:00
2025-09-26 06:05:37 +02:00
return true;
}
Log::debug(sprintf('"%s" is not a month/year range.', $date));
return false;
}
protected function isYearRange(string $date): bool
2020-05-16 12:11:06 +02:00
{
// if regex for YYYY-xx-xx:
$pattern = '/^(19|20)\d\d-xx-xx$/';
2025-05-27 17:06:15 +02:00
$result = preg_match($pattern, $date);
2025-09-07 07:43:04 +02:00
if (0 !== $result) {
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('"%s" is a year range.', $date));
2020-05-16 12:11:06 +02:00
return true;
2020-05-16 12:11:06 +02:00
}
2025-09-07 07:51:01 +02:00
Log::debug(sprintf('"%s" is not a year range.', $date));
2020-05-16 12:11:06 +02:00
return false;
2020-05-16 12:11:06 +02:00
}
/**
2025-09-26 06:05:37 +02:00
* format of string is xxxx-xx-DD
*/
2025-09-26 06:05:37 +02:00
protected function parseDayRange(string $date): array
{
$parts = explode('-', $date);
2026-01-23 15:09:50 +01:00
return ['day' => $parts[2]];
}
2025-09-26 06:05:37 +02:00
protected function parseDefaultDate(string $date): Carbon
2023-05-29 13:56:55 +02:00
{
2025-09-26 06:05:37 +02:00
$result = false;
2025-09-26 06:05:37 +02:00
try {
$result = Carbon::createFromFormat('Y-m-d', $date);
} catch (InvalidFormatException $e) {
Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage()));
}
if (false === $result) {
return today(config('app.timezone'))->startOfDay();
2023-06-21 12:34:58 +02:00
}
2025-09-26 06:05:37 +02:00
return $result;
}
protected function parseKeyword(string $keyword): Carbon
{
$today = today(config('app.timezone'))->startOfDay();
return match ($keyword) {
2026-01-23 15:14:29 +01:00
default => $today,
'yesterday' => $today->subDay(),
'tomorrow' => $today->addDay(),
'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY),
'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY),
'start of this month' => $today->startOfMonth(),
'end of this month' => $today->endOfMonth(),
2025-09-26 06:05:37 +02:00
'start of this quarter' => $today->startOfQuarter(),
2026-01-23 15:14:29 +01:00
'end of this quarter' => $today->endOfQuarter(),
'start of this year' => $today->startOfYear(),
'end of this year' => $today->endOfYear()
2025-09-26 06:05:37 +02:00
};
}
/**
2025-09-26 06:05:37 +02:00
* format of string is xxxx-MM-xx
*/
2025-09-26 06:05:37 +02:00
protected function parseMonthRange(string $date): array
{
2025-09-26 06:05:37 +02:00
Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date));
$parts = explode('-', $date);
2026-01-23 15:09:50 +01:00
return ['month' => $parts[1]];
}
2025-09-26 06:05:37 +02:00
/**
* format of string is YYYY-MM-xx
*/
protected function parseMonthYearRange(string $date): array
2023-05-29 13:56:55 +02:00
{
2025-09-26 06:05:37 +02:00
Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date));
$parts = explode('-', $date);
2023-05-29 13:56:55 +02:00
2026-01-23 15:09:50 +01:00
return ['year' => $parts[0], 'month' => $parts[1]];
2025-09-26 06:05:37 +02:00
}
protected function parseRelativeDate(string $date): Carbon
{
Log::debug(sprintf('Now in parseRelativeDate("%s")', $date));
$parts = explode(' ', $date);
$today = today(config('app.timezone'))->startOfDay();
$functions = [
2026-01-23 15:09:50 +01:00
['d' => 'subDays', 'w' => 'subWeeks', 'm' => 'subMonths', 'q' => 'subQuarters', 'y' => 'subYears'],
2026-01-23 15:14:29 +01:00
['d' => 'addDays', 'w' => 'addWeeks', 'm' => 'addMonths', 'q' => 'addQuarters', 'y' => 'addYears'],
2025-09-26 06:05:37 +02:00
];
foreach ($parts as $part) {
Log::debug(sprintf('Now parsing part "%s"', $part));
2026-01-23 15:14:29 +01:00
$part = trim($part);
2025-09-26 06:05:37 +02:00
// verify if correct
2026-01-23 15:14:29 +01:00
$pattern = '/[+-]\d+[wqmdy]/';
$result = preg_match($pattern, $part);
2025-09-26 06:05:37 +02:00
if (0 === $result) {
Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part));
continue;
}
$direction = str_starts_with($part, '+') ? 1 : 0;
$period = $part[strlen($part) - 1];
2026-01-23 15:09:50 +01:00
$number = (int) substr($part, 1, -1);
2025-09-26 06:05:37 +02:00
if (!array_key_exists($period, $functions[$direction])) {
Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period));
continue;
}
2026-01-23 15:14:29 +01:00
$func = $functions[$direction][$period];
2025-09-26 06:05:37 +02:00
Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d')));
$today->{$func}($number); // @phpstan-ignore-line
Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d')));
}
2025-09-26 06:05:37 +02:00
return $today;
}
/**
2025-09-26 06:05:37 +02:00
* format of string is YYYY-xx-xx
*/
2025-09-26 06:05:37 +02:00
protected function parseYearRange(string $date): array
{
2025-09-26 06:05:37 +02:00
Log::debug(sprintf('parseYearRange: Parsed "%s"', $date));
$parts = explode('-', $date);
2026-01-23 15:09:50 +01:00
return ['year' => $parts[0]];
}
2025-09-26 06:05:37 +02:00
/**
* format of string is YYYY-xx-DD
*/
private function parseDayYearRange(string $date): array
{
2025-09-26 06:05:37 +02:00
Log::debug(sprintf('parseDayYearRange: Parsed "%s".', $date));
$parts = explode('-', $date);
2023-06-21 12:34:58 +02:00
2026-01-23 15:09:50 +01:00
return ['year' => $parts[0], 'day' => $parts[2]];
}
/**
2025-09-26 06:05:37 +02:00
* format of string is xxxx-MM-DD
*/
2025-09-26 06:05:37 +02:00
private function parseMonthDayRange(string $date): array
{
2025-09-26 06:05:37 +02:00
Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date));
$parts = explode('-', $date);
2026-01-23 15:09:50 +01:00
return ['month' => $parts[1], 'day' => $parts[2]];
}
2020-05-16 12:11:06 +02:00
}