🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
499 lines
18 KiB
Plaintext
Executable File
499 lines
18 KiB
Plaintext
Executable File
TIME(7) RSpade Framework Manual TIME(7)
|
|
|
|
NAME
|
|
Rsx_Time, Rsx_Date - Date and datetime handling for RSpade applications
|
|
|
|
SYNOPSIS
|
|
PHP Datetime:
|
|
use App\RSpade\Core\Time\Rsx_Time;
|
|
|
|
$now = Rsx_Time::now();
|
|
$iso = Rsx_Time::to_iso($datetime);
|
|
$localized = Rsx_Time::to_user_timezone($datetime);
|
|
$formatted = Rsx_Time::format_datetime_with_tz($datetime);
|
|
$relative = Rsx_Time::relative($datetime);
|
|
|
|
PHP Date:
|
|
use App\RSpade\Core\Time\Rsx_Date;
|
|
|
|
$today = Rsx_Date::today();
|
|
$formatted = Rsx_Date::format($date);
|
|
$relative = Rsx_Date::relative($date);
|
|
$is_past = Rsx_Date::is_past($date);
|
|
$next_week = Rsx_Date::add_days($date, 7);
|
|
$monday = Rsx_Date::start_of_week($date);
|
|
$month_name = Rsx_Date::month_human($date);
|
|
|
|
JavaScript Datetime:
|
|
const now = Rsx_Time.now();
|
|
const iso = Rsx_Time.to_iso(datetime);
|
|
const formatted = Rsx_Time.format_datetime_with_tz(datetime);
|
|
const relative = Rsx_Time.relative(datetime);
|
|
|
|
JavaScript Date:
|
|
const today = Rsx_Date.today();
|
|
const formatted = Rsx_Date.format(date);
|
|
const relative = Rsx_Date.relative(date);
|
|
const is_past = Rsx_Date.is_past(date);
|
|
const next_week = Rsx_Date.add_days(date, 7);
|
|
const monday = Rsx_Date.start_of_week(date);
|
|
|
|
DESCRIPTION
|
|
RSpade provides two separate classes for handling temporal values:
|
|
|
|
Rsx_Time - Datetimes (moments in time)
|
|
Represents specific moments. Always has time component. Timezone-aware.
|
|
Stored in UTC, displayed in user's timezone.
|
|
Format: ISO 8601 "2024-12-24T15:30:45.123Z"
|
|
|
|
Rsx_Date - Dates (calendar dates)
|
|
Represents calendar dates without time. Timezone-agnostic.
|
|
"December 24, 2025" is the same day everywhere.
|
|
Format: Always "YYYY-MM-DD" (e.g., "2024-12-24")
|
|
|
|
STRINGS, NOT OBJECTS
|
|
All external-facing APIs return STRINGS, not Carbon or Date objects:
|
|
|
|
$model->created_at // "2024-12-24T15:30:45.123Z" (string)
|
|
$model->due_date // "2024-12-24" (string)
|
|
Rsx_Time::now_iso() // "2024-12-24T15:30:45.123Z" (string)
|
|
Rsx_Time::to_iso($x) // "2024-12-24T15:30:45.123Z" (string)
|
|
Rsx_Time::add($x, 60) // "2024-12-24T15:31:45.123Z" (string)
|
|
|
|
This design keeps PHP and JavaScript in sync - identical string formats
|
|
on both sides, no serialization surprises.
|
|
|
|
Carbon is used internally for calculations, but never exposed externally.
|
|
The only method returning Carbon is parse(), for internal calculations:
|
|
|
|
$carbon = Rsx_Time::parse($time); // Carbon for calculations
|
|
$result = Rsx_Time::to_iso($carbon); // Back to string for output
|
|
|
|
CRITICAL: Type Separation
|
|
Date functions THROW if passed a datetime.
|
|
Datetime functions THROW if passed a date-only string.
|
|
|
|
This is intentional. Mixing dates and datetimes causes bugs:
|
|
- "2024-12-24" as datetime would become midnight UTC, wrong in other timezones
|
|
- "2024-12-24T00:00:00Z" as date loses the time information
|
|
|
|
Examples of errors:
|
|
Rsx_Time::parse('2024-12-24')
|
|
// THROWS: "Use Rsx_Date::parse() for dates without time components"
|
|
|
|
Rsx_Date::parse('2024-12-24T15:30:00Z')
|
|
// THROWS: "Use Rsx_Time::parse() for datetimes with time components"
|
|
|
|
DATE VS DATETIME
|
|
Use DATE when:
|
|
- Due dates (the task is due on this calendar day)
|
|
- Birth dates (born on this day, no time)
|
|
- Anniversaries, holidays
|
|
- Any value where time of day is irrelevant
|
|
|
|
Use DATETIME when:
|
|
- Event start/end times
|
|
- Created/updated timestamps
|
|
- Scheduled appointments
|
|
- Any value where the exact moment matters
|
|
|
|
Database columns:
|
|
DATE - For date-only fields
|
|
DATETIME(3) - For datetime fields (millisecond precision)
|
|
|
|
RSX_DATE CLASS
|
|
All functions work with "YYYY-MM-DD" format strings.
|
|
|
|
Parsing & Validation
|
|
parse($input)
|
|
Returns "YYYY-MM-DD" string or null.
|
|
THROWS on datetime input.
|
|
|
|
is_date($input)
|
|
Returns true if input is valid date string.
|
|
|
|
Current Date
|
|
today()
|
|
Returns today's date as "YYYY-MM-DD" in user's timezone.
|
|
PHP: $today = Rsx_Date::today();
|
|
JS: const today = Rsx_Date.today();
|
|
|
|
now()
|
|
Alias for today(). Provided for consistency with Rsx_Time::now().
|
|
|
|
Formatting
|
|
format($date)
|
|
Display format: "Dec 24, 2025"
|
|
|
|
format_iso($date)
|
|
Ensures "YYYY-MM-DD" format.
|
|
|
|
relative($date)
|
|
Human-readable relative date.
|
|
"Today", "Yesterday", "Tomorrow", "3 days ago", "in 5 days"
|
|
|
|
Comparison
|
|
is_today($date) True if date is today
|
|
is_past($date) True if date is before today
|
|
is_future($date) True if date is after today
|
|
diff_days($d1, $d2) Days between dates (positive if d2 > d1)
|
|
|
|
Arithmetic
|
|
add_days($date, $days)
|
|
Add (or subtract with negative) days from date.
|
|
Returns "YYYY-MM-DD" or null if invalid input.
|
|
|
|
Week/Month Boundaries
|
|
start_of_week($date) Monday of the week containing date
|
|
end_of_week($date) Sunday of the week containing date
|
|
start_of_month($date) First day of month (YYYY-MM-01)
|
|
end_of_month($date) Last day of month
|
|
|
|
is_weekend($date) True if Saturday or Sunday
|
|
is_weekday($date) True if Monday-Friday
|
|
|
|
Component Extractors
|
|
All extractors return null for invalid input.
|
|
|
|
day($date) Day of month (1-31)
|
|
dow($date) Day of week integer (0=Sunday, 6=Saturday)
|
|
dow_human($date) Full day name ("Monday", "Tuesday", etc.)
|
|
dow_short($date) Short day name ("Mon", "Tue", etc.)
|
|
month($date) Month number (1-12)
|
|
month_human($date) Full month name ("January", "February")
|
|
month_human_short($date) Short month name ("Jan", "Feb")
|
|
year($date) Four-digit year (e.g., 2025)
|
|
|
|
Database
|
|
to_database($date)
|
|
Returns "YYYY-MM-DD" for database storage (same as ISO format).
|
|
|
|
RSX_TIME CLASS
|
|
All functions accept ISO 8601 datetime strings or Carbon/Date objects.
|
|
All functions RETURN strings (except parse, which returns Carbon for
|
|
internal calculations).
|
|
|
|
Parsing & Validation
|
|
parse($input)
|
|
Returns Carbon (PHP) or Date (JS) in UTC.
|
|
Used for internal calculations, not for output.
|
|
THROWS on date-only string input.
|
|
|
|
is_datetime($input)
|
|
Returns true if input is valid datetime (not date-only).
|
|
|
|
Current Time
|
|
now() Returns current time as Carbon/Date (for calculations)
|
|
now_iso() Returns current time as ISO 8601 string (preferred)
|
|
now_ms() Returns current time as Unix milliseconds
|
|
|
|
Timezone Handling
|
|
get_user_timezone()
|
|
Returns user's IANA timezone (e.g., "America/Chicago").
|
|
|
|
to_timezone($time, $tz)
|
|
Convert datetime to specific timezone.
|
|
|
|
to_user_timezone($time)
|
|
Convert datetime to user's timezone.
|
|
|
|
get_timezone_abbr($time, $tz)
|
|
Get timezone abbreviation (e.g., "CST", "CDT").
|
|
DST-aware based on the actual date.
|
|
|
|
Serialization
|
|
to_iso($time)
|
|
Returns ISO 8601 UTC string: "2024-12-24T15:30:45.123Z"
|
|
|
|
to_ms($time)
|
|
Returns Unix timestamp in milliseconds.
|
|
|
|
to_database($time) (PHP only)
|
|
Returns MySQL format: "2024-12-24 15:30:45.123"
|
|
|
|
Formatting
|
|
format_time($time, $tz) "3:30 PM"
|
|
format_datetime($time, $tz) "Dec 24, 2024, 3:30 PM"
|
|
format_datetime_with_tz($time) "Dec 24, 2024, 3:30 PM CST"
|
|
format($time, $format, $tz) (PHP only) Custom PHP date format
|
|
|
|
Duration & Relative
|
|
diff_seconds($start, $end)
|
|
Seconds between two datetimes.
|
|
|
|
seconds_until($time)
|
|
Seconds until future time (negative if in past).
|
|
|
|
seconds_since($time)
|
|
Seconds since past time (negative if in future).
|
|
|
|
duration_to_human($seconds, $short)
|
|
Long: "2 hours and 30 minutes"
|
|
Short: "2h 30m"
|
|
|
|
relative($time)
|
|
"2 hours ago", "in 3 days", "just now"
|
|
|
|
Arithmetic
|
|
add($time, $seconds)
|
|
Add seconds to time. Returns ISO 8601 string.
|
|
|
|
subtract($time, $seconds)
|
|
Subtract seconds from time. Returns ISO 8601 string.
|
|
|
|
Comparison
|
|
is_past($time) True if datetime is in the past
|
|
is_future($time) True if datetime is in the future
|
|
is_today($time) True if datetime is today (in user's timezone)
|
|
|
|
Component Extractors
|
|
Extract date/time components from a datetime. All require timezone
|
|
parameter ($tz) or use user's timezone if omitted.
|
|
Return null for invalid input.
|
|
|
|
day($time, $tz) Day of month (1-31)
|
|
dow($time, $tz) Day of week integer (0=Sunday, 6=Saturday)
|
|
dow_human($time, $tz) Full day name ("Monday", "Tuesday", etc.)
|
|
dow_short($time, $tz) Short day name ("Mon", "Tue", etc.)
|
|
month($time, $tz) Month number (1-12)
|
|
month_human($time, $tz) Full month name ("January", "February")
|
|
month_human_short($time, $tz) Short month name ("Jan", "Feb")
|
|
year($time, $tz) Four-digit year (e.g., 2025)
|
|
hour($time, $tz) Hour (0-23)
|
|
minute($time, $tz) Minute (0-59)
|
|
|
|
Live Updates (JavaScript only)
|
|
countdown($element, target_time, options)
|
|
Live countdown to future time. Updates every second.
|
|
|
|
const ctrl = Rsx_Time.countdown($('#timer'), deadline, {
|
|
short: true,
|
|
on_complete: () => alert('Done!')
|
|
});
|
|
ctrl.stop(); // Stop the countdown
|
|
|
|
countup($element, start_time, options)
|
|
Live elapsed time since past time.
|
|
|
|
Rsx_Time.countup($('.elapsed'), started_at, { short: true });
|
|
|
|
TIMEZONE INITIALIZATION
|
|
User timezone is resolved in order:
|
|
1. login_users.timezone (user's preference)
|
|
2. sites.timezone (site's default timezone)
|
|
3. config('rsx.datetime.default_timezone')
|
|
4. 'America/Chicago' (hardcoded fallback)
|
|
|
|
Timezone is cached per-request and auto-invalidates when user or site
|
|
changes (e.g., after login/logout or site switch).
|
|
|
|
Page Load
|
|
On page load, window.rsxapp includes:
|
|
server_time - ISO 8601 UTC timestamp from server
|
|
user_timezone - IANA timezone identifier
|
|
|
|
Rsx_Time._on_framework_core_init() reads these automatically.
|
|
|
|
AJAX Sync
|
|
Every AJAX response includes _server_time and _user_timezone.
|
|
Rsx_Time.sync_from_ajax() is called automatically to:
|
|
- Update user timezone if changed
|
|
- Sync server time offset on first request or timezone change
|
|
|
|
This corrects for client clock skew. Rsx_Time.now() returns
|
|
server-adjusted time.
|
|
|
|
COMPONENT EXPECTATIONS
|
|
Date Picker Components
|
|
val() returns "YYYY-MM-DD" or null
|
|
val(value) accepts "YYYY-MM-DD" or null
|
|
THROWS if passed datetime format
|
|
Internal display shows localized format (e.g., "Dec 24, 2025")
|
|
|
|
class Date_Picker extends Form_Input_Abstract {
|
|
val(value) {
|
|
if (arguments.length === 0) {
|
|
return this.state.value; // "YYYY-MM-DD" or null
|
|
}
|
|
if (value != null && !Rsx_Date.is_date(value)) {
|
|
throw new Error('Date_Picker requires YYYY-MM-DD format');
|
|
}
|
|
this.state.value = value;
|
|
this._update_display();
|
|
}
|
|
}
|
|
|
|
Datetime Picker Components
|
|
val() returns ISO 8601 string or null
|
|
val(value) accepts ISO 8601 string or null
|
|
THROWS if passed date-only format
|
|
Internal display shows localized time in user's timezone
|
|
|
|
class Datetime_Picker extends Form_Input_Abstract {
|
|
val(value) {
|
|
if (arguments.length === 0) {
|
|
return this.state.value; // ISO 8601 or null
|
|
}
|
|
if (value != null && !Rsx_Time.is_datetime(value)) {
|
|
throw new Error('Datetime_Picker requires ISO 8601 format');
|
|
}
|
|
this.state.value = value;
|
|
this._update_display();
|
|
}
|
|
}
|
|
|
|
DATA FLOW
|
|
Date Field (e.g., due_date)
|
|
|
|
Database: DATE column, value "2025-12-24"
|
|
|
|
|
PHP Model: $task->due_date = "2025-12-24" (string)
|
|
| (Rsx_Date_Cast returns YYYY-MM-DD strings, NOT Carbon)
|
|
|
|
|
JSON Response: {"due_date": "2025-12-24"}
|
|
|
|
|
JS Model: task.due_date = "2025-12-24" (string)
|
|
|
|
|
Date Picker: val() = "2025-12-24", display "Dec 24, 2025"
|
|
|
|
|
Form Submit: {"due_date": "2025-12-24"}
|
|
|
|
|
PHP Controller: Rsx_Date::parse($params['due_date'])
|
|
|
|
|
Database: "2025-12-24"
|
|
|
|
Datetime Field (e.g., scheduled_at)
|
|
|
|
Database: DATETIME(3), value "2025-12-24 15:30:45.123" (UTC)
|
|
|
|
|
PHP Model: $event->scheduled_at = "2025-12-24T15:30:45.123Z" (string)
|
|
| (Rsx_DateTime_Cast converts MySQL format to ISO 8601)
|
|
|
|
|
JSON Serialize: {"scheduled_at": "2025-12-24T15:30:45.123Z"}
|
|
|
|
|
JS Model: event.scheduled_at = "2025-12-24T15:30:45.123Z" (string)
|
|
|
|
|
Datetime Picker: val() = ISO string, display "Dec 24, 9:30 AM CST"
|
|
|
|
|
User edits to "Dec 24, 10:00 AM CST"
|
|
Picker converts to UTC: "2025-12-24T16:00:00.000Z"
|
|
|
|
|
Form Submit: {"scheduled_at": "2025-12-24T16:00:00.000Z"}
|
|
|
|
|
PHP Controller: Rsx_Time::parse($params['scheduled_at']) -> Carbon
|
|
| (only if calculations needed; can also save string directly)
|
|
|
|
|
Database: "2025-12-24 16:00:00.000"
|
|
|
|
EXAMPLES
|
|
PHP - Date handling:
|
|
$due_date = Rsx_Date::parse($params['due_date']); // "2025-12-24"
|
|
|
|
if (Rsx_Date::is_past($due_date)) {
|
|
return response_error(Ajax::ERROR_VALIDATION, [
|
|
'due_date' => 'Due date cannot be in the past'
|
|
]);
|
|
}
|
|
|
|
$task->due_date = Rsx_Date::to_database($due_date);
|
|
$task->save();
|
|
|
|
PHP - Datetime handling:
|
|
$event_time = Rsx_Time::parse($params['event_time']);
|
|
|
|
return [
|
|
'id' => $event->id,
|
|
'event_time' => Rsx_Time::to_iso($event_time),
|
|
'formatted' => Rsx_Time::format_datetime_with_tz($event_time),
|
|
'is_past' => Rsx_Time::is_past($event_time),
|
|
];
|
|
|
|
JavaScript - Date display:
|
|
const due = this.data.task.due_date; // "2025-12-24"
|
|
this.$sid('due').text(Rsx_Date.format(due)); // "Dec 24, 2025"
|
|
|
|
if (Rsx_Date.is_past(due)) {
|
|
this.$sid('due').addClass('text-danger');
|
|
}
|
|
|
|
JavaScript - Datetime display with countdown:
|
|
const event_time = this.data.event.scheduled_at; // ISO string
|
|
|
|
this.$sid('time').text(Rsx_Time.format_datetime(event_time));
|
|
this.$sid('relative').text(Rsx_Time.relative(event_time));
|
|
|
|
if (Rsx_Time.is_future(event_time)) {
|
|
this._countdown = Rsx_Time.countdown(
|
|
this.$sid('countdown'),
|
|
event_time,
|
|
{ short: true, on_complete: () => this.reload() }
|
|
);
|
|
}
|
|
|
|
ERROR HANDLING
|
|
Wrong type errors are thrown immediately:
|
|
|
|
// PHP
|
|
try {
|
|
Rsx_Time::parse('2025-12-24');
|
|
} catch (\InvalidArgumentException $e) {
|
|
// "Rsx_Time::parse() received date-only string..."
|
|
}
|
|
|
|
// JavaScript
|
|
try {
|
|
Rsx_Time.parse('2025-12-24');
|
|
} catch (e) {
|
|
// "Rsx_Time.parse() received date-only string..."
|
|
}
|
|
|
|
These errors indicate a programming mistake - the wrong function is
|
|
being used. Fix the code rather than catching the exception.
|
|
|
|
CONFIGURATION
|
|
system/config/rsx.php:
|
|
'datetime' => [
|
|
'default_timezone' => env('RSX_DEFAULT_TIMEZONE', 'America/Chicago'),
|
|
],
|
|
|
|
User timezone stored in login_users.timezone column.
|
|
Site timezone stored in sites.timezone column (default: 'America/Chicago').
|
|
|
|
MODEL CASTING
|
|
Model date/datetime columns are automatically cast to strings via custom
|
|
Eloquent casts. No configuration needed - columns are detected from the
|
|
database schema.
|
|
|
|
Rsx_DateTime_Cast (for DATETIME/TIMESTAMP columns)
|
|
- Database read: "2024-12-24 15:30:45" -> "2024-12-24T15:30:45.000Z"
|
|
- Database write: "2024-12-24T15:30:45.000Z" -> "2024-12-24 15:30:45"
|
|
- Accepts: ISO 8601 strings, MySQL datetime strings, Unix timestamps
|
|
- Rejects: Carbon objects (use to_iso() to convert first)
|
|
|
|
Rsx_Date_Cast (for DATE columns)
|
|
- Database read: "2024-12-24" -> "2024-12-24"
|
|
- Database write: "2024-12-24" -> "2024-12-24"
|
|
- Accepts: YYYY-MM-DD strings only
|
|
- Rejects: Carbon objects, datetime strings
|
|
|
|
Why strings instead of Carbon?
|
|
1. Prevents timezone bugs - dates have no timezone, Carbon assumes one
|
|
2. PHP/JS parity - identical formats on both sides
|
|
3. No serialization surprises - what you see is what you get
|
|
4. Simpler mental model - just strings everywhere
|
|
|
|
Overriding casts:
|
|
If a model needs different behavior, override $casts in the model:
|
|
|
|
protected $casts = [
|
|
'special_date' => 'date', // Use Laravel's default
|
|
];
|
|
|
|
SEE ALSO
|
|
Rsx_Time, Rsx_Date, Rsx_DateTime_Cast, Rsx_Date_Cast
|
|
|
|
AUTHOR
|
|
RSpade Framework
|
|
|
|
RSpade January 2026 TIME(7)
|