firefly-iii/app/Support/ParseDateString.php

425 lines
13 KiB
PHP
Raw Normal View History

2020-05-16 05:11:06 -05:00
<?php
2020-06-30 12:05:35 -05: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 05:11:06 -05:00
declare(strict_types=1);
2020-05-16 05:11:06 -05:00
namespace FireflyIII\Support;
use Carbon\Carbon;
use Carbon\CarbonInterface;
2023-12-09 13:12:34 -06:00
use Carbon\Exceptions\InvalidFormatException;
2020-05-16 05:11:06 -05:00
use FireflyIII\Exceptions\FireflyException;
2023-12-09 13:12:34 -06:00
use Illuminate\Support\Facades\Log;
2020-05-16 05:11:06 -05:00
/**
* Class ParseDateString
*/
class ParseDateString
{
2022-09-28 00:35:57 -05:00
private array $keywords
2020-05-16 05:11:06 -05:00
= [
'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',
'end of this year',
];
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 (!str_contains($date, 'xx') && !str_contains($date, 'xxxx')) {
return false;
}
return true;
}
2020-05-16 05:11:06 -05:00
/**
2021-09-18 03:21:29 -05:00
* @throws FireflyException
2023-12-22 13:12:38 -06:00
*
* @SuppressWarnings(PHPMD.NPathComplexity)
2020-05-16 05:11:06 -05:00
*/
public function parseDate(string $date): Carbon
{
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('parseDate("%s")', $date));
$date = strtolower($date);
2020-05-16 05:11:06 -05:00
// parse keywords:
if (in_array($date, $this->keywords, true)) {
return $this->parseKeyword($date);
}
// if regex for YYYY-MM-DD:
$pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
$result = preg_match($pattern, $date);
2023-12-20 22:06:51 -06:00
if (false !== $result && 0 !== $result) {
2020-05-16 05:11:06 -05:00
return $this->parseDefaultDate($date);
}
// if + or -:
2021-09-18 03:20:19 -05:00
if (str_starts_with($date, '+') || str_starts_with($date, '-')) {
2020-05-16 05:11:06 -05: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 11:03:31 -06: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 03:20:19 -05:00
if (10 === strlen($date) && (str_contains($date, 'xx') || str_contains($date, 'xxxx'))) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('[c] Detected a date range ("%s"), return a fake date.', $date));
2023-12-20 12:35:52 -06: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 13:01:33 -05:00
// maybe a year, nothing else?
if (4 === strlen($date) && is_numeric($date) && (int)$date > 1000 && (int)$date <= 3000) {
2020-09-14 13:01:33 -05:00
return new Carbon(sprintf('%d-01-01', $date));
}
2022-09-28 00:35:57 -05:00
throw new FireflyException(sprintf('[d] Not a recognised date format: "%s"', $date));
}
2023-06-21 05:34:58 -05:00
protected function parseKeyword(string $keyword): Carbon
{
$today = today(config('app.timezone'))->startOfDay();
return match ($keyword) { // @phpstan-ignore-line
2023-07-15 09:02:42 -05: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),
2023-07-15 09:02:42 -05:00
'start of this month' => $today->startOfMonth(),
'end of this month' => $today->endOfMonth(),
2023-06-21 05:34:58 -05:00
'start of this quarter' => $today->startOfQuarter(),
2023-07-15 09:02:42 -05:00
'end of this quarter' => $today->endOfQuarter(),
'start of this year' => $today->startOfYear(),
'end of this year' => $today->endOfYear(),
2023-06-21 05:34:58 -05:00
};
}
protected function parseDefaultDate(string $date): Carbon
{
2023-12-09 13:12:34 -06:00
$result = false;
2023-12-20 12:35:52 -06:00
2023-12-09 13:12:34 -06:00
try {
$result = Carbon::createFromFormat('Y-m-d', $date);
2023-12-09 23:45:59 -06:00
} catch (InvalidFormatException $e) { // @phpstan-ignore-line
2023-12-09 13:12:34 -06:00
Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage()));
}
if (false === $result) {
2023-11-27 22:31:26 -06:00
$result = today(config('app.timezone'))->startOfDay();
}
2023-12-20 12:35:52 -06:00
2023-11-27 22:31:26 -06:00
return $result;
2023-06-21 05:34:58 -05:00
}
protected function parseRelativeDate(string $date): Carbon
{
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('Now in parseRelativeDate("%s")', $date));
2023-06-21 05:34:58 -05:00
$parts = explode(' ', $date);
$today = today(config('app.timezone'))->startOfDay();
$functions = [
[
'd' => 'subDays',
'w' => 'subWeeks',
'm' => 'subMonths',
'q' => 'subQuarters',
'y' => 'subYears',
],
[
'd' => 'addDays',
'w' => 'addWeeks',
'm' => 'addMonths',
'q' => 'addQuarters',
'y' => 'addYears',
],
];
foreach ($parts as $part) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('Now parsing part "%s"', $part));
$part = trim($part);
2023-06-21 05:34:58 -05:00
// verify if correct
$pattern = '/[+-]\d+[wqmdy]/';
$result = preg_match($pattern, $part);
2023-12-20 22:06:51 -06:00
if (0 === $result || false === $result) {
2023-10-29 00:32:00 -05:00
app('log')->error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part));
2023-12-20 12:35:52 -06:00
2023-06-21 05:34:58 -05:00
continue;
}
$direction = str_starts_with($part, '+') ? 1 : 0;
$period = $part[strlen($part) - 1];
$number = (int)substr($part, 1, -1);
2023-06-21 05:34:58 -05:00
if (!array_key_exists($period, $functions[$direction])) {
2023-10-29 00:32:00 -05:00
app('log')->error(sprintf('No method for direction %d and period "%s".', $direction, $period));
2023-12-20 12:35:52 -06:00
2023-06-21 05:34:58 -05:00
continue;
}
$func = $functions[$direction][$period];
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d')));
2023-12-20 12:35:52 -06:00
$today->{$func}($number); // @phpstan-ignore-line
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('Resulting date is %s', $today->format('Y-m-d')));
2023-06-21 05:34:58 -05:00
}
return $today;
}
public function parseRange(string $date): array
{
// several types of range can be submitted
$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-20 22:06:51 -06:00
/**
* Returns true if this matches regex for xxxx-xx-DD:
*/
protected function isDayRange(string $date): bool
{
2023-02-22 11:03:31 -06:00
$pattern = '/^xxxx-xx-(0[1-9]|[12]\d|3[01])$/';
2023-12-20 22:06:51 -06:00
$result = preg_match($pattern, $date);
if (false !== $result && 0 !== $result) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is a day range.', $date));
return true;
}
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is not a day range.', $date));
return false;
}
/**
2023-06-21 05:34:58 -05:00
* format of string is xxxx-xx-DD
*/
2023-06-21 05:34:58 -05:00
protected function parseDayRange(string $date): array
{
2023-06-21 05:34:58 -05:00
$parts = explode('-', $date);
2023-05-29 06:56:55 -05:00
2023-06-21 05:34:58 -05:00
return [
'day' => $parts[2],
];
}
protected function isMonthRange(string $date): bool
{
// if regex for xxxx-MM-xx:
$pattern = '/^xxxx-(0[1-9]|1[012])-xx$/';
2023-12-22 13:12:38 -06:00
$result = preg_match($pattern, $date);
2023-12-20 22:06:51 -06:00
if (false !== $result && 0 !== $result) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is a month range.', $date));
return true;
}
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is not a month range.', $date));
return false;
}
/**
2023-06-21 05:34:58 -05:00
* format of string is xxxx-MM-xx
*/
2023-06-21 05:34:58 -05:00
protected function parseMonthRange(string $date): array
{
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('parseMonthRange: Parsed "%s".', $date));
2023-06-21 05:34:58 -05:00
$parts = explode('-', $date);
2023-05-29 06:56:55 -05:00
2023-06-21 05:34:58 -05:00
return [
'month' => $parts[1],
];
}
protected function isYearRange(string $date): bool
2020-05-16 05:11:06 -05:00
{
// if regex for YYYY-xx-xx:
$pattern = '/^(19|20)\d\d-xx-xx$/';
2023-12-22 13:12:38 -06:00
$result = preg_match($pattern, $date);
2023-12-20 22:06:51 -06:00
if (false !== $result && 0 !== $result) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is a year range.', $date));
2020-05-16 05:11:06 -05:00
return true;
2020-05-16 05:11:06 -05:00
}
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is not a year range.', $date));
2020-05-16 05:11:06 -05:00
return false;
2020-05-16 05:11:06 -05:00
}
/**
2023-06-21 05:34:58 -05:00
* format of string is YYYY-xx-xx
*/
2023-06-21 05:34:58 -05:00
protected function parseYearRange(string $date): array
{
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('parseYearRange: Parsed "%s"', $date));
$parts = explode('-', $date);
return [
2023-06-21 05:34:58 -05:00
'year' => $parts[0],
];
}
2023-06-21 05:34:58 -05:00
protected function isMonthDayRange(string $date): bool
2023-05-29 06:56:55 -05:00
{
2023-06-21 05:34:58 -05:00
// if regex for xxxx-MM-DD:
$pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
2023-12-22 13:12:38 -06:00
$result = preg_match($pattern, $date);
2023-12-20 22:06:51 -06:00
if (false !== $result && 0 !== $result) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is a month/day range.', $date));
2023-06-21 05:34:58 -05:00
return true;
}
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is not a month/day range.', $date));
2023-06-21 05:34:58 -05:00
return false;
}
/**
* format of string is xxxx-MM-DD
*/
private function parseMonthDayRange(string $date): array
{
app('log')->debug(sprintf('parseMonthDayRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
'month' => $parts[1],
'day' => $parts[2],
];
}
2023-06-21 05:34:58 -05:00
protected function isDayYearRange(string $date): bool
2023-05-29 06:56:55 -05:00
{
2023-06-21 05:34:58 -05:00
// if regex for YYYY-xx-DD:
$pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/';
2023-12-22 13:12:38 -06:00
$result = preg_match($pattern, $date);
2023-12-20 22:06:51 -06:00
if (false !== $result && 0 !== $result) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is a day/year range.', $date));
2023-05-29 06:56:55 -05:00
2023-06-21 05:34:58 -05:00
return true;
}
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is not a day/year range.', $date));
2023-06-21 05:34:58 -05:00
return false;
}
/**
* format of string is YYYY-xx-DD
*/
private function parseDayYearRange(string $date): array
{
app('log')->debug(sprintf('parseDayYearRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
'year' => $parts[0],
'day' => $parts[2],
];
}
2023-06-21 05:34:58 -05:00
protected function isMonthYearRange(string $date): bool
{
2023-06-21 05:34:58 -05:00
// if regex for YYYY-MM-xx:
$pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/';
2023-12-22 13:12:38 -06:00
$result = preg_match($pattern, $date);
2023-12-20 22:06:51 -06:00
if (false !== $result && 0 !== $result) {
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is a month/year range.', $date));
2023-06-21 05:34:58 -05:00
return true;
}
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('"%s" is not a month/year range.', $date));
2023-06-21 05:34:58 -05:00
return false;
}
/**
2023-06-21 05:34:58 -05:00
* format of string is YYYY-MM-xx
*/
2023-06-21 05:34:58 -05:00
protected function parseMonthYearRange(string $date): array
{
2023-10-29 00:33:43 -05:00
app('log')->debug(sprintf('parseMonthYearRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
2023-06-21 05:34:58 -05:00
'year' => $parts[0],
'month' => $parts[1],
];
}
2020-05-16 05:11:06 -05:00
}