Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add date interval validation rules #123

Merged
merged 3 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
<!-- /top-badges -->

<!-- rules-counter -->
[![Static Badge](https://img.shields.io/badge/Rules-367-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml)
[![Static Badge](https://img.shields.io/badge/Rules-153-green?label=Cell%20rules&labelColor=blue&color=gray)](src/Rules/Cell)
[![Static Badge](https://img.shields.io/badge/Rules-373-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml)
[![Static Badge](https://img.shields.io/badge/Rules-159-green?label=Cell%20rules&labelColor=blue&color=gray)](src/Rules/Cell)
[![Static Badge](https://img.shields.io/badge/Rules-206-green?label=Aggregate%20rules&labelColor=blue&color=gray)](src/Rules/Aggregate)
[![Static Badge](https://img.shields.io/badge/Rules-8-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks)
[![Static Badge](https://img.shields.io/badge/Rules-32/54/9-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml)
[![Static Badge](https://img.shields.io/badge/Rules-31/54/9-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml)
<!-- /rules-counter -->

A console utility designed for validating CSV files against a strictly defined schema and validation rules outlined
Expand Down Expand Up @@ -409,6 +409,16 @@ columns:
is_time: true # Check if the cell value is a valid time in the format "HH:MM:SS AM/PM" / "HH:MM:SS" / "HH:MM". Case-insensitive.
is_leap_year: true # Check if the cell value is a leap year. Example: "2008", "2008-02-29 23:59:59 UTC".

# Date Intervals. Under the hood, the strings are converted to seconds and compared.
# See: https://www.php.net/manual/en/class.dateinterval.php
# See: https://www.php.net/manual/en/dateinterval.createfromdatestring.php
date_interval_min: PT0S # 0 seconds
date_interval_greater: 1day 1sec # 1 day and 1 second
date_interval_not: 100 days # Except for the 100 days
date_interval: P2W # Exactly 2 weeks
date_interval_less: PT23H59M59S # 23 hours, 59 minutes, and 59 seconds
date_interval_max: P1Y # 1 year

# Specific formats
is_bool: true # Allow only boolean values "true" and "false", case-insensitive.
is_uuid: true # Validates whether the input is a valid UUID. It also supports validation of specific versions 1, 3, 4 and 5.
Expand Down Expand Up @@ -830,8 +840,8 @@ Behind the scenes to what is outlined in the yml above, there are additional che
* Check that each row matches the number of columns.
* With `strict_column_order` rule, you can check that the columns are in the correct order.
* With `allow_extra_columns` rule, you can check that there are no extra columns in the CSV file.
* If `csv.header: true`. Schema contains an unknown column `name` that is not found in the CSV file.
* If `csv.header: false`. Compare the number of columns in the schema and the CSV file.
* If `csv.header: true`. Schema contains an unknown column `name` that is not found in the CSV file.
* If `csv.header: false`. Compare the number of columns in the schema and the CSV file.

<!-- /extra-rules -->

Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name" : "jbzoo/csv-blueprint",
"type" : "project",
"description" : "CLI Utility for Validating and Generating CSV Files Based on Custom Rules. It ensures your data meets specified criteria, streamlining data management and integrity checks.",
"type" : "project",
"description" : "CLI Utility for Validating and Generating CSV files based on custom rules. It ensures your data meets specified criteria, streamlining data management and integrity checks.",
"license" : "MIT",
"keywords" : [
"jbzoo",
Expand Down
1 change: 1 addition & 0 deletions csv-blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

// Convert all errors to exceptions. Looks like we have critical case, and we need to stop or handle it.
// We have to do it becase tool uses 3rd-party libraries, and we can't trust them.
// So, we need to catch all errors and handle them.
\set_error_handler(static function ($severity, $message, $file, $line): void {
throw new \ErrorException($message, 0, $severity, $file, $line);
});
Expand Down
11 changes: 9 additions & 2 deletions schema-examples/full.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name" : "CSV Blueprint Schema Example",
"description" : "This YAML file provides a detailed description and validation rules for CSV files\nto be processed by CSV Blueprint tool. It includes specifications for file name patterns,\nCSV formatting options, and extensive validation criteria for individual columns and their values,\nsupporting a wide range of data validation rules from basic type checks to complex regex validations.\nThis example serves as a comprehensive guide for creating robust CSV file validations.\n",
"description" : "This YAML file provides a detailed description and validation rules for CSV files\nto be processed by CSV Blueprint tool. It includes specifications for file name patterns,\nCSV formatting options, and extensive validation criteria for individual columns and their values,\nsupporting a wide range of data validation rules from basic type checks to complex regex validations.\nThis example serves as a comprehensive guide for creating robust CSV file validations.\n",

"filename_pattern" : "\/demo(-\\d+)?\\.csv$\/i",

Expand All @@ -23,7 +23,7 @@
"name" : "Column Name (header)",
"description" : "Lorem ipsum",
"example" : "Some example",
"required" : true,
"required" : true,

"rules" : {
"not_empty" : true,
Expand Down Expand Up @@ -89,6 +89,13 @@
"is_time" : true,
"is_leap_year" : true,

"date_interval_min" : "PT0S",
"date_interval_greater" : "1day 1sec",
"date_interval_not" : "100 days",
"date_interval" : "P2W",
"date_interval_less" : "PT23H59M59S",
"date_interval_max" : "P1Y",

"is_bool" : true,
"is_uuid" : true,
"is_slug" : true,
Expand Down
7 changes: 7 additions & 0 deletions schema-examples/full.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
'is_time' => true,
'is_leap_year' => true,

'date_interval_min' => 'PT0S',
'date_interval_greater' => '1day 1sec',
'date_interval_not' => '100 days',
'date_interval' => 'P2W',
'date_interval_less' => 'PT23H59M59S',
'date_interval_max' => 'P1Y',

'is_bool' => true,
'is_uuid' => true,
'is_slug' => true,
Expand Down
10 changes: 10 additions & 0 deletions schema-examples/full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ columns:
is_time: true # Check if the cell value is a valid time in the format "HH:MM:SS AM/PM" / "HH:MM:SS" / "HH:MM". Case-insensitive.
is_leap_year: true # Check if the cell value is a leap year. Example: "2008", "2008-02-29 23:59:59 UTC".

# Date Intervals. Under the hood, the strings are converted to seconds and compared.
# See: https://www.php.net/manual/en/class.dateinterval.php
# See: https://www.php.net/manual/en/dateinterval.createfromdatestring.php
date_interval_min: PT0S # 0 seconds
date_interval_greater: 1day 1sec # 1 day and 1 second
date_interval_not: 100 days # Except for the 100 days
date_interval: P2W # Exactly 2 weeks
date_interval_less: PT23H59M59S # 23 hours, 59 minutes, and 59 seconds
date_interval_max: P1Y # 1 year

# Specific formats
is_bool: true # Allow only boolean values "true" and "false", case-insensitive.
is_uuid: true # Validates whether the input is a valid UUID. It also supports validation of specific versions 1, 3, 4 and 5.
Expand Down
7 changes: 7 additions & 0 deletions schema-examples/full_clean.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ columns:
is_time: true
is_leap_year: true

date_interval_min: PT0S
date_interval_greater: '1day 1sec'
date_interval_not: '100 days'
date_interval: P2W
date_interval_less: PT23H59M59S
date_interval_max: P1Y

is_bool: true
is_uuid: true
is_slug: true
Expand Down
124 changes: 124 additions & 0 deletions src/Rules/Cell/ComboDateInterval.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

/**
* JBZoo Toolbox - Csv-Blueprint.
*
* This file is part of the JBZoo Toolbox project.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT
* @copyright Copyright (C) JBZoo.com, All rights reserved.
* @see https://github.com/JBZoo/Csv-Blueprint
*/

declare(strict_types=1);

namespace JBZoo\CsvBlueprint\Rules\Cell;

final class ComboDateInterval extends AbstractCellRuleCombo
{
protected const NAME = 'date interval';

protected const INVALID_DATEINTERVAL_ACTUAL = -1;
protected const INVALID_DATEINTERVAL_EXPECTED = -2;

public function getHelpMeta(): array
{
return [
[
'Date Intervals. Under the hood, the strings are converted to seconds and compared.',
'See: https://www.php.net/manual/en/class.dateinterval.php',
'See: https://www.php.net/manual/en/dateinterval.createfromdatestring.php',
],
[
self::MIN => ['PT0S', '0 seconds'],
self::GREATER => ['1day 1sec', '1 day and 1 second'],
self::EQ => ['P2W', 'Exactly 2 weeks'],
self::NOT => ['100 days', 'Except for the 100 days'],
self::LESS => ['PT23H59M59S', '23 hours, 59 minutes, and 59 seconds'],
self::MAX => ['P1Y', '1 year'],
],
];
}

protected function getActualCell(string $cellValue): float
{
try {
$seconds = self::dateIntervalToSeconds($cellValue);
} catch (\Exception) {
return self::INVALID_DATEINTERVAL_ACTUAL;
}

return $seconds;
}

protected function getExpected(): float
{
$expectedValue = $this->getOptionAsString();

try {
$seconds = self::dateIntervalToSeconds($expectedValue);
} catch (\Exception) {
return self::INVALID_DATEINTERVAL_EXPECTED;
}

return $seconds;
}

protected function getExpectedStr(): string
{
$expectedValue = $this->getOptionAsString();

try {
$seconds = self::dateIntervalToSeconds($expectedValue);
} catch (\Exception $exception) {
return "<red>{$exception->getMessage()}</red>";
}

return "{$seconds} ({$expectedValue}) seconds";
}

protected function getCurrentStr(string $cellValue): string
{
try {
$seconds = self::dateIntervalToSeconds($cellValue);
} catch (\Exception $exception) {
return "<red>{$exception->getMessage()}</red>";
}

return "parsed as \"{$seconds}\" seconds";
}

private static function dateIntervalToSeconds(string $dateIntervalOrAsString): int
{
try {
$interval = new \DateInterval($dateIntervalOrAsString);
} catch (\Exception) {
try {
$interval = \DateInterval::createFromDateString($dateIntervalOrAsString);
} catch (\Exception) {
throw new \RuntimeException("Can't parse date interval: {$dateIntervalOrAsString}");
}
}

if (!$interval instanceof \DateInterval) {
throw new \RuntimeException("Can't parse date interval: {$dateIntervalOrAsString}");
}

$daysPerYear = 365.25; // Average considering leap years
$daysPerMonth = 30; // Average. "365.25 / 12 ~ 30.4166666667"
$hoursPerDay = 24;
$minutesPerHour = 60;
$secondsPerMinute = 60;

$yearsToDays = $interval->y * $daysPerYear;
$monthsToDays = $interval->m * $daysPerMonth;
$days = $interval->d + $yearsToDays + $monthsToDays;
$hours = $interval->h + ($days * $hoursPerDay);
$minutes = $interval->i + ($hours * $minutesPerHour);
$seconds = $interval->s + ($minutes * $secondsPerMinute);

return (int)$seconds;
}
}
58 changes: 58 additions & 0 deletions tests/Rules/Cell/ComboDateIntervalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/**
* JBZoo Toolbox - Csv-Blueprint.
*
* This file is part of the JBZoo Toolbox project.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT
* @copyright Copyright (C) JBZoo.com, All rights reserved.
* @see https://github.com/JBZoo/Csv-Blueprint
*/

declare(strict_types=1);

namespace JBZoo\PHPUnit\Rules\Cell;

use JBZoo\CsvBlueprint\Rules\AbstarctRule as Combo;
use JBZoo\CsvBlueprint\Rules\Cell\ComboDateInterval;
use JBZoo\PHPUnit\Rules\TestAbstractCellRuleCombo;

use function JBZoo\PHPUnit\isSame;

class ComboDateIntervalTest extends TestAbstractCellRuleCombo
{
protected string $ruleClass = ComboDateInterval::class;

public function testEqual(): void
{
$rule = $this->create('1 day', Combo::EQ);

isSame('', $rule->test(''));
isSame('', $rule->test('1 day'));

$rule = $this->create('1 day', Combo::EQ);
isSame('', $rule->test('1 day'));

foreach ($rule->getHelpMeta()[1] as $examples) {
$rule = $this->create($examples[0], Combo::EQ);
isSame('', $rule->test($examples[0]), $examples[0]);
}

isSame(
'The date interval of the value "<c>qwerty</c>" is ' .
'<red>Can\'t parse date interval: qwerty</red>, ' .
'which is not equal than the expected "<green>31557600 (P1Y) seconds</green>"',
$rule->test('qwerty', true),
);

$rule = $this->create('P2W', Combo::EQ);
isSame(
'The date interval of the value "1 day" is parsed as "86400" seconds, ' .
'which is not equal than the expected "1209600 (P2W) seconds"',
$rule->test('1 day'),
);
}
}
1 change: 0 additions & 1 deletion tests/schemas/todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ columns:
# Dates
age: 35
dateperiod: 1
dateinterval: 1

# Codes
subdivision_code: [ ] # https://github.com/Respect/Validation/blob/main/docs/rules/SubdivisionCode.md
Expand Down