From f8b262dc123dff82ebe00bb9120903526ad50d76 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Thu, 23 Jul 2026 17:49:20 -0300 Subject: [PATCH] feat: Add month and year arithmetic to LocalDate and Instant. The new plusMonths, minusMonths, plusYears, and minusYears methods clamp to the last valid day of the target month, and on Instant they reanchor through a timezone before normalizing back to UTC. Shifts crossing the supported 0001 to 9999 range raise InvalidLocalDate. This change also adopts the Slevomat coding standard and migrates phpcs to it. --- .claude/hooks/php-ordering-conformance.py | 609 ++++++++++++++++++ .../php-prose-punctuation-conformance.py | 203 ++++++ .claude/rules/php-library-code-style.md | 48 +- .claude/rules/php-library-tooling.md | 27 +- .claude/settings.json | 17 + .claude/skills/tiny-blocks-create/SKILL.md | 5 +- .../assets/config/.gitattributes | 3 + .../assets/config/.gitignore | 2 + .../assets/config/composer.json | 2 + .../assets/config/phpcs.xml | 88 ++- .gitattributes | 3 + .gitignore | 2 + README.md | 129 +++- composer.json | 4 +- phpcs.xml | 88 ++- phpstan.neon.dist | 4 + src/DayOfWeek.php | 6 +- src/Duration.php | 6 +- src/Elapsed.php | 2 +- src/Exceptions/InvalidLocalDate.php | 5 + src/Instant.php | 112 +++- src/Internal/CalendarDay.php | 23 + src/Internal/Seconds.php | 4 +- src/Internal/ZonedShift.php | 30 + src/LocalDate.php | 116 +++- src/Stopwatch.php | 2 +- src/TimeOfDay.php | 4 +- src/Timezones.php | 2 +- tests/Unit/CalendarDayTest.php | 25 + tests/Unit/DayOfWeekTest.php | 2 +- tests/Unit/DurationTest.php | 2 +- tests/Unit/InstantSerializationTest.php | 2 +- tests/Unit/InstantTest.php | 225 ++++++- tests/Unit/LocalDateTest.php | 404 +++++++++++- tests/Unit/PeriodTest.php | 2 +- tests/Unit/SystemMonotonicClockTest.php | 2 +- tests/Unit/TimeOfDayTest.php | 2 +- tests/Unit/ZonedShiftTest.php | 25 + 38 files changed, 2067 insertions(+), 170 deletions(-) create mode 100644 .claude/hooks/php-ordering-conformance.py create mode 100644 .claude/hooks/php-prose-punctuation-conformance.py create mode 100644 src/Internal/CalendarDay.php create mode 100644 src/Internal/ZonedShift.php create mode 100644 tests/Unit/CalendarDayTest.php create mode 100644 tests/Unit/ZonedShiftTest.php diff --git a/.claude/hooks/php-ordering-conformance.py b/.claude/hooks/php-ordering-conformance.py new file mode 100644 index 0000000..cdabb98 --- /dev/null +++ b/.claude/hooks/php-ordering-conformance.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python3 +"""PHP ordering conformance hook for tiny-blocks PHP libraries. + +Self-contained PostToolUse hook on Edit|Write|MultiEdit. Verifies the deterministic +ordering conventions for PHP declarations: + +- Parameter ordering: declaration parameters (constructors, factories, methods, + property promotion) in three tiers, required parameters first, then defaulted + parameters, then a variadic, each tier by identifier length ascending, + alphabetical tie-breaker, semantic pairs preserved. A PHPUnit test method fed by + a data provider is exempt, its parameters are the columns of its data set. +- Member ordering: constants, enum cases, constructor, static methods, instance + methods, in that group order, each group length-ascending with alphabetical + tie-breaker. PHPUnit test classes instead order methods as lifecycle hooks (in + execution order), then other methods, then data providers. + +The analysis is pure (FileUnit in, Violation out) and runs in three passes over +well-formed PHP: a lexical pass blanks every comment, string, and heredoc/nowdoc +body (LITERALS), a structural pass maps every bracket to its pair (bracket_spans); +extraction assigns tokens of interest to their containers by flat walks. Control +flow uses guard clauses only and nesting never exceeds two levels. Reports +violations to stderr and exits 2 to prompt Claude, exits 0 silently if no violations +or the file is out of scope. +""" + +import json +import re +import sys +from dataclasses import dataclass +from enum import Enum +from functools import cached_property +from pathlib import Path +from typing import Final + +# --- Configuration ---------------------------------------------------------- + +# In-scope files: PHP sources under src/ or tests/. +SCOPE_PATTERN: Final = re.compile(r"(^|/)(src|tests)/.+\.php$") + +# Semantic groups. When two or more members of a group appear in the same parameter +# list, they keep their natural order, sorting as a unit at the lead member's key. +# Groups of arbitrary length are supported (calendar and clock components included). +SEMANTIC_GROUPS: Final = ( + ("start", "end"), + ("from", "to"), + ("startAt", "endAt"), + ("createdAt", "updatedAt"), + ("before", "after"), + ("min", "max"), + ("year", "month", "day"), + ("hour", "minute", "second", "nanosecond"), +) + +# Each member maps to (lead, position, members). The members present in the same list +# keep their natural intra-group order, sorting as a unit at the lead member's key. +GROUP_MEMBER: Final = { + member: (group[0], position, group) + for group in SEMANTIC_GROUPS + for position, member in enumerate(group) +} + +MODIFIERS: Final = ("abstract", "final", "private", "protected", "public", "static") + +# The lexical grammar: every PHP construct that must not be scanned as code. +# Alternatives are ordered, the heredoc label closes via backreference. +LITERALS: Final = re.compile( + r""" + /\*.*?\*/ # block comment + | //[^\n]* # line comment + | \#(?!\[)[^\n]* # hash comment, never a #[ attribute + | <<<[ \t]*(?P['"]?)(?P