# Carbon (Date/Time)

Carbon is Magic's date and time utility, inspired by PHP's Carbon library, offering a fluent API for creating, formatting, manipulating, and comparing date values with human-readable output.

- [Introduction](#introduction)
- [Creating Carbon Instances](#creating-carbon-instances)
- [Formatting](#formatting)
- [Manipulation](#manipulation)
- [Comparison](#comparison)
- [Human Readable](#human-readable)
- [Short Human Readable](#short-human-readable)
- [Timezone Support](#timezone-support)
- [Testing](#testing)
- [Model Integration](#model-integration)

## Introduction

Magic includes Carbon, a powerful date/time utility inspired by PHP's Carbon library. Carbon provides an expressive, fluent API for creating, formatting, and manipulating dates and times.

```dart
// Create, manipulate, and format dates fluently
Carbon.now().addDays(5).format('MMMM d, yyyy');  // "January 20, 2024"

// Human-readable differences
carbon.diffForHumans();  // "2 days ago"
```

## Creating Carbon Instances

```dart
// Current date/time
final now = Carbon.now();

// Parse from string
final date = Carbon.parse('2024-01-15');
final datetime = Carbon.parse('2024-01-15 14:30:00');
final iso = Carbon.parse('2024-01-15T14:30:00Z');

// From DateTime
final fromDart = Carbon.fromDateTime(DateTime.now());

// Specific dates
final today = Carbon.today();       // Today at 00:00:00
final tomorrow = Carbon.tomorrow();
final yesterday = Carbon.yesterday();
```

## Formatting

```dart
final date = Carbon.now();

// Standard formats
date.format('yyyy-MM-dd');           // "2024-01-15"
date.format('dd/MM/yyyy');           // "15/01/2024"
date.format('MMMM d, yyyy');         // "January 15, 2024"
date.format('EEEE');                 // "Monday"
date.format('h:mm a');               // "2:30 PM"

// ISO 8601
date.toIso8601String();              // "2024-01-15T14:30:00.000Z"

// Convenience methods
date.toDateString();                 // "2024-01-15"
date.toTimeString();                 // "14:30:00"
date.toDateTimeString();             // "2024-01-15 14:30:00"
```

### Format Tokens

| Token | Output | Description |
|-------|--------|-------------|
| `yyyy` | 2024 | 4-digit year |
| `yy` | 24 | 2-digit year |
| `MMMM` | January | Full month name |
| `MMM` | Jan | Abbreviated month |
| `MM` | 01 | 2-digit month |
| `dd` | 15 | 2-digit day |
| `d` | 15 | Day of month |
| `EEEE` | Monday | Full weekday name |
| `EEE` | Mon | Abbreviated weekday |
| `HH` | 14 | 24-hour hour |
| `hh` | 02 | 12-hour hour |
| `mm` | 30 | Minutes |
| `ss` | 00 | Seconds |
| `a` | PM | AM/PM |

## Manipulation

### Adding Time

```dart
final date = Carbon.now();

date.addSeconds(30);
date.addMinutes(15);
date.addHours(2);
date.addDays(5);
date.addWeeks(2);
date.addMonths(3);
date.addYears(1);
```

### Subtracting Time

```dart
date.subSeconds(30);
date.subMinutes(15);
date.subHours(2);
date.subDays(5);
date.subWeeks(2);
date.subMonths(3);
date.subYears(1);
```

### Start/End of Period

```dart
date.startOfDay();     // 00:00:00
date.endOfDay();       // 23:59:59
date.startOfWeek();    // Monday 00:00:00
date.endOfWeek();      // Sunday 23:59:59
date.startOfMonth();   // First day 00:00:00
date.endOfMonth();     // Last day 23:59:59
```

## Comparison

```dart
final date1 = Carbon.parse('2024-01-15');
final date2 = Carbon.parse('2024-01-20');

// Boolean comparisons
date1.isBefore(date2);    // true
date1.isAfter(date2);     // false
date1.isSame(date2);      // false

// Special checks
date.isToday();
date.isTomorrow();
date.isYesterday();
date.isWeekend();
date.isWeekday();
date.isFuture();
date.isPast();

// Between check
date.isBetween(startDate, endDate);
```

### Difference

```dart
final start = Carbon.parse('2024-01-15');
final end = Carbon.parse('2024-01-20');

start.diff(end).inDays;      // 5
start.diff(end).inHours;     // 120
start.diff(end).inMinutes;   // 7200
```

## Human Readable

The `diffForHumans()` method returns a human-readable string:

```dart
Carbon.now().subMinutes(5).diffForHumans();   // "5 minutes ago"
Carbon.now().subHours(2).diffForHumans();     // "2 hours ago"
Carbon.now().subDays(1).diffForHumans();      // "1 day ago"
Carbon.now().subMonths(3).diffForHumans();    // "3 months ago"

// Future dates
Carbon.now().addDays(3).diffForHumans();      // "in 3 days"
Carbon.now().addHours(1).diffForHumans();     // "in 1 hour"
```

### Relative To Another Date

```dart
final date1 = Carbon.parse('2024-01-15');
final date2 = Carbon.parse('2024-01-20');

date1.diffForHumans(date2);  // "5 days before"
date2.diffForHumans(date1);  // "5 days after"
```

## Short Human Readable

`shortDiffForHumans([other])` is a compact ladder (seconds, minutes, hours, days, weeks, months, years, each threshold exclusive of the next) for dense tables and list rows where `diffForHumans()` reads too wide. It measures against Carbon's clock (frozen while `Carbon.setTestNow` is set), or against `other` when given:

```dart
createdAt.shortDiffForHumans();  // "14m ago"
dueAt.shortDiffForHumans();      // "5m from now"
```

A gap under one second reads `"Just now"`; a future instant reads `":time from now"` instead of `":time ago"`. Months are truncated 30-day buckets, so a 45-day gap reads `"1mo ago"`, not `"1 month, 15 days ago"`.

Every unit and wrapper resolves through the `Lang` catalogue when present (`time.units_short.<unit>`, `time.ago`, `time.from_now`, `time.just_now`), falling back to the English literal (`:counts`, `:countm`, `:counth`, `:countd`, `:countw`, `:countmo`, `:county`, `:time ago`, `:time from now`, `Just now`) when no catalogue is loaded or the key is missing:

```json
{
  "time": {
    "units_short": { "minute": ":count dk" },
    "ago": ":time önce"
  }
}
```

```dart
// With the catalogue above loaded for 'tr':
event.shortDiffForHumans();  // "14 dk önce"
```

## Timezone Support

When `localization.auto_detect_timezone` is enabled (it defaults to `false`), Magic
resolves the device's real IANA timezone identifier on boot through the platform's
native APIs, via the `flutter_timezone` package. The resolved identifier is what
`DateManager.instance.timezoneName` returns, and the `LocalizationInterceptor`
sends it as the `X-Timezone` header on every outgoing request. When detection
cannot resolve a valid zone, the configured `localization.timezone` value stays in
place; Magic does not guess a zone from the UTC offset, because an offset does not
identify a zone (Istanbul and Kyiv share a winter offset but differ in DST rules).

Carbon instances do NOT pick this zone up implicitly. A `Carbon` built without an
explicit zone uses the system local time, so the detected identifier is there for
the backend to interpret timestamps with, not a global conversion applied to every
instance. To work in a specific zone, pass it explicitly:

```dart
Carbon.now('Europe/Istanbul');           // build in a zone
Carbon.now().setTimezone('America/New_York'); // convert an existing instance
```

## Testing

`Carbon.setTestNow([testNow])` freezes the clock `Carbon` reads, mirroring Laravel's
`Carbon::setTestNow()`. Call it with no argument (or `null`) to clear the freeze; `Carbon.hasTestNow()`
reports whether one is currently set. The freeze is static state, so clear it in `tearDown` or it leaks
into the next test.

```dart
setUp(() {
  Carbon.setTestNow(Carbon.create(year: 2024, month: 1, day: 15, hour: 10));
});

tearDown(() {
  Carbon.setTestNow(); // never leak a frozen clock into the next test
});

test('a rule that reads "now"', () {
  expect(Carbon.now().toDateString(), '2024-01-15');
});
```

Every read of "now" honours the freeze: `Carbon.now()`, `isToday()`, `isYesterday()`, `isTomorrow()`,
`isFuture()`, `isPast()`, and `diffForHumans()` without an argument. `Carbon.now(timezone)` still converts
the frozen instant into the requested zone. Constructors that take an explicit moment
(`Carbon.parse()`, `Carbon.fromDateTime()`, `Carbon.create()`) are unaffected.

## Model Integration

Carbon integrates seamlessly with Eloquent models through attribute casting:

```dart
class Post extends Model with HasTimestamps {
  @override
  Map<String, String> get casts => const {
    'published_at': 'datetime',
    'expires_at': 'datetime',
  };

  // Typed getter
  Carbon? get publishedAt => getAttribute('published_at') as Carbon?;
  Carbon? get expiresAt => getAttribute('expires_at') as Carbon?;
}
```

### Using in Views

```dart
// Display formatted date
WText(post.publishedAt?.format('MMMM d, yyyy') ?? 'Draft')

// Human-readable
WText(post.publishedAt?.diffForHumans() ?? '')  // "2 days ago"

// Conditional display
if (post.expiresAt?.isFuture() == true) {
  WText('Expires ${post.expiresAt!.diffForHumans()}');
}
```

### HasTimestamps Mixin

Models using `HasTimestamps` automatically get `createdAt` and `updatedAt` as Carbon instances:

```dart
class User extends Model with HasTimestamps {
  // Automatically available:
  // Carbon? get createdAt
  // Carbon? get updatedAt
}

// Usage
WText('Member since ${user.createdAt?.format('MMM yyyy')}');
```

> [!TIP]
> Use `diffForHumans()` in your UI for relative timestamps that are easier for users to understand.
