diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json index a38ed6a6..d995a5e3 100644 --- a/docs/.vitepress/toc_en.json +++ b/docs/.vitepress/toc_en.json @@ -1,11 +1,38 @@ { "/": [ { - "text": "CakePHP Chronos", + "text": "Getting Started", "collapsed": false, "items": [ { "text": "Introduction", "link": "/index" }, - { "text": "3.x Migration Guide", "link": "/3-x-migration-guide" }, + { "text": "Creating Instances", "link": "/creating-instances" }, + { "text": "3.x Migration Guide", "link": "/3-x-migration-guide" } + ] + }, + { + "text": "Working with Chronos", + "collapsed": false, + "items": [ + { "text": "Modifying Values", "link": "/modifying" }, + { "text": "Comparing Values", "link": "/comparing" }, + { "text": "Formatting & Output", "link": "/formatting" } + ] + }, + { + "text": "Other Classes", + "collapsed": false, + "items": [ + { "text": "ChronosDate", "link": "/chronos-date" }, + { "text": "ChronosTime", "link": "/chronos-time" }, + { "text": "Periods & Intervals", "link": "/periods-and-intervals" } + ] + }, + { + "text": "Advanced", + "collapsed": false, + "items": [ + { "text": "Testing", "link": "/testing" }, + { "text": "Configuration", "link": "/configuration" }, { "text": "API", "link": "https://api.cakephp.org/chronos" } ] } diff --git a/docs/en/chronos-date.md b/docs/en/chronos-date.md new file mode 100644 index 00000000..41d77a00 --- /dev/null +++ b/docs/en/chronos-date.md @@ -0,0 +1,172 @@ +# ChronosDate + +PHP provides only datetime classes that combine both date and time parts. +Representing calendar dates can be awkward with `DateTimeImmutable` as it includes +time and timezones, which aren't part of a "date". + +Chronos provides `ChronosDate` which allows you to represent dates without time. +The time is always fixed to `00:00:00` and is not affected by the server timezone +or modifier methods. + +## Creating Instances + +```php +use Cake\Chronos\ChronosDate; + +$today = ChronosDate::today(); +$yesterday = ChronosDate::yesterday(); +$tomorrow = ChronosDate::tomorrow(); + +// Parse from string +$date = ChronosDate::parse('2015-12-25'); + +// Create from components +$date = ChronosDate::create(2015, 12, 25); + +// Parse with format +$date = ChronosDate::createFromFormat('m/d/Y', '12/25/2015'); + +// From array +$date = ChronosDate::createFromArray([ + 'year' => 2015, + 'month' => 12, + 'day' => 25, +]); +``` + +## Timezone for "Today" + +Although `ChronosDate` uses a fixed internal timezone, you can specify which +timezone to use for determining the current date: + +```php +// Takes the current date from Asia/Tokyo timezone +$today = ChronosDate::today('Asia/Tokyo'); +``` + +This is useful when you need "today" in a specific timezone that differs from +the server's timezone. + +## Time is Ignored + +Time modifications have no effect on ChronosDate: + +```php +$today = ChronosDate::today(); + +// Time modifications are ignored +$today->modify('+1 hour'); // Still the same date + +// Outputs just the date: '2015-12-20' +echo $today; +``` + +## Available Methods + +ChronosDate includes most of the same modifier and comparison methods as Chronos, +but without time-related operations: + +### Modifiers + +```php +$date = ChronosDate::today(); + +// Add/subtract time periods +$date->addDays(5); +$date->subWeeks(2); +$date->addMonths(3); +$date->addYears(1); + +// Jump to boundaries +$date->startOfMonth(); +$date->endOfMonth(); +$date->startOfYear(); +$date->endOfYear(); +$date->startOfWeek(); +$date->endOfWeek(); + +// Day of week navigation +$date->next(Chronos::MONDAY); +$date->previous(Chronos::FRIDAY); +$date->firstOfMonth(Chronos::TUESDAY); +$date->lastOfMonth(Chronos::SUNDAY); +``` + +### Comparisons + +```php +$date1 = ChronosDate::parse('2015-12-25'); +$date2 = ChronosDate::parse('2015-12-31'); + +$date1->equals($date2); +$date1->lessThan($date2); +$date1->greaterThan($date2); +$date1->between($start, $end); + +// Calendar checks +$date->isToday(); +$date->isYesterday(); +$date->isTomorrow(); +$date->isFuture(); +$date->isPast(); +$date->isWeekend(); +$date->isWeekday(); +$date->isMonday(); +// etc. +``` + +### Differences + +```php +$date1->diffInDays($date2); +$date1->diffInWeeks($date2); +$date1->diffInMonths($date2); +$date1->diffInYears($date2); +$date1->diffInWeekdays($date2); +$date1->diffInWeekendDays($date2); + +// Human readable +$date1->diffForHumans($date2); // "6 days before" +``` + +## Extracting Components + +```php +$date = ChronosDate::parse('2015-12-25'); + +$date->year; // 2015 +$date->month; // 12 +$date->day; // 25 +$date->dayOfWeek; // 5 (Friday) +$date->dayOfYear; // 359 +$date->quarter; // 4 +``` + +## Converting to Array + +```php +$date = ChronosDate::parse('2015-12-25'); +$array = $date->toArray(); +// [ +// 'year' => 2015, +// 'month' => 12, +// 'day' => 25, +// 'dayOfWeek' => 5, +// 'dayOfYear' => 359, +// ] +``` + +## Converting to DateTime + +If you need a full datetime, you can convert to `DateTimeImmutable`: + +```php +$date = ChronosDate::parse('2015-12-25'); + +// Convert with optional timezone +$datetime = $date->toDateTimeImmutable(); +$datetime = $date->toDateTimeImmutable('America/New_York'); + +// Alias +$datetime = $date->toNative(); +``` diff --git a/docs/en/chronos-time.md b/docs/en/chronos-time.md new file mode 100644 index 00000000..f2bbc277 --- /dev/null +++ b/docs/en/chronos-time.md @@ -0,0 +1,173 @@ +# ChronosTime + +`ChronosTime` represents a time of day without any date component. This is useful +for representing things like business hours, schedules, or any scenario where +you need to work with time independently of a specific date. + +## Creating Instances + +```php +use Cake\Chronos\ChronosTime; + +// Current time +$now = ChronosTime::now(); + +// Parse from string +$time = ChronosTime::parse('14:30:00'); +$time = ChronosTime::parse('2:30 PM'); + +// From a Chronos or DateTimeInterface +$chronos = new Chronos('2015-12-25 14:30:00'); +$time = ChronosTime::parse($chronos); + +// Special times +$midnight = ChronosTime::midnight(); // 00:00:00 +$noon = ChronosTime::noon(); // 12:00:00 +$endOfDay = ChronosTime::endOfDay(); // 23:59:59 +``` + +## Getting and Setting Components + +```php +$time = ChronosTime::parse('14:30:45.123456'); + +// Getters +$time->getHours(); // 14 +$time->getMinutes(); // 30 +$time->getSeconds(); // 45 +$time->getMicroseconds(); // 123456 + +// Setters (return new instance) +$time = $time->setHours(16); +$time = $time->setMinutes(45); +$time = $time->setSeconds(30); +$time = $time->setMicroseconds(0); + +// Set all at once +$time = $time->setTime(16, 45, 30, 0); +``` + +## Hour Boundaries + +Jump to the start or end of the current hour: + +```php +$time = ChronosTime::parse('14:35:22'); + +$time->startOfHour(); // 14:00:00 +$time->endOfHour(); // 14:59:59 +``` + +## Comparisons + +ChronosTime provides comparison methods similar to Chronos: + +```php +$time1 = ChronosTime::parse('09:00:00'); +$time2 = ChronosTime::parse('17:00:00'); + +$time1->equals($time2); // false +$time1->greaterThan($time2); // false +$time1->greaterThanOrEquals($time2); // false +$time1->lessThan($time2); // true +$time1->lessThanOrEquals($time2); // true + +// Check if between two times +$lunchTime = ChronosTime::parse('12:30:00'); +$lunchTime->between($time1, $time2); // true +``` + +## Special Time Checks + +```php +$time = ChronosTime::parse('00:00:00'); + +$time->isMidnight(); // true (00:00:00) +$time->isStartOfDay(); // true (alias for midnight) +$time->isMidday(); // false (checks for 12:00:00) +$time->isEndOfDay(); // false (checks for 23:59:59) +``` + +## Formatting + +```php +$time = ChronosTime::parse('14:30:45'); + +// Default format (H:i:s) +echo $time; // "14:30:45" + +// Custom format +echo $time->format('g:i A'); // "2:30 PM" +echo $time->format('H:i'); // "14:30" +``` + +### Custom Default Format + +You can change the default string format: + +```php +ChronosTime::setToStringFormat('g:i A'); +echo ChronosTime::parse('14:30:00'); // "2:30 PM" + +// Reset to default +ChronosTime::resetToStringFormat(); +``` + +## Converting to Array + +```php +$time = ChronosTime::parse('14:30:45.123456'); +$array = $time->toArray(); +// [ +// 'hour' => 14, +// 'minute' => 30, +// 'second' => 45, +// 'microsecond' => 123456, +// ] +``` + +## Converting to DateTime + +If you need a full datetime, you can convert to `DateTimeImmutable`: + +```php +$time = ChronosTime::parse('14:30:00'); + +// Converts to today at the given time +$datetime = $time->toDateTimeImmutable(); + +// With specific timezone +$datetime = $time->toDateTimeImmutable('America/New_York'); + +// Alias +$datetime = $time->toNative(); +``` + +## Use Cases + +### Business Hours + +```php +$openTime = ChronosTime::parse('09:00:00'); +$closeTime = ChronosTime::parse('17:00:00'); +$now = ChronosTime::now(); + +if ($now->between($openTime, $closeTime)) { + echo "We're open!"; +} +``` + +### Scheduling + +```php +$meetingTime = ChronosTime::parse('14:00:00'); +$currentTime = ChronosTime::now(); + +if ($currentTime->lessThan($meetingTime)) { + echo "Meeting hasn't started yet"; +} elseif ($currentTime->equals($meetingTime)) { + echo "Meeting is starting now!"; +} else { + echo "Meeting has started"; +} +``` diff --git a/docs/en/comparing.md b/docs/en/comparing.md new file mode 100644 index 00000000..e470e398 --- /dev/null +++ b/docs/en/comparing.md @@ -0,0 +1,149 @@ +# Comparing Values + +Once you have 2 instances of Chronos date/time objects you can compare them in +a variety of ways. + +## Comparison Methods + +```php +// Full suite of comparators +$first->equals($second); +$first->notEquals($second); +$first->greaterThan($second); +$first->greaterThanOrEquals($second); +$first->lessThan($second); +$first->lessThanOrEquals($second); + +// See if the current object is between two others +$now->between($start, $end); + +// Find which argument is closest or farthest +$now->closest($june, $november); +$now->farthest($june, $november); + +// Get the min or max compared to another date +$now->min($other); // Returns earlier of the two +$now->max($other); // Returns later of the two + +// Get the average between two dates +$average = $now->average($other); +``` + +## Calendar Checks + +You can inquire about where a given value falls on the calendar: + +```php +$now->isToday(); +$now->isYesterday(); +$now->isTomorrow(); +$now->isFuture(); +$now->isPast(); + +// Week checks +$now->isLastWeek(); +$now->isNextWeek(); + +// Month checks +$now->isLastMonth(); +$now->isNextMonth(); + +// Year checks +$now->isLastYear(); +$now->isNextYear(); + +// Half-year checks +$now->isFirstHalf(); // January - June +$now->isSecondHalf(); // July - December + +// Leap year +$now->isLeapYear(); +``` + +## Day of Week Checks + +```php +// Check if weekend +$now->isWeekend(); +$now->isWeekday(); + +// Check specific days +$now->isMonday(); +$now->isTuesday(); +$now->isWednesday(); +$now->isThursday(); +$now->isFriday(); +$now->isSaturday(); +$now->isSunday(); +``` + +## Relative Time Checks + +You can find out if a value was within a relative time period: + +```php +$time->wasWithinLast('3 days'); +$time->isWithinNext('3 hours'); +``` + +## Generating Differences + +In addition to comparing datetimes, calculating differences or deltas between +two values is a common task: + +```php +// Get a DateInterval representing the difference +$first->diff($second); + +// Get difference as a count of specific units +$first->diffInHours($second); +$first->diffInDays($second); +$first->diffInWeeks($second); +$first->diffInMonths($second); +$first->diffInYears($second); +``` + +### Absolute vs Relative Differences + +By default, diff methods return absolute values. Pass `false` to get signed values: + +```php +$past = Chronos::parse('-3 days'); +$now = Chronos::now(); + +$past->diffInDays($now); // 3 (absolute) +$past->diffInDays($now, false); // -3 (relative, past is negative) +``` + +### Filtered Differences + +Calculate differences with custom filtering: + +```php +// Count weekdays between two dates +$start->diffInWeekdays($end); + +// Count weekend days between two dates +$start->diffInWeekendDays($end); + +// Custom filter +$start->diffInDaysFiltered(function ($date) { + return $date->isWeekday(); +}, $end); +``` + +## Human-Readable Differences + +You can generate human readable differences suitable for use in a feed or +timeline: + +```php +// Difference from now +echo $date->diffForHumans(); // "3 hours ago" + +// Difference from another point in time +echo $date->diffForHumans($other); // "1 hour ago" + +// Without "ago"/"from now" suffix +echo $date->diffForHumans(null, true); // "3 hours" +``` diff --git a/docs/en/configuration.md b/docs/en/configuration.md new file mode 100644 index 00000000..2998eeac --- /dev/null +++ b/docs/en/configuration.md @@ -0,0 +1,165 @@ +# Configuration + +Chronos provides several configuration options for customizing behavior. + +## Week Configuration + +### Week Start and End + +By default, weeks start on Monday (1) and end on Sunday (7). You can customize this: + +```php +use Cake\Chronos\Chronos; + +// Set week to start on Sunday (7) +Chronos::setWeekStartsAt(Chronos::SUNDAY); + +// Set week to end on Saturday (6) +Chronos::setWeekEndsAt(Chronos::SATURDAY); + +// Get current settings +$start = Chronos::getWeekStartsAt(); // 7 +$end = Chronos::getWeekEndsAt(); // 6 +``` + +This affects methods like `startOfWeek()` and `endOfWeek()`: + +```php +Chronos::setWeekStartsAt(Chronos::SUNDAY); + +$date = Chronos::parse('2024-01-15'); // Monday +$date->startOfWeek(); // Returns Sunday, January 14 +``` + +### Weekend Days + +By default, Saturday and Sunday are considered weekend days. You can customize this: + +```php +// Set weekend to Friday and Saturday +Chronos::setWeekendDays([ + Chronos::FRIDAY, + Chronos::SATURDAY, +]); + +// Get current weekend days +$weekendDays = Chronos::getWeekendDays(); +``` + +This affects methods like `isWeekend()` and `isWeekday()`: + +```php +Chronos::setWeekendDays([Chronos::FRIDAY, Chronos::SATURDAY]); + +$friday = Chronos::parse('2024-01-12'); // Friday +$friday->isWeekend(); // true +$friday->isWeekday(); // false + +$sunday = Chronos::parse('2024-01-14'); // Sunday +$sunday->isWeekend(); // false +$sunday->isWeekday(); // true +``` + +## Day Constants + +Chronos provides constants for days of the week: + +```php +Chronos::MONDAY; // 1 +Chronos::TUESDAY; // 2 +Chronos::WEDNESDAY; // 3 +Chronos::THURSDAY; // 4 +Chronos::FRIDAY; // 5 +Chronos::SATURDAY; // 6 +Chronos::SUNDAY; // 7 +``` + +## Translation / Localization + +Chronos includes a simple translation system for `diffForHumans()` output. +By default, only English translations are included. + +### Using with CakePHP I18n + +For full localization support, use the `cakephp/i18n` package: + +```php +use Cake\Chronos\Chronos; +use Cake\I18n\RelativeTimeFormatter; + +// Set up the formatter with CakePHP's i18n +Chronos::diffFormatter(new RelativeTimeFormatter()); + +// Now diffForHumans() will use localized strings +$date = Chronos::parse('-3 days'); +echo $date->diffForHumans(); // Localized output +``` + +### Custom Formatter + +You can implement `DifferenceFormatterInterface` for custom formatting: + +```php +use Cake\Chronos\DifferenceFormatterInterface; +use DateTimeInterface; + +class MyFormatter implements DifferenceFormatterInterface +{ + public function diffForHumans( + DateTimeInterface $date, + ?DateTimeInterface $other = null, + bool $absolute = false + ): string { + // Your custom implementation + } +} + +Chronos::diffFormatter(new MyFormatter()); +``` + +## PSR-20 Clock Interface + +Chronos provides `ClockFactory` which implements PSR-20's `ClockInterface`. +This is useful for dependency injection and testing: + +```php +use Cake\Chronos\ClockFactory; + +$clock = new ClockFactory(); +$now = $clock->now(); // Returns DateTimeImmutable +``` + +The `ClockFactory` respects `Chronos::setTestNow()`, making it useful for testing: + +```php +Chronos::setTestNow('2024-01-15 12:00:00'); + +$clock = new ClockFactory(); +$now = $clock->now(); // Returns 2024-01-15 12:00:00 +``` + +### Using in Your Application + +```php +use Psr\Clock\ClockInterface; + +class MyService +{ + public function __construct( + private ClockInterface $clock + ) {} + + public function doSomething(): void + { + $now = $this->clock->now(); + // ... + } +} + +// In production +$service = new MyService(new ClockFactory()); + +// In tests +Chronos::setTestNow('2024-01-15'); +$service = new MyService(new ClockFactory()); +``` diff --git a/docs/en/creating-instances.md b/docs/en/creating-instances.md new file mode 100644 index 00000000..4c51484e --- /dev/null +++ b/docs/en/creating-instances.md @@ -0,0 +1,84 @@ +# Creating Instances + +There are many ways to get an instance of Chronos or ChronosDate. There are a number of +factory methods that work with different argument sets. + +## Basic Factory Methods + +```php +use Cake\Chronos\Chronos; + +$now = Chronos::now(); +$today = Chronos::today(); +$yesterday = Chronos::yesterday(); +$tomorrow = Chronos::tomorrow(); + +// Parse relative expressions +$date = Chronos::parse('+2 days, +3 hours'); + +// Date and time integer values +$date = Chronos::create(2015, 12, 25, 4, 32, 58); + +// Date or time integer values +$date = Chronos::createFromDate(2015, 12, 25); +$date = Chronos::createFromTime(11, 45, 10); + +// Parse formatted values +$date = Chronos::createFromFormat('m/d/Y', '06/15/2015'); +``` + +## Creating from Timestamps + +You can create Chronos instances from Unix timestamps: + +```php +// From integer timestamp +$date = Chronos::createFromTimestamp(1450000000); + +// From timestamp with timezone +$date = Chronos::createFromTimestamp(1450000000, 'America/New_York'); +``` + +## Creating from Arrays + +You can create instances from an array of date/time components: + +```php +$date = Chronos::createFromArray([ + 'year' => 2015, + 'month' => 12, + 'day' => 25, + 'hour' => 14, + 'minute' => 30, + 'second' => 0, +]); +``` + +## Min and Max Values + +You can get the minimum and maximum representable dates: + +```php +$min = Chronos::minValue(); // Earliest possible date +$max = Chronos::maxValue(); // Latest possible date +``` + +## Creating from Existing DateTimeInterface + +If you already have a `DateTimeInterface` instance, you can convert it to Chronos: + +```php +$native = new DateTimeImmutable('2015-12-25 14:30:00'); +$chronos = Chronos::instance($native); +``` + +## Timezone Handling + +All factory methods accept a timezone parameter: + +```php +// Create with specific timezone +$date = Chronos::now('Asia/Tokyo'); +$date = Chronos::today('Europe/London'); +$date = Chronos::create(2015, 12, 25, 14, 30, 0, 'America/New_York'); +``` diff --git a/docs/en/formatting.md b/docs/en/formatting.md new file mode 100644 index 00000000..3528a428 --- /dev/null +++ b/docs/en/formatting.md @@ -0,0 +1,124 @@ +# Formatting and Output + +Chronos provides a number of methods for displaying or outputting datetime +objects. + +## String Conversion + +```php +// Uses the format controlled by setToStringFormat() +echo $date; + +// Explicit format +echo $date->format('Y-m-d H:i:s'); +``` + +## Standard Format Methods + +```php +echo $time->toAtomString(); // 1975-12-25T14:15:16-05:00 +echo $time->toCookieString(); // Thursday, 25-Dec-1975 14:15:16 EST +echo $time->toIso8601String(); // 1975-12-25T14:15:16-05:00 +echo $time->toRfc822String(); // Thu, 25 Dec 75 14:15:16 -0500 +echo $time->toRfc850String(); // Thursday, 25-Dec-75 14:15:16 EST +echo $time->toRfc1036String(); // Thu, 25 Dec 75 14:15:16 -0500 +echo $time->toRfc1123String(); // Thu, 25 Dec 1975 14:15:16 -0500 +echo $time->toRfc2822String(); // Thu, 25 Dec 1975 14:15:16 -0500 +echo $time->toRfc3339String(); // 1975-12-25T14:15:16-05:00 +echo $time->toRfc7231String(); // Thu, 25 Dec 1975 14:15:16 GMT +echo $time->toRssString(); // Thu, 25 Dec 1975 14:15:16 -0500 +echo $time->toW3cString(); // 1975-12-25T14:15:16-05:00 +``` + +## Common Format Methods + +```php +echo $time->toTimeString(); // 14:15:16 +echo $time->toDateString(); // 1975-12-25 +echo $time->toDateTimeString(); // 1975-12-25 14:15:16 +echo $time->toFormattedDateString(); // Dec 25, 1975 +echo $time->toDayDateTimeString(); // Thu, Dec 25, 1975 2:15 PM +``` + +## Quarter and Week + +```php +echo $time->toQuarter(); // 4 +echo $time->toWeek(); // 52 + +// Get the start and end dates of the quarter +[$start, $end] = $time->toQuarterRange(); +``` + +## Converting to Array + +You can convert Chronos instances to an array of components: + +```php +$time = new Chronos('2015-12-25 14:30:45.123456'); +$array = $time->toArray(); +// [ +// 'year' => 2015, +// 'month' => 12, +// 'day' => 25, +// 'hour' => 14, +// 'minute' => 30, +// 'second' => 45, +// 'microsecond' => 123456, +// 'dayOfWeek' => 5, +// 'dayOfYear' => 359, +// 'timezone' => 'UTC', +// ] +``` + +This also works with `ChronosDate` and `ChronosTime`: + +```php +$date = new ChronosDate('2015-12-25'); +$date->toArray(); +// ['year' => 2015, 'month' => 12, 'day' => 25, 'dayOfWeek' => 5, 'dayOfYear' => 359] + +$time = new ChronosTime('14:30:45.123456'); +$time->toArray(); +// ['hour' => 14, 'minute' => 30, 'second' => 45, 'microsecond' => 123456] +``` + +## Extracting Components + +You can access parts of a date object directly via properties: + +```php +$time = new Chronos('2015-12-31 23:59:58.123456'); +$time->year; // 2015 +$time->month; // 12 +$time->day; // 31 +$time->hour; // 23 +$time->minute; // 59 +$time->second; // 58 +$time->micro; // 123456 +``` + +Other properties that can be accessed: + +- `timezone` - The DateTimeZone instance +- `timezoneName` - Timezone name string +- `dayOfWeek` - 1 (Monday) through 7 (Sunday) +- `dayOfMonth` - 1 through 31 +- `dayOfYear` - 1 through 366 +- `daysInMonth` - 28 through 31 +- `timestamp` - Unix timestamp +- `quarter` - 1 through 4 +- `half` - 1 or 2 + +## Customizing Default Format + +You can customize the default string format used when casting to string: + +```php +Chronos::setToStringFormat('Y-m-d'); + +echo Chronos::now(); // "2015-12-25" + +// Reset to default +Chronos::resetToStringFormat(); +``` diff --git a/docs/en/index.md b/docs/en/index.md index d01afc51..0aa2329c 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -21,34 +21,9 @@ using `toDateTimeImmutable()`. To install Chronos, you should use `composer`. From your application's ROOT directory (where composer.json file is located) run the following: -```php -php composer.phar require "cakephp/chronos:^3.0" -``` - -## Creating Instances - -There are many ways to get an instance of Chronos or Date. There are a number of -factory methods that work with different argument sets: -```php -use Cake\Chronos\Chronos; - -$now = Chronos::now(); -$today = Chronos::today(); -$yesterday = Chronos::yesterday(); -$tomorrow = Chronos::tomorrow(); -// Parse relative expressions -$date = Chronos::parse('+2 days, +3 hours'); - -// Date and time integer values. -$date = Chronos::create(2015, 12, 25, 4, 32, 58); - -// Date or time integer values. -$date = Chronos::createFromDate(2015, 12, 25); -$date = Chronos::createFromTime(11, 45, 10); - -// Parse formatted values. -$date = Chronos::createFromFormat('m/d/Y', '06/15/2015'); +```bash +composer require "cakephp/chronos:^3.0" ``` ## Working with Immutable Objects @@ -63,6 +38,7 @@ around datetimes are not always easy to identify, data can be modified accidenta or without the developer knowing. Immutable objects prevent accidental changes to data, and make code free of order-based dependency issues. Immutability does mean that you will need to remember to replace variables when using modifiers: + ```php // This code doesn't work with immutable objects $chronos->addDay(1); @@ -73,244 +49,7 @@ return $chronos; $chronos = $chronos->addDay(1); $chronos = doSomething($chronos); return $chronos; - ``` + By capturing the return value of each modification your code will work as expected. - -## Date Objects - -PHP provides only date-time classes that combines both dates and time parts. -Representing calendar dates can be a bit awkward with `DateTimeImmutable` as it includes -time and timezones, which aren't part of a 'date'. Chronos provides -`ChronosDate` that allows you to represent dates. The time these objects -these objects is always fixed to `00:00:00` and not affeced by the server time zone -or modify helpers: -```php -use Cake\Chronos\ChronosDate; - -$today = ChronosDate::today(); - -// Changes to the time/timezone are ignored. -$today->modify('+1 hours'); - -// Outputs '2015-12-20' -echo $today; - -``` -Although `ChronosDate` uses a fixed time zone internally, you can specify which -time zone to use for current time such as `now()` or `today()`: -```php -use Cake\Chronos\ChronosDate; - -// Takes the current date from Asia/Tokyo time zone -$today = ChronosDate::today('Asia/Tokyo'); -``` - -## Modifier Methods - -Chronos objects provide modifier methods that let you modify the value in -a granular way: -```php -// Set components of the datetime value. -$halloween = Chronos::create() - ->year(2015) - ->month(10) - ->day(31) - ->hour(20) - ->minute(30); - -``` -You can also modify parts of the datetime relatively: -```php -$future = Chronos::create() - ->addYears(1) - ->subMonths(2) - ->addDays(15) - ->addHours(20) - ->subMinutes(2); - -``` -It is also possible to make big jumps to defined points in time: -```php -$time = Chronos::create(); -$time->startOfDay(); -$time->endOfDay(); -$time->startOfMonth(); -$time->endOfMonth(); -$time->startOfYear(); -$time->endOfYear(); -$time->startOfWeek(); -$time->endOfWeek(); - -``` -Or jump to specific days of the week: -```php -$time->next(Chronos::TUESDAY); -$time->previous(Chronos::MONDAY); - -``` -When modifying dates/times across DST (Daylight Savings Time) transitions -your operations may gain/lose an additional hours resulting in hour values that -don't add up. You can avoid these issues by first changing your timezone to -`UTC`, modifying the time: -```php -// Additional hour gained. -$time = new Chronos('2014-03-30 00:00:00', 'Europe/London'); -debug($time->modify('+24 hours')); // 2014-03-31 01:00:00 - -// First switch to UTC, and modify -$time = $time->setTimezone('UTC') - ->modify('+24 hours'); - -``` -Once you are done modifying the time you can add the original timezone to get -the localized time. - -## Comparison Methods - -Once you have 2 instances of Chronos date/time objects you can compare them in -a variety of ways: -```php -// Full suite of comparators exist -// equals, notEquals, greaterThan, greaterThanOrEquals, lessThan, lessThanOrEquals -$first->equals($second); -$first->greaterThanOrEquals($second); - -// See if the current object is between two others. -$now->between($start, $end); - -// Find which argument is closest or farthest. -$now->closest($june, $november); -$now->farthest($june, $november); - -``` -You can also inquire about where a given value falls on the calendar: -```php -$now->isToday(); -$now->isYesterday(); -$now->isFuture(); -$now->isPast(); - -// Check the day of the week -$now->isWeekend(); - -// All other weekday methods exist too. -$now->isMonday(); - -``` -You can also find out if a value was within a relative time period: -```php -$time->wasWithinLast('3 days'); -$time->isWithinNext('3 hours'); -``` - -## Generating Differences - -In addition to comparing datetimes, calculating differences or deltas between -two values is a common task: -```php -// Get a DateInterval representing the difference -$first->diff($second); - -// Get difference as a count of specific units. -$first->diffInHours($second); -$first->diffInDays($second); -$first->diffInWeeks($second); -$first->diffInYears($second); - -``` -You can generate human readable differences suitable for use in a feed or -timeline: -```php -// Difference from now. -echo $date->diffForHumans(); - -// Difference from another point in time. -echo $date->diffForHumans($other); // 1 hour ago; -``` - -## Formatting Strings - -Chronos provides a number of methods for displaying our outputting datetime -objects: -```php -// Uses the format controlled by setToStringFormat() -echo $date; - -// Different standard formats -echo $time->toAtomString(); // 1975-12-25T14:15:16-05:00 -echo $time->toCookieString(); // Thursday, 25-Dec-1975 14:15:16 EST -echo $time->toIso8601String(); // 1975-12-25T14:15:16-05:00 -echo $time->toRfc822String(); // Thu, 25 Dec 75 14:15:16 -0500 -echo $time->toRfc850String(); // Thursday, 25-Dec-75 14:15:16 EST -echo $time->toRfc1036String(); // Thu, 25 Dec 75 14:15:16 -0500 -echo $time->toRfc1123String(); // Thu, 25 Dec 1975 14:15:16 -0500 -echo $time->toRfc2822String(); // Thu, 25 Dec 1975 14:15:16 -0500 -echo $time->toRfc3339String(); // 1975-12-25T14:15:16-05:00 -echo $time->toRssString(); // Thu, 25 Dec 1975 14:15:16 -0500 -echo $time->toW3cString(); // 1975-12-25T14:15:16-05:00 - -// Get the quarter/week -echo $time->toQuarter(); // 4 -echo $time->toWeek(); // 52 - -// Generic formatting -echo $time->toTimeString(); // 14:15:16 -echo $time->toDateString(); // 1975-12-25 -echo $time->toDateTimeString(); // 1975-12-25 14:15:16 -echo $time->toFormattedDateString(); // Dec 25, 1975 -echo $time->toDayDateTimeString(); // Thu, Dec 25, 1975 2:15 PM -``` - -## Extracting Date Components - -Getting parts of a date object can be done by directly accessing properties: -```php -$time = new Chronos('2015-12-31 23:59:58.123'); -$time->year; // 2015 -$time->month; // 12 -$time->day; // 31 -$time->hour // 23 -$time->minute // 59 -$time->second // 58 -$time->micro // 123 - -``` -Other properties that can be accessed are: - -- timezone -- timezoneName -- dayOfWeek -- dayOfMonth -- dayOfYear -- daysInMonth -- timestamp -- quarter -- half - -## Testing Aids - -When writing unit tests, it is helpful to fixate the current time. Chronos lets -you fix the current time for each class. As part of your test suite's bootstrap -process you can include the following: -```php -Chronos::setTestNow(Chronos::now()); -ChronosDate::setTestNow(ChronosDate::parse(Chronos::now())); - -``` -This will fix the current time of all objects to be the point at which the test -suite started. - -For example, if you fixate the `Chronos` to some moment in the past, any new -instance of `Chronos` created with `now` or a relative time string, will be -returned relative to the fixated time: -```php -Chronos::setTestNow(new Chronos('1975-12-25 00:00:00')); - -$time = new Chronos(); // 1975-12-25 00:00:00 -$time = new Chronos('1 hour ago'); // 1975-12-24 23:00:00 - -``` -To reset the fixation, simply call `setTestNow()` again with no parameter or -with `null` as a parameter. diff --git a/docs/en/modifying.md b/docs/en/modifying.md new file mode 100644 index 00000000..c5a96a63 --- /dev/null +++ b/docs/en/modifying.md @@ -0,0 +1,184 @@ +# Modifying Values + +Chronos objects provide modifier methods that let you modify the value in +a granular way. Remember that all Chronos objects are immutable, so modifier +methods return a new instance. + +## Setting Components + +You can set individual components of the datetime value: + +```php +// Set components of the datetime value +$halloween = Chronos::create() + ->year(2015) + ->month(10) + ->day(31) + ->hour(20) + ->minute(30); +``` + +## Relative Modifications + +You can modify parts of the datetime relatively: + +```php +$future = Chronos::now() + ->addYears(1) + ->subMonths(2) + ->addDays(15) + ->addHours(20) + ->subMinutes(2); +``` + +Available add/sub methods: + +- `addYears()` / `subYears()` +- `addMonths()` / `subMonths()` +- `addWeeks()` / `subWeeks()` +- `addDays()` / `subDays()` +- `addWeekdays()` / `subWeekdays()` +- `addHours()` / `subHours()` +- `addMinutes()` / `subMinutes()` +- `addSeconds()` / `subSeconds()` + +### Month Overflow Handling + +By default, adding months will clamp the day if it would overflow: + +```php +// January 31 + 1 month = February 28 (clamped) +$date = Chronos::create(2015, 1, 31)->addMonths(1); +``` + +If you want to allow overflow: + +```php +// January 31 + 1 month = March 3 (overflowed) +$date = Chronos::create(2015, 1, 31)->addMonthsWithOverflow(1); +``` + +The same applies to years with `addYearsWithOverflow()` and `subYearsWithOverflow()`. + +## Jump to Boundaries + +You can jump to defined points in time: + +```php +$time = Chronos::now(); + +// Day boundaries +$time->startOfDay(); // 00:00:00 +$time->endOfDay(); // 23:59:59 + +// Month boundaries +$time->startOfMonth(); // First day of month, 00:00:00 +$time->endOfMonth(); // Last day of month, 23:59:59 + +// Year boundaries +$time->startOfYear(); // January 1st, 00:00:00 +$time->endOfYear(); // December 31st, 23:59:59 + +// Week boundaries +$time->startOfWeek(); // Start of week (configurable) +$time->endOfWeek(); // End of week (configurable) + +// Decade boundaries +$time->startOfDecade(); +$time->endOfDecade(); + +// Century boundaries +$time->startOfCentury(); +$time->endOfCentury(); +``` + +## Day of Week Navigation + +Jump to specific days of the week: + +```php +$time = Chronos::now(); + +// Next/previous occurrence of a day +$time->next(Chronos::TUESDAY); +$time->previous(Chronos::MONDAY); + +// First/last of month +$time->firstOfMonth(); // First day of month +$time->firstOfMonth(Chronos::FRIDAY); // First Friday of month +$time->lastOfMonth(Chronos::MONDAY); // Last Monday of month + +// Nth occurrence in month +$time->nthOfMonth(2, Chronos::SATURDAY); // 2nd Saturday of month + +// Quarter-based +$time->firstOfQuarter(); +$time->lastOfQuarter(Chronos::FRIDAY); +$time->nthOfQuarter(3, Chronos::MONDAY); + +// Year-based +$time->firstOfYear(); +$time->lastOfYear(Chronos::SUNDAY); +$time->nthOfYear(10, Chronos::WEDNESDAY); // 10th Wednesday of the year +``` + +## Finding Occurrences + +Find the next or previous occurrence of a specific day at a specific time: + +```php +$now = Chronos::now(); + +// Find next Monday at 9:00 AM +$nextMeeting = $now->nextOccurrenceOf(Chronos::MONDAY, 9, 0, 0); + +// Find previous Friday at 5:00 PM +$lastFriday = $now->previousOccurrenceOf(Chronos::FRIDAY, 17, 0, 0); +``` + +If the current day matches and the time hasn't passed yet, `nextOccurrenceOf()` +returns today. Similarly, `previousOccurrenceOf()` returns today if the time +has already passed. + +## Timezone Modifications + +### Converting Timezone + +`setTimezone()` converts the datetime to the new timezone, adjusting the time: + +```php +$time = new Chronos('2015-12-25 12:00:00', 'UTC'); +$tokyo = $time->setTimezone('Asia/Tokyo'); +// 2015-12-25 21:00:00 Asia/Tokyo (9 hours later) +``` + +### Shifting Timezone + +`shiftTimezone()` keeps the same wall clock time but changes the timezone: + +```php +$time = new Chronos('2015-12-25 12:00:00', 'UTC'); +$tokyo = $time->shiftTimezone('Asia/Tokyo'); +// 2015-12-25 12:00:00 Asia/Tokyo (same time, different zone) +``` + +This is useful when you have a datetime that was stored without timezone +information and you need to assign the correct timezone. + +## DST Considerations + +When modifying dates/times across DST (Daylight Savings Time) transitions, +your operations may gain/lose an additional hour resulting in values that +don't add up. You can avoid these issues by first changing your timezone to +UTC, modifying the time, then converting back: + +```php +// Additional hour gained +$time = new Chronos('2014-03-30 00:00:00', 'Europe/London'); +debug($time->modify('+24 hours')); // 2014-03-31 01:00:00 + +// First switch to UTC, modify, then convert back +$time = $time->setTimezone('UTC') + ->modify('+24 hours') + ->setTimezone('Europe/London'); +``` diff --git a/docs/en/periods-and-intervals.md b/docs/en/periods-and-intervals.md new file mode 100644 index 00000000..4ea8682e --- /dev/null +++ b/docs/en/periods-and-intervals.md @@ -0,0 +1,185 @@ +# Periods and Intervals + +Chronos provides wrapper classes for working with date periods and intervals +that return Chronos instances instead of native PHP DateTime objects. + +## ChronosPeriod + +`ChronosPeriod` wraps a `DatePeriod` and yields `Chronos` instances when iterating: + +```php +use Cake\Chronos\Chronos; +use Cake\Chronos\ChronosPeriod; +use DateInterval; +use DatePeriod; + +// Create a DatePeriod +$start = new Chronos('2024-01-01'); +$end = new Chronos('2024-01-10'); +$interval = new DateInterval('P1D'); // 1 day + +$datePeriod = new DatePeriod($start, $interval, $end); + +// Wrap it with ChronosPeriod +$period = new ChronosPeriod($datePeriod); + +// Iterate - each item is a Chronos instance +foreach ($period as $date) { + echo $date->format('Y-m-d') . "\n"; + // $date is a Chronos instance with all helper methods available +} +``` + +### Safety Check + +ChronosPeriod validates that the interval is not zero, which would cause an +infinite loop: + +```php +// This throws InvalidArgumentException +$period = new ChronosPeriod( + new DatePeriod($start, new DateInterval('PT0S'), $end) +); +``` + +## ChronosDatePeriod + +`ChronosDatePeriod` works the same way but yields `ChronosDate` instances: + +```php +use Cake\Chronos\ChronosDate; +use Cake\Chronos\ChronosDatePeriod; +use DateInterval; +use DatePeriod; + +$start = new Chronos('2024-01-01'); +$end = new Chronos('2024-01-10'); +$interval = new DateInterval('P1D'); + +$datePeriod = new DatePeriod($start, $interval, $end); +$period = new ChronosDatePeriod($datePeriod); + +foreach ($period as $date) { + // $date is a ChronosDate instance + echo $date->format('Y-m-d') . "\n"; +} +``` + +## ChronosInterval + +`ChronosInterval` is a wrapper around `DateInterval` that provides additional +convenience methods including ISO 8601 duration string formatting. + +### Creating Intervals + +```php +use Cake\Chronos\ChronosInterval; + +// From ISO 8601 duration string +$interval = ChronosInterval::create('P1Y2M3D'); +$interval = ChronosInterval::create('PT4H30M'); + +// From individual components +$interval = ChronosInterval::createFromValues( + years: 1, + months: 2, + days: 3, + hours: 4, + minutes: 30, +); + +// From relative date string +$interval = ChronosInterval::createFromDateString('1 year + 2 months'); +$interval = ChronosInterval::createFromDateString('3 days'); + +// From existing DateInterval +$native = new DateInterval('P1D'); +$interval = ChronosInterval::instance($native); +``` + +### Formatting + +```php +$interval = ChronosInterval::createFromValues( + years: 1, + months: 2, + days: 3, + hours: 4, + minutes: 30, + seconds: 15, +); + +// ISO 8601 duration string +echo $interval->toIso8601String(); // "P1Y2M3DT4H30M15S" +echo (string)$interval; // Same as above + +// strtotime-compatible string +echo $interval->toDateString(); // "1 year 2 months 3 days 4 hours 30 minutes 15 seconds" + +// Standard DateInterval format +echo $interval->format('%y years, %m months, %d days'); +``` + +### Calculations + +```php +$interval = ChronosInterval::createFromValues(days: 5, hours: 12); + +// Total calculations (approximate for months/years) +echo $interval->totalDays(); // 5 +echo $interval->totalSeconds(); // 475200 + +// Check properties +$interval->isZero(); // false +$interval->isNegative(); // false +``` + +### Arithmetic + +```php +$interval1 = ChronosInterval::createFromValues(days: 5); +$interval2 = ChronosInterval::createFromValues(days: 3); + +// Add intervals +$sum = $interval1->add($interval2); // 8 days + +// Subtract intervals +$diff = $interval1->sub($interval2); // 2 days +``` + +::: warning +Arithmetic operations perform simple component addition/subtraction without +normalization. For example, 70 minutes stays as 70 minutes rather than being +converted to 1 hour and 10 minutes. +::: + +### Accessing Components + +You can access the underlying DateInterval properties: + +```php +$interval = ChronosInterval::createFromValues(years: 1, months: 6, days: 15); + +echo $interval->y; // 1 +echo $interval->m; // 6 +echo $interval->d; // 15 + +// Get the underlying DateInterval +$native = $interval->toNative(); +``` + +### Creating Intervals with Chronos + +The `Chronos` class also has a helper for creating intervals: + +```php +use Cake\Chronos\Chronos; + +$interval = Chronos::createInterval( + years: 1, + months: 2, + weeks: 1, // Converted to days + days: 3, + hours: 4, +); +``` diff --git a/docs/en/testing.md b/docs/en/testing.md new file mode 100644 index 00000000..1a01c757 --- /dev/null +++ b/docs/en/testing.md @@ -0,0 +1,138 @@ +# Testing Aids + +When writing unit tests, it is helpful to fixate the current time. Chronos provides +several methods to mock the current time for testing. + +## Setting Test Time + +You can fix the current time using `setTestNow()`: + +```php +use Cake\Chronos\Chronos; +use Cake\Chronos\ChronosDate; + +// Fix the current time +Chronos::setTestNow(new Chronos('2015-12-25 00:00:00')); + +// Now all "now" calls return the fixed time +$time = Chronos::now(); // 2015-12-25 00:00:00 +$time = new Chronos(); // 2015-12-25 00:00:00 +$time = new Chronos('1 hour ago'); // 2015-12-24 23:00:00 +``` + +Relative expressions are calculated from the test time: + +```php +Chronos::setTestNow(new Chronos('2015-12-25 00:00:00')); + +$time = new Chronos('+2 days'); // 2015-12-27 00:00:00 +$time = Chronos::parse('yesterday'); // 2015-12-24 00:00:00 +``` + +## Resetting Test Time + +To reset the fixation, call `setTestNow()` with no parameter or `null`: + +```php +Chronos::setTestNow(); +// or +Chronos::setTestNow(null); +``` + +## Checking Test Mode + +You can check if test time is currently set: + +```php +if (Chronos::hasTestNow()) { + // Currently in test mode +} + +// Get the current test time (null if not set) +$testTime = Chronos::getTestNow(); +``` + +## Scoped Test Time + +For cleaner tests, you can use `withTestNow()` which automatically resets the +test time after your callback completes: + +```php +$result = Chronos::withTestNow('2015-12-25 10:00:00', function () { + // Inside this callback, "now" is fixed to 2015-12-25 10:00:00 + $time = Chronos::now(); + + // Do your assertions... + return $time->hour; // 10 +}); + +// After the callback, test time is automatically reset +// $result contains the return value from the callback +``` + +This is especially useful in test methods: + +```php +public function testChristmasDiscount(): void +{ + Chronos::withTestNow('2015-12-25', function () { + $order = new Order(); + $this->assertTrue($order->hasChristmasDiscount()); + }); + + Chronos::withTestNow('2015-07-04', function () { + $order = new Order(); + $this->assertFalse($order->hasChristmasDiscount()); + }); +} +``` + +The callback can also accept the test time as a parameter: + +```php +Chronos::withTestNow('2015-12-25', function (Chronos $now) { + echo $now->format('Y-m-d'); // 2015-12-25 +}); +``` + +## ChronosDate Test Time + +`ChronosDate` has its own separate test time: + +```php +use Cake\Chronos\ChronosDate; + +ChronosDate::setTestNow(ChronosDate::parse('2015-12-25')); + +$today = ChronosDate::today(); // 2015-12-25 + +// Reset +ChronosDate::setTestNow(); +``` + +## Test Suite Setup + +In your test suite's bootstrap process, you might want to set a fixed time +for all tests: + +```php +// In tests/bootstrap.php +Chronos::setTestNow(Chronos::now()); +ChronosDate::setTestNow(ChronosDate::today()); +``` + +Or use the scoped version in individual test methods or setUp/tearDown: + +```php +protected function setUp(): void +{ + parent::setUp(); + Chronos::setTestNow('2015-12-25 12:00:00'); +} + +protected function tearDown(): void +{ + Chronos::setTestNow(); + parent::tearDown(); +} +```