Skip to content

Commit ac9d1ac

Browse files
committed
Add multipleOf to FloatSchema
1 parent bd39123 commit ac9d1ac

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

doc/Schema/FloatSchema.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ $schema->minimum(5.0); // Greater than or equal to 5.0
2323
$schema->exclusiveMinimum(5.0); // Greater than 5.0
2424
$schema->exclusiveMaximum(10.0); // Less than 10.0
2525
$schema->maximum(10.0); // Less than or equal to 10.0
26+
$schema->multipleOf(0.5); // Multiple of 0.5
2627
```
2728

2829
### Sign Constraints
@@ -86,4 +87,5 @@ $coordinatesSchema->parse(['lat' => 47.1, 'lng' => 8.2]);
8687
| `float.exclusiveMinimum` | Value is not greater than to threshold |
8788
| `float.exclusiveMaximum` | Value is not less than to threshold |
8889
| `float.maximum` | Value is not less than or equal to threshold |
90+
| `float.multipleOf` | Value is not a multiple of threshold |
8991
| `float.int` | Cannot convert float to int without precision loss (for `toInt()`) |

src/Schema/FloatSchema.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ final class FloatSchema extends AbstractSchemaInnerParse implements SchemaInterf
2424
public const string ERROR_MAXIMUM_CODE = 'float.maximum';
2525
public const string ERROR_MAXIMUM_TEMPLATE = 'Value should be maximum {{maximum}}, {{given}} given';
2626

27+
public const string ERROR_MULTIPLE_OF_CODE = 'float.multipleOf';
28+
public const string ERROR_MULTIPLE_OF_TEMPLATE = 'Value should be multiple of {{multipleOf}}, {{given}} given';
29+
2730
/** @deprecated: see ERROR_MINIMUM_CODE */
2831
public const string ERROR_GTE_CODE = 'float.gte';
2932

@@ -119,6 +122,33 @@ public function maximum(float $maximum): static
119122
});
120123
}
121124

125+
public function multipleOf(float $multipleOf): static
126+
{
127+
if ($multipleOf <= 0.0) {
128+
throw new \InvalidArgumentException(
129+
\sprintf('Argument #1 ($multipleOf) must be greater than 0, %s given', $multipleOf)
130+
);
131+
}
132+
133+
return $this->postParse(static function (float $float) use ($multipleOf) {
134+
$quotient = $float / $multipleOf;
135+
$roundedQuotient = round($quotient);
136+
$epsilon = 10 * PHP_FLOAT_EPSILON * max(1.0, abs($quotient));
137+
138+
if (abs($quotient - $roundedQuotient) <= $epsilon) {
139+
return $float;
140+
}
141+
142+
throw new ErrorsException(
143+
new Error(
144+
self::ERROR_MULTIPLE_OF_CODE,
145+
self::ERROR_MULTIPLE_OF_TEMPLATE,
146+
['multipleOf' => $multipleOf, 'given' => $float]
147+
)
148+
);
149+
});
150+
}
151+
122152
/**
123153
* @deprecated Use minimum($minimum) instead
124154
*/

tests/Unit/Schema/FloatSchemaTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function testImmutability(): void
2929
self::assertNotSame($schema, $schema->exclusiveMinimum(0.0));
3030
self::assertNotSame($schema, $schema->exclusiveMaximum(0.0));
3131
self::assertNotSame($schema, $schema->maximum(0.0));
32+
self::assertNotSame($schema, $schema->multipleOf(0.1));
3233
}
3334

3435
public function testParseSuccess(): void
@@ -367,6 +368,117 @@ public function testParseWithInvalidMaximum(): void
367368
}
368369
}
369370

371+
public function testParseWithValidMultipleOf(): void
372+
{
373+
$input = 0.3;
374+
$multipleOf = 0.1;
375+
376+
$schema = (new FloatSchema())->multipleOf($multipleOf);
377+
378+
self::assertSame($input, $schema->parse($input));
379+
}
380+
381+
public function testParseWithInvalidMultipleOf(): void
382+
{
383+
$input = 0.35;
384+
$multipleOf = 0.1;
385+
386+
$schema = (new FloatSchema())->multipleOf($multipleOf);
387+
388+
try {
389+
$schema->parse($input);
390+
391+
throw new \Exception('code should not be reached');
392+
} catch (ErrorsException $errorsException) {
393+
self::assertSame([
394+
[
395+
'path' => '',
396+
'error' => [
397+
'code' => 'float.multipleOf',
398+
'template' => 'Value should be multiple of {{multipleOf}}, {{given}} given',
399+
'variables' => [
400+
'multipleOf' => $multipleOf,
401+
'given' => $input,
402+
],
403+
],
404+
],
405+
], $errorsException->errors->jsonSerialize());
406+
}
407+
}
408+
409+
public function testMultipleOfWithInvalidMultipleOf(): void
410+
{
411+
$this->expectException(\InvalidArgumentException::class);
412+
$this->expectExceptionMessage('Argument #1 ($multipleOf) must be greater than 0, 0 given');
413+
414+
(new FloatSchema())->multipleOf(0.0);
415+
}
416+
417+
public function testParseWithValidMultipleOfTrickyFloatingPointValues(): void
418+
{
419+
$cases = [
420+
[0.3, 0.1],
421+
[0.7, 0.1],
422+
[0.58, 0.01],
423+
[-0.3, 0.1],
424+
[1.0E-12, 1.0E-13],
425+
];
426+
427+
foreach ($cases as [$input, $multipleOf]) {
428+
$schema = (new FloatSchema())->multipleOf($multipleOf);
429+
430+
self::assertSame($input, $schema->parse($input));
431+
}
432+
}
433+
434+
public function testParseWithInvalidMultipleOfTrickyFloatingPointValues(): void
435+
{
436+
$cases = [
437+
[0.3000000000001, 0.1],
438+
[0.3, 0.2],
439+
[1.0E-12, 3.0E-13],
440+
[-0.35, 0.1],
441+
];
442+
443+
foreach ($cases as [$input, $multipleOf]) {
444+
$schema = (new FloatSchema())->multipleOf($multipleOf);
445+
446+
try {
447+
$schema->parse($input);
448+
449+
throw new \Exception('code should not be reached');
450+
} catch (ErrorsException $errorsException) {
451+
self::assertSame('float.multipleOf', $errorsException->errors->jsonSerialize()[0]['error']['code']);
452+
}
453+
}
454+
}
455+
456+
public function testParseWithValidMultipleOfAtEpsilonBoundary(): void
457+
{
458+
$multipleOf = 1.0;
459+
$input = 1.0 - 10 * PHP_FLOAT_EPSILON;
460+
461+
$schema = (new FloatSchema())->multipleOf($multipleOf);
462+
463+
self::assertSame($input, $schema->parse($input));
464+
}
465+
466+
public function testParseWithInvalidMultipleOfJustAboveEpsilonBoundary(): void
467+
{
468+
$multipleOf = 1.0;
469+
$input = 1.0 - 21 * PHP_FLOAT_EPSILON / 2;
470+
471+
$schema = (new FloatSchema())->multipleOf($multipleOf);
472+
473+
try {
474+
$schema->parse($input);
475+
476+
throw new \Exception('code should not be reached');
477+
} catch (ErrorsException $errorsException) {
478+
self::assertSame('float.multipleOf', $errorsException->errors->jsonSerialize()[0]['error']['code']);
479+
}
480+
}
481+
370482
public function testParseWithValidGte(): void
371483
{
372484
$input = 4.1;

0 commit comments

Comments
 (0)