Skip to content

Commit 70a8bf6

Browse files
committed
fix: resolve floating-point precision bug in multipleOf validation
- Issue: Validator::isFloat()->multipleOf(0.01)->validate(500.01) failed due to precision - Root cause: fmod(500.01, 0.01) returned 0.009999999999980497 instead of 0.0 - Solution: Implement epsilon comparison (1e-9 tolerance) for floating-point remainders - Impact: Decimal multiples (currency, measurements) now validate correctly Technical details: - Enhanced multipleOf() with dual condition: abs(remainder) < epsilon || abs(remainder - divisor) < epsilon - Maintains integer path performance (no epsilon needed for exact arithmetic) - Handles both near-zero and near-divisor remainder cases Examples now working: - 500.01 multipleOf 0.01 ✅ - 19.99 multipleOf 0.01 ✅ - 1234.56 multipleOf 0.01 ✅ - Add comprehensive test coverage for floating-point precision edge cases - Update project documentation with bug fix details - Total test suite: 127 tests, 363 assertions
1 parent 98d3f24 commit 70a8bf6

6 files changed

Lines changed: 31 additions & 5 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ A comprehensive, fluent validation library for PHP inspired by Valibot and Zod.
6565
## 🔧 Development Workflow
6666

6767
### Code Quality
68-
- **Testing** - Pest PHP with organized test suite (10 focused test files, 126 tests, 358 assertions)
68+
- **Testing** - Pest PHP with organized test suite (10 focused test files, 127 tests, 363 assertions)
6969
- **Static Analysis** - PHPStan at maximum level for type safety
7070
- **Code Style** - PHP-CS-Fixer for consistent formatting
7171
- **Performance** - Optimized validation logic with eliminated code duplication
@@ -110,7 +110,7 @@ tests/
110110
- **8 validator types** covering all PHP data types
111111
- **35+ built-in validation methods** including static logical combinators, array filtering, type-aware transformations, form-safe coercion, and intuitive custom validation
112112
- **5,000+ lines of documentation** with practical examples and comprehensive coverage
113-
- **126 unit tests** with comprehensive coverage (358 assertions)
113+
- **127 unit tests** with comprehensive coverage (363 assertions)
114114
- **Zero technical debt** with modern PHP 8.1+ codebase
115115
- **Zero dead links** in documentation with seamless navigation
116116
- **Complete API documentation** with accurate method signatures and examples

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ All notable changes to this project will be documented in this file.
1313
- Added comprehensive test coverage (6 new tests, 23 new assertions)
1414

1515
### Fixed
16+
- **CRITICAL BUG**: Fixed floating-point precision issue in `multipleOf()` validation
17+
- **Issue**: `Validator::isFloat()->multipleOf(0.01)->validate(500.01)` failed due to floating-point arithmetic precision
18+
- **Root Cause**: `fmod(500.01, 0.01)` returned `0.009999999999980497` instead of `0.0`
19+
- **Fix**: Implemented epsilon comparison (`1e-9` tolerance) for floating-point remainder validation
20+
- **Impact**: Decimal multiples (currency, measurements) now validate correctly
21+
- Added comprehensive test coverage for floating-point precision edge cases
1622
- **CRITICAL BUG**: Fixed schema validation field inclusion behavior in ObjectValidator and AssociativeValidator
1723
- **Issue**: Validators were incorrectly including ALL schema fields in results, even when not provided in input
1824
- **Fix**: Now only includes fields that were actually provided in input OR have default values applied

IDEAS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ $formatted = Validator::isString()
4848
- ✅ Smart array coercion (indexed arrays auto-reindex, associative keys preserved)
4949
- ✅ Clean variadic syntax with `pipe(...$transformers)`
5050
- ✅ Perfect integration with external libraries (Laravel, Symfony)
51-
- ✅ Comprehensive test coverage (126 tests, 358 assertions)
51+
- ✅ Comprehensive test coverage (127 tests, 363 assertions)
5252
- ✅ Complete documentation overhaul with comprehensive coverage across all guides
5353
- ✅ Fixed all dead links in documentation for seamless navigation
5454
- ✅ Added type-aware transformation system documentation with detailed examples
5555
- ✅ Documented `filterEmpty()` method with practical use cases
5656
- ✅ Updated API reference to match actual implementation
5757
- ✅ Fixed critical schema validation bug - only include provided fields or fields with defaults in results
5858
- ✅ Enhanced form-safe coercion - empty strings coerce to empty objects/arrays for better form handling
59+
- ✅ Fixed floating-point precision bug in multipleOf validation for accurate decimal calculations
5960

6061
### Intuitive Custom Validation (v0.6.0)
6162
**Status**: ✅ **IMPLEMENTED**

ROADMAP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,12 @@ $stringValidator = Validator::isString()
304304
- [ ] **Interactive documentation** - Runnable examples
305305

306306
### Testing & Quality
307-
- [x] ✅ Organized test suite (10 focused test files, 120 tests, 335 assertions)
307+
- [x] ✅ Organized test suite (10 focused test files, 127 tests, 363 assertions)
308308
- [x] ✅ 100% PHPStan compliance
309309
- [x] ✅ PHP-CS-Fixer standards
310310
- [x] ✅ Static logical combinators test coverage
311311
- [x] ✅ Comprehensive schema validation test coverage
312+
- [x] ✅ Floating-point precision bug fixes with comprehensive edge case testing
312313
- [ ] **Mutation testing** - Enhanced test quality verification
313314
- [ ] **Property-based testing** - Randomized validation testing
314315
- [ ] **Performance benchmarking** - Continuous performance monitoring

src/Lemmon/NumericConstraintsTrait.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ function ($value, $key = null, $input = null) use ($divisor) {
5454
if (is_int($divisor) && is_int($value)) {
5555
return $value % $divisor === 0;
5656
}
57-
return fmod((float) $value, (float) $divisor) === 0.0;
57+
58+
// Use epsilon comparison for floating-point precision
59+
$remainder = fmod((float) $value, (float) $divisor);
60+
$epsilon = 1e-9; // Tolerance for floating-point comparison
61+
return abs($remainder) < $epsilon || abs($remainder - $divisor) < $epsilon;
5862
},
5963
$message ?? "Value must be a multiple of {$divisor}"
6064
);

tests/FloatValidatorTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,17 @@
6060

6161
$positiveValidator->validate(-1);
6262
})->throws(Lemmon\ValidationException::class, 'Value must be positive');
63+
64+
it('should handle floating-point precision in multipleOf validation', function () {
65+
// Test cases that previously failed due to floating-point precision
66+
$validator = Validator::isFloat()->multipleOf(0.01);
67+
68+
expect($validator->validate(500.01))->toBe(500.01); // Original bug case
69+
expect($validator->validate(19.99))->toBe(19.99); // Another precision case
70+
expect($validator->validate(1234.56))->toBe(1234.56); // Larger number
71+
72+
// Test with smaller precision
73+
$smallValidator = Validator::isFloat()->multipleOf(0.001);
74+
expect($smallValidator->validate(0.999))->toBe(0.999);
75+
expect($smallValidator->validate(1.001))->toBe(1.001);
76+
});

0 commit comments

Comments
 (0)