Skip to content

Commit

Permalink
Refactor cell value validation technique and docs (#102)
Browse files Browse the repository at this point in the history
Switched from using Utils regex method to Respect\Validation\Validator
in IsFloat and IsInt classes for better and more accurate cell value
validation. This update also includes correct handling for empty string
values and returns null instead. Corresponding tests have been modified
to adapt to these changes for consistency.
  • Loading branch information
SmetDenis committed Mar 27, 2024
1 parent a62c222 commit aa6e8e5
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 48 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Expand Up @@ -100,6 +100,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
coverage: none
tools: composer
extensions: ast

Expand Down Expand Up @@ -138,6 +139,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: none
tools: composer
extensions: ast

Expand Down
22 changes: 11 additions & 11 deletions README.md
Expand Up @@ -4,7 +4,7 @@
[![GitHub Release](https://img.shields.io/github/v/release/jbzoo/csv-blueprint?label=Latest)](https://github.com/jbzoo/csv-blueprint/releases) [![Total Downloads](https://poser.pugx.org/jbzoo/csv-blueprint/downloads)](https://packagist.org/packages/jbzoo/csv-blueprint/stats) [![Docker Pulls](https://img.shields.io/docker/pulls/jbzoo/csv-blueprint.svg)](https://hub.docker.com/r/jbzoo/csv-blueprint/tags) [![Docker Image Size](https://img.shields.io/docker/image-size/jbzoo/csv-blueprint)](https://hub.docker.com/r/jbzoo/csv-blueprint/tags)

<!-- rules-counter -->
[![Static Badge](https://img.shields.io/badge/Rules-292-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml) [![Static Badge](https://img.shields.io/badge/Rules-81-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-5-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks) [![Static Badge](https://img.shields.io/badge/Rules-199-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml)
[![Static Badge](https://img.shields.io/badge/Rules-292-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml) [![Static Badge](https://img.shields.io/badge/Rules-81-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-5-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks) [![Static Badge](https://img.shields.io/badge/Rules-142/54/8-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml)
<!-- /rules-counter -->

## Introduction
Expand Down Expand Up @@ -198,8 +198,8 @@ columns:
contains_one: [ a, b ] # Only one of the strings must be part of the CSV value.
contains_any: [ a, b ] # At least one of the string must be part of the CSV value.
contains_all: [ a, b ] # All the strings must be part of a CSV value.
starts_with: "prefix " # Example: "prefix Hello World".
ends_with: " suffix" # Example: "Hello World suffix".
starts_with: 'prefix ' # Example: "prefix Hello World".
ends_with: ' suffix' # Example: "Hello World suffix".

# Under the hood it converts and compares as float values.
# Comparison accuracy is 10 digits after a dot.
Expand Down Expand Up @@ -519,13 +519,13 @@ columns:
# See: https://en.wikipedia.org/wiki/Quartile
# There are multiple methods for computing quartiles: ["exclusive", "inclusive"]. Exclusive is ussually classic.
# Available types: ["0%", "Q1", "Q2", "Q3", "100%", "IQR"] ("IQR" is Interquartile Range)
# Example: `[ inclusive, 'Q3', 42.0 ]` - the Q3 inclusive quartile is 50.0
quartiles_min: [ 'exclusive', '0%', 1.0 ] # x >= 1.0
quartiles_greater: [ 'inclusive', 'Q1', 2.0 ] # x > 2.0
quartiles_not: [ 'exclusive', 'Q2', 5.0 ] # x != 5.0
quartiles: [ 'inclusive', 'Q3', 7.0 ] # x == 7.0
quartiles_less: [ 'exclusive', '100%', 8.0 ] # x < 8.0
quartiles_max: [ 'inclusive', 'IQR', 9.0 ] # x <= 9.0
# Example: `[ inclusive, 'Q3', 42.0 ]` - the Q3 inclusive quartile is 42.0
quartiles_min: [ exclusive, '0%', 1.0 ] # x >= 1.0
quartiles_greater: [ inclusive, 'Q1', 2.0 ] # x > 2.0
quartiles_not: [ exclusive, 'Q2', 5.0 ] # x != 5.0
quartiles: [ inclusive, 'Q3', 7.0 ] # x == 7.0
quartiles_less: [ exclusive, '100%', 8.0 ] # x < 8.0
quartiles_max: [ inclusive, 'IQR', 9.0 ] # x <= 9.0

# Midhinge. The average of the first and third quartiles and is thus a measure of location.
# Equivalently, it is the 25% trimmed mid-range or 25% midsummary; it is an L-estimator.
Expand Down Expand Up @@ -938,7 +938,7 @@ But... it's not a problem for most cases. And it solves the problem of validatin

The utility is made to just pick up and use and not think about how it works internally.
Moreover, everything is covered as strictly as possible by tests, strict typing of variables + `~7` linters and static analyzers (max level of rules).
Also, if you look, you'll see that any PR goes through about `~30` different checks on GitHub Actions (matrix of PHP versions and mods).
Also, if you look, you'll see that any PR goes through about `~10` different checks on GitHub Actions (matrix of PHP versions and mods).
Since I don't know under what conditions the code will be used, everything I can think of is covered. The wonderful world of Open Source.

So... as strictly as possible in today's PHP world. I think it works as expected.
Expand Down
18 changes: 9 additions & 9 deletions schema-examples/full.yml
Expand Up @@ -112,8 +112,8 @@ columns:
contains_one: [ a, b ] # Only one of the strings must be part of the CSV value.
contains_any: [ a, b ] # At least one of the string must be part of the CSV value.
contains_all: [ a, b ] # All the strings must be part of a CSV value.
starts_with: "prefix " # Example: "prefix Hello World".
ends_with: " suffix" # Example: "Hello World suffix".
starts_with: 'prefix ' # Example: "prefix Hello World".
ends_with: ' suffix' # Example: "Hello World suffix".

# Under the hood it converts and compares as float values.
# Comparison accuracy is 10 digits after a dot.
Expand Down Expand Up @@ -433,13 +433,13 @@ columns:
# See: https://en.wikipedia.org/wiki/Quartile
# There are multiple methods for computing quartiles: ["exclusive", "inclusive"]. Exclusive is ussually classic.
# Available types: ["0%", "Q1", "Q2", "Q3", "100%", "IQR"] ("IQR" is Interquartile Range)
# Example: `[ inclusive, 'Q3', 42.0 ]` - the Q3 inclusive quartile is 50.0
quartiles_min: [ 'exclusive', '0%', 1.0 ] # x >= 1.0
quartiles_greater: [ 'inclusive', 'Q1', 2.0 ] # x > 2.0
quartiles_not: [ 'exclusive', 'Q2', 5.0 ] # x != 5.0
quartiles: [ 'inclusive', 'Q3', 7.0 ] # x == 7.0
quartiles_less: [ 'exclusive', '100%', 8.0 ] # x < 8.0
quartiles_max: [ 'inclusive', 'IQR', 9.0 ] # x <= 9.0
# Example: `[ inclusive, 'Q3', 42.0 ]` - the Q3 inclusive quartile is 42.0
quartiles_min: [ exclusive, '0%', 1.0 ] # x >= 1.0
quartiles_greater: [ inclusive, 'Q1', 2.0 ] # x > 2.0
quartiles_not: [ exclusive, 'Q2', 5.0 ] # x != 5.0
quartiles: [ inclusive, 'Q3', 7.0 ] # x == 7.0
quartiles_less: [ exclusive, '100%', 8.0 ] # x < 8.0
quartiles_max: [ inclusive, 'IQR', 9.0 ] # x <= 9.0

# Midhinge. The average of the first and third quartiles and is thus a measure of location.
# Equivalently, it is the 25% trimmed mid-range or 25% midsummary; it is an L-estimator.
Expand Down
14 changes: 7 additions & 7 deletions src/Rules/Aggregate/ComboQuartiles.php
Expand Up @@ -49,15 +49,15 @@ public function getHelpMeta(): array
'Available types: ' . Utils::printList(self::TYPES) . ' ("IQR" is Interquartile Range)',
// Example
'Example: `[ ' . self::METHODS[1] . ", '" . self::TYPES[3] . "', 42.0 ]`" .
' - the ' . self::TYPES[3] . ' ' . self::METHODS[1] . ' quartile is 50.0',
' - the ' . self::TYPES[3] . ' ' . self::METHODS[1] . ' quartile is 42.0',
],
[
self::MIN => ["[ '" . self::METHODS[0] . "', '" . self::TYPES[0] . "', 1.0 ]", 'x >= 1.0'],
self::GREATER => ["[ '" . self::METHODS[1] . "', '" . self::TYPES[1] . "', 2.0 ]", 'x > 2.0'],
self::NOT => ["[ '" . self::METHODS[0] . "', '" . self::TYPES[2] . "', 5.0 ]", 'x != 5.0'],
self::EQ => ["[ '" . self::METHODS[1] . "', '" . self::TYPES[3] . "', 7.0 ]", 'x == 7.0'],
self::LESS => ["[ '" . self::METHODS[0] . "', '" . self::TYPES[4] . "', 8.0 ]", 'x < 8.0'],
self::MAX => ["[ '" . self::METHODS[1] . "', '" . self::TYPES[5] . "', 9.0 ]", 'x <= 9.0'],
self::MIN => ['[ ' . self::METHODS[0] . ", '" . self::TYPES[0] . "', 1.0 ]", 'x >= 1.0'],
self::GREATER => ['[ ' . self::METHODS[1] . ", '" . self::TYPES[1] . "', 2.0 ]", 'x > 2.0'],
self::NOT => ['[ ' . self::METHODS[0] . ", '" . self::TYPES[2] . "', 5.0 ]", 'x != 5.0'],
self::EQ => ['[ ' . self::METHODS[1] . ", '" . self::TYPES[3] . "', 7.0 ]", 'x == 7.0'],
self::LESS => ['[ ' . self::METHODS[0] . ", '" . self::TYPES[4] . "', 8.0 ]", 'x < 8.0'],
self::MAX => ['[ ' . self::METHODS[1] . ", '" . self::TYPES[5] . "', 9.0 ]", 'x <= 9.0'],
],
];
}
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Cell/EndsWith.php
Expand Up @@ -23,7 +23,7 @@ public function getHelpMeta(): array
return [
[],
[
self::DEFAULT => ['" suffix"', 'Example: "Hello World suffix".'],
self::DEFAULT => ["' suffix'", 'Example: "Hello World suffix".'],
],
];
}
Expand Down
8 changes: 6 additions & 2 deletions src/Rules/Cell/IsFloat.php
Expand Up @@ -16,7 +16,7 @@

namespace JBZoo\CsvBlueprint\Rules\Cell;

use JBZoo\CsvBlueprint\Utils;
use Respect\Validation\Validator;

class IsFloat extends AbstractCellRule
{
Expand All @@ -32,7 +32,11 @@ public function getHelpMeta(): array

public function validateRule(string $cellValue): ?string
{
if (Utils::testRegex('/^-?\d+(\.\d+)?$/', $cellValue)) {
if ($cellValue === '') {
return null;
}

if (!Validator::floatVal()->validate($cellValue)) {
return "Value \"<c>{$cellValue}</c>\" is not a float number";
}

Expand Down
8 changes: 6 additions & 2 deletions src/Rules/Cell/IsInt.php
Expand Up @@ -16,7 +16,7 @@

namespace JBZoo\CsvBlueprint\Rules\Cell;

use JBZoo\CsvBlueprint\Utils;
use Respect\Validation\Validator;

final class IsInt extends AbstractCellRule
{
Expand All @@ -32,7 +32,11 @@ public function getHelpMeta(): array

public function validateRule(string $cellValue): ?string
{
if (Utils::testRegex('/^-?\d+$/', $cellValue)) {
if ($cellValue === '') {
return null;
}

if (!Validator::intVal()->validate($cellValue)) {
return "Value \"<c>{$cellValue}</c>\" is not an integer";
}

Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Cell/StartsWith.php
Expand Up @@ -23,7 +23,7 @@ public function getHelpMeta(): array
return [
[],
[
self::DEFAULT => ['"prefix "', 'Example: "prefix Hello World".'],
self::DEFAULT => ["'prefix '", 'Example: "prefix Hello World".'],
],
];
}
Expand Down
15 changes: 4 additions & 11 deletions tests/ReadmeTest.php
Expand Up @@ -78,9 +78,9 @@ public function testBadgeOfRules(): void
$totalRules = $cellRules + $aggRules + $extraRules;

$todoYml = yml(Tools::SCHEMA_TODO);
$planToAdd = \count($todoYml->findArray('columns.0.rules')) +
(\count($todoYml->findArray('columns.0.aggregate_rules')) * 6)
+ \count([
$planToAdd = \count($todoYml->findArray('columns.0.rules')) . '/' .
(\count($todoYml->findArray('columns.0.aggregate_rules')) * 6) . '/' .
\count([
'required',
'null_values',
'multiple + separator',
Expand All @@ -89,16 +89,9 @@ public function testBadgeOfRules(): void
'complex_rules. one example',
'inherit',
'rule not found',
])
- \count([
'first_value',
'second_value',
'last_value',
'sorted',
'custom_func',
]);

$badge = static function (string $label, int $count, string $url, string $color): string {
$badge = static function (string $label, int|string $count, string $url, string $color): string {
$label = \str_replace(' ', '%20', $label);
$badge = "![Static Badge](https://img.shields.io/badge/Rules-{$count}-green" .
"?label={$label}&labelColor={$color}&color=gray)";
Expand Down
17 changes: 13 additions & 4 deletions tests/Rules/Cell/IsFloatTest.php
Expand Up @@ -28,16 +28,21 @@ final class IsFloatTest extends TestAbstractCellRule
public function testPositive(): void
{
$rule = $this->create(true);
isSame(null, $rule->validate(''));
isSame('', $rule->test(''));
isSame('', $rule->test('1'));
isSame('', $rule->test('01'));
isSame('', $rule->test('1.0'));
isSame('', $rule->test('01.0'));
isSame('', $rule->test('.0'));
isSame('', $rule->test('.1'));
isSame('', $rule->test('-1'));
isSame('', $rule->test('-1.0'));
isSame('', $rule->test('1e5'));
isSame('', $rule->test('1E5'));
isSame('', $rule->test(' 1E5'));

$rule = $this->create(false);
isSame(null, $rule->validate(' 1'));
isSame(null, $rule->validate(' q'));
}

public function testNegative(): void
Expand All @@ -48,8 +53,12 @@ public function testNegative(): void
$rule->test('1.000.000'),
);
isSame(
'Value " 1" is not a float number',
$rule->test(' 1'),
'Value "1.000 000" is not a float number',
$rule->test('1.000 000'),
);
isSame(
'Value " q" is not a float number',
$rule->test(' q'),
);
}
}
9 changes: 9 additions & 0 deletions tests/Rules/Cell/IsIntTest.php
Expand Up @@ -34,6 +34,7 @@ public function testPositive(): void
isSame('', $rule->test('0'));
isSame('', $rule->test('00'));
isSame('', $rule->test('-1'));
isSame('', $rule->test('089'));

$rule = $this->create(false);
isSame(null, $rule->validate(' 1'));
Expand All @@ -42,6 +43,14 @@ public function testPositive(): void
public function testNegative(): void
{
$rule = $this->create(true);
isSame(
'Value "1_000_000" is not an integer',
$rule->test('1_000_000'),
);
isSame(
'Value "1000 000" is not an integer',
$rule->test('1000 000'),
);
isSame(
'Value "1.000.000" is not an integer',
$rule->test('1.000.000'),
Expand Down

0 comments on commit aa6e8e5

Please sign in to comment.