Release v1.4.0 #146

Merged
myrmidex merged 71 commits from release/v1.4.0 into main 2026-08-15 00:36:54 +02:00
2 changed files with 259 additions and 0 deletions
Showing only changes of commit ef5346ed70 - Show all commits

73
app/Support/DateRange.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace App\Support;
use Illuminate\Support\Carbon;
use InvalidArgumentException;
class DateRange
{
public const MAX_DAYS = 731;
public function __construct(
public readonly Carbon $from,
public readonly Carbon $to,
) {
if ($to->lessThan($from)) {
throw new InvalidArgumentException('The end of a date range cannot precede its start.');
}
}
public static function preset(string $preset): self
{
$now = Carbon::now();
return match ($preset) {
'today' => new self($now->copy()->startOfDay(), $now->copy()->endOfDay()),
'week' => new self($now->copy()->startOfWeek(), $now->copy()->endOfWeek()),
'month' => new self($now->copy()->startOfMonth(), $now->copy()->endOfMonth()),
'year' => new self($now->copy()->startOfYear(), $now->copy()->endOfYear()),
'all' => new self(Carbon::createFromTimestamp(0), $now->copy()->endOfDay()),
default => throw new InvalidArgumentException("Unknown date range preset [{$preset}]."),
};
}
/**
* @return array<string, string>
*/
public static function presets(): array
{
return [
'today' => 'Today',
'week' => 'This Week',
'month' => 'This Month',
'year' => 'This Year',
'all' => 'All Time',
];
}
/**
* Every day the range touches, as Y-m-d, so callers can zero-fill empty buckets.
*
* @return array<int, string>
*/
public function days(): array
{
$days = [];
$cursor = $this->from->copy()->startOfDay();
$last = $this->to->copy()->startOfDay();
if ($cursor->diffInDays($last) >= self::MAX_DAYS) {
throw new InvalidArgumentException(
'A range wider than '.self::MAX_DAYS.' days cannot be bucketed by day; bucket by month instead.'
);
}
while ($cursor->lessThanOrEqualTo($last)) {
$days[] = $cursor->toDateString();
$cursor->addDay();
}
return $days;
}
}

View file

@ -0,0 +1,186 @@
<?php
namespace Tests\Unit\Support;
use App\Support\DateRange;
use Illuminate\Support\Carbon;
use InvalidArgumentException;
use Tests\TestCase;
class DateRangeTest extends TestCase
{
public function test_it_exposes_from_and_to(): void
{
$from = Carbon::parse('2026-07-01 00:00:00');
$to = Carbon::parse('2026-07-31 23:59:59');
$range = new DateRange($from, $to);
$this->assertTrue($from->equalTo($range->from));
$this->assertTrue($to->equalTo($range->to));
}
public function test_it_rejects_a_to_earlier_than_from(): void
{
$this->expectException(InvalidArgumentException::class);
new DateRange(
Carbon::parse('2026-07-31 00:00:00'),
Carbon::parse('2026-07-01 00:00:00'),
);
}
public function test_it_allows_a_range_covering_a_single_instant(): void
{
$instant = Carbon::parse('2026-07-01 12:00:00');
$range = new DateRange($instant, $instant);
$this->assertTrue($range->from->equalTo($range->to));
}
public function test_it_builds_today_as_the_current_day(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
$range = DateRange::preset('today');
$this->assertSame('2026-07-15 00:00:00', $range->from->toDateTimeString());
$this->assertSame('2026-07-15 23:59:59', $range->to->toDateTimeString());
}
public function test_it_builds_week_month_and_year_presets(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
$week = DateRange::preset('week');
$this->assertTrue($week->from->equalTo(Carbon::parse('2026-07-15')->startOfWeek()));
$this->assertTrue($week->to->equalTo(Carbon::parse('2026-07-15')->endOfWeek()));
$month = DateRange::preset('month');
$this->assertSame('2026-07-01 00:00:00', $month->from->toDateTimeString());
$this->assertSame('2026-07-31 23:59:59', $month->to->toDateTimeString());
$year = DateRange::preset('year');
$this->assertSame('2026-01-01 00:00:00', $year->from->toDateTimeString());
$this->assertSame('2026-12-31 23:59:59', $year->to->toDateTimeString());
}
public function test_it_builds_all_as_a_bounded_range_ending_now(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
$range = DateRange::preset('all');
$this->assertSame('2026-07-15 23:59:59', $range->to->toDateTimeString());
$this->assertTrue($range->from->lessThan(Carbon::parse('2000-01-01')));
}
public function test_it_rejects_an_unknown_preset(): void
{
$this->expectException(InvalidArgumentException::class);
DateRange::preset('fortnight');
}
public function test_it_lists_the_available_presets(): void
{
$presets = DateRange::presets();
$this->assertSame(
['today', 'week', 'month', 'year', 'all'],
array_keys($presets),
);
$this->assertSame('Today', $presets['today']);
$this->assertSame('All Time', $presets['all']);
}
public function test_it_lists_each_day_in_the_range(): void
{
$range = new DateRange(
Carbon::parse('2026-07-01 09:30:00'),
Carbon::parse('2026-07-04 18:00:00'),
);
$this->assertSame(
['2026-07-01', '2026-07-02', '2026-07-03', '2026-07-04'],
$range->days(),
);
}
public function test_it_lists_a_single_day_for_a_same_day_range(): void
{
$range = new DateRange(
Carbon::parse('2026-07-01 00:00:00'),
Carbon::parse('2026-07-01 23:59:59'),
);
$this->assertSame(['2026-07-01'], $range->days());
}
public function test_it_lists_days_across_a_month_boundary(): void
{
$range = new DateRange(
Carbon::parse('2026-01-30 00:00:00'),
Carbon::parse('2026-02-02 23:59:59'),
);
$this->assertSame(
['2026-01-30', '2026-01-31', '2026-02-01', '2026-02-02'],
$range->days(),
);
}
public function test_it_lists_days_across_a_year_boundary(): void
{
$range = new DateRange(
Carbon::parse('2025-12-30 00:00:00'),
Carbon::parse('2026-01-02 23:59:59'),
);
$this->assertSame(
['2025-12-30', '2025-12-31', '2026-01-01', '2026-01-02'],
$range->days(),
);
}
public function test_it_lists_every_day_of_a_leap_february(): void
{
$range = new DateRange(
Carbon::parse('2028-02-27 00:00:00'),
Carbon::parse('2028-03-01 23:59:59'),
);
$this->assertSame(
['2028-02-27', '2028-02-28', '2028-02-29', '2028-03-01'],
$range->days(),
);
}
public function test_it_lists_days_up_to_the_maximum_span(): void
{
$from = Carbon::parse('2026-01-01 00:00:00');
$range = new DateRange($from, $from->copy()->addDays(DateRange::MAX_DAYS - 1)->endOfDay());
$this->assertCount(DateRange::MAX_DAYS, $range->days());
}
public function test_it_refuses_to_list_days_for_a_range_wider_than_the_maximum(): void
{
$from = Carbon::parse('2026-01-01 00:00:00');
$range = new DateRange($from, $from->copy()->addDays(DateRange::MAX_DAYS)->endOfDay());
$this->expectException(InvalidArgumentException::class);
$range->days();
}
public function test_it_refuses_to_list_days_for_the_all_preset(): void
{
Carbon::setTestNow('2026-07-15 13:45:00');
$this->expectException(InvalidArgumentException::class);
DateRange::preset('all')->days();
}
}