# Copyright (c) 2023 Yuki Kimoto
# MIT License
class Time::Piece {
version "0.010";
use Sys;
use Time::Seconds;
use Time::Local;
use Sys::Time::Tm;
# Interfaces
interface Cloneable;
# Class Variables
our $DATE_SEP : string;
our $TIME_SEP : string;
our $MON_LIST : string[];
our $FULLMON_LIST : string[];
our $DAY_LIST : string[];
our $FULLDAY_LIST : string[];
our $LOCALE : string[];
# Fields
has is_localtime : byte;
has tm : Sys::Time::Tm;
has epoch : ro long;
INIT {
$DATE_SEP = "-";
$TIME_SEP = ":";
$MON_LIST = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
$FULLMON_LIST = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
$DAY_LIST = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
$FULLDAY_LIST = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
}
# Class Methods
private static method new : Time::Piece ($epoch : long = -1, $is_localtime : int = 0, $allow_minus : int = 0) {
unless ($allow_minus) {
if ($epoch < 0) {
$epoch = Sys->time;
}
}
my $new_tp = new Time::Piece;
my $new_tm = (Sys::Time::Tm)undef;
if ($is_localtime) {
$new_tm = Sys->localtime($epoch);
}
else {
$new_tm = Sys->gmtime($epoch);
}
$new_tp->{tm} = $new_tm;
$new_tp->{is_localtime} = (byte)$is_localtime;
$new_tp->{epoch} = $epoch;
return $new_tp;
}
static method localtime : Time::Piece ($epoch : long = -1, $allow_minus :int = 0) {
my $new_tp = &new($epoch, 1, $allow_minus);
return $new_tp;
}
static method localtime_tp : Time::Piece ($tp : Time::Piece) {
unless ($tp) {
die "\$tp must be defined.";
}
my $new_tp = &new(0, 1);
my $new_tm = Sys->localtime(0);
$new_tm->set_tm_sec($tp->{tm}->tm_sec);
$new_tm->set_tm_min($tp->{tm}->tm_min);
$new_tm->set_tm_hour($tp->{tm}->tm_hour);
$new_tm->set_tm_mday($tp->{tm}->tm_mday);
$new_tm->set_tm_mon($tp->{tm}->tm_mon);
$new_tm->set_tm_year($tp->{tm}->tm_year);
$new_tm->set_tm_wday($tp->{tm}->tm_wday);
$new_tm->set_tm_yday($tp->{tm}->tm_yday);
$new_tm->set_tm_isdst($tp->{tm}->tm_isdst);
$new_tp->{tm} = $new_tm;
$new_tp->{epoch} = Time::Local->timelocal($new_tm);
return $new_tp;
}
static method gmtime : Time::Piece ($epoch : long = -1, $allow_minus : int = 0) {
my $new_tp = &new($epoch, 0, $allow_minus);
return $new_tp;
}
static method gmtime_tp : Time::Piece ($tp : Time::Piece) {
unless ($tp) {
die "\$tp must be defined.";
}
my $new_tp = &new(0, 1);
my $new_tm = Sys->gmtime(0);
$new_tm->set_tm_sec($tp->{tm}->tm_sec);
$new_tm->set_tm_min($tp->{tm}->tm_min);
$new_tm->set_tm_hour($tp->{tm}->tm_hour);
$new_tm->set_tm_mday($tp->{tm}->tm_mday);
$new_tm->set_tm_mon($tp->{tm}->tm_mon);
$new_tm->set_tm_year($tp->{tm}->tm_year);
$new_tm->set_tm_wday($tp->{tm}->tm_wday);
$new_tm->set_tm_yday($tp->{tm}->tm_yday);
$new_tm->set_tm_isdst($tp->{tm}->tm_isdst);
$new_tp->{tm} = $new_tm;
$new_tp->{epoch} = Time::Local->timelocal($new_tm);
return $new_tp;
}
static method strptime : Time::Piece ($string : string, $format : string) {
my $new_tm = &strptime_tm($string, $format);
my $new_tp = &new(0, 0);
$new_tp->{tm} = $new_tm;
my $epoch = Time::Local->timegm($new_tm);
my $wday = &dayofweek($epoch);
$new_tp->{tm}->set_tm_wday($wday);
$new_tp->{epoch} = $epoch;
return $new_tp;
}
private native static method strptime_tm : Sys::Time::Tm ($string : string, $format : string);
static method _is_leap_year : int ($year : int) {
return (($year %4 == 0) && !($year % 100 == 0)) || ($year % 400 == 0);
}
# Instance Methods
method sec : int () {
return $self->{tm}->tm_sec;
}
method second : int () {
return $self->sec;
}
method min : int () {
return $self->{tm}->tm_min;
}
method minute : int () {
return $self->min;
}
method hour : int () {
return $self->{tm}->tm_hour;
}
method mday : int () {
return $self->{tm}->tm_mday;
}
method day_of_month : int () {
return $self->mday;
}
method mon : int () {
return $self->{tm}->tm_mon + 1;
}
method _mon : int () {
return $self->{tm}->tm_mon;
}
method month : string ($mon_list : string[] = undef) {
if ($mon_list) {
return $mon_list->[$self->{tm}->tm_mon];
}
else {
return $MON_LIST->[$self->{tm}->tm_mon];
}
}
method monname : string ($mon_list : string[] = undef) {
return $self->month($mon_list);
}
method fullmonth : string ($mon_list : string[] = undef) {
if ($mon_list) {
return $mon_list->[$self->{tm}->tm_mon];
}
else {
return $FULLMON_LIST->[$self->{tm}->tm_mon];
}
}
method year : int () {
return $self->{tm}->tm_year + 1900;
}
method _year : int () {
return $self->{tm}->tm_year;
}
method yy : int () {
my $res = $self->{tm}->tm_year % 100;
return $res;
}
method wday : int () {
return $self->{tm}->tm_wday + 1;
}
method _wday : int () {
return $self->{tm}->tm_wday;
}
method day_of_week : int () {
return $self->_wday;
}
private static method dayofweek : int ($now : long, $tz_offset : int = 0) {
# Calculate number of seconds since midnight 1 Jan 1970 local time
my $localtime = $now + ($tz_offset * 60 * 60);
# Convert to number of days since 1 Jan 1970
my $days_since_epoch = $localtime / 86400;
# 1 Jan 1970 was a Thursday, so add 4 so Sunday is day 0, and mod 7
my $day_of_week = (int)(($days_since_epoch + 4) % 7);
return $day_of_week;
}
method wdayname : string ($day_list : string[] = undef) {
if ($day_list) {
return $day_list->[$self->{tm}->tm_wday];
}
else {
return $DAY_LIST->[$self->{tm}->tm_wday];
}
}
method day : string ($day_list : string[] = undef) {
return $self->wdayname($day_list);
}
method fullday : string ($day_list : string[] = undef) {
if ($day_list) {
return $day_list->[$self->{tm}->tm_wday];
}
else {
return $FULLDAY_LIST->[$self->{tm}->tm_wday];
}
}
method yday : int () {
return $self->{tm}->tm_yday;
}
method day_of_year : int () {
return $self->yday;
}
method isdst : int () {
return $self->{tm}->tm_isdst;
}
method daylight_savings : int () {
return $self->isdst;
}
method tzoffset : Time::Seconds () {
unless ($self->{is_localtime}) {
return Time::Seconds->new(0);
}
my $epoch = $self->{epoch};
my $jd_localtime = $self->_jd(Sys->localtime($epoch));
my $jd_gmtime = $self->_jd(Sys->gmtime($epoch));
my $tzoffset = 24 * ($jd_localtime - $jd_gmtime);
my $minite_round = 0.5;
if ($tzoffset < 0) {
$minite_round = -$minite_round;
}
my $seconds = (int)($tzoffset * 60 + ($minite_round)) * 60;
return Time::Seconds->new($seconds);
}
method hms : string ($sep : string = undef) {
unless ($sep) {
$sep = $TIME_SEP;
}
return Format->sprintf("%02d$sep%02d$sep%02d", [(object)$self->{tm}->tm_hour, $self->{tm}->tm_min, $self->{tm}->tm_sec]);
}
method time : string ($sep : string = undef) {
return $self->hms($sep);
}
method ymd : string ($sep : string = undef) {
unless ($sep) {
$sep = $DATE_SEP;
}
return Format->sprintf("%d$sep%02d$sep%02d", [(object)$self->year, $self->mon, $self->{tm}->tm_mday]);
}
method date : string ($sep : string = undef) {
return $self->ymd($sep);
}
method mdy : string ($sep : string = undef) {
unless ($sep) {
$sep = $DATE_SEP;
}
return Format->sprintf("%02d$sep%02d$sep%d", [(object)$self->mon, $self->{tm}->tm_mday, $self->year]);
}
method dmy : string ($sep : string = undef) {
unless ($sep) {
$sep = $DATE_SEP;
}
return Format->sprintf("%02d$sep%02d$sep%d", [(object)$self->{tm}->tm_mday, $self->mon, $self->year]);
}
method datetime : string () {
return Fn->join("T", [$self->date($DATE_SEP), $self->time($TIME_SEP)]);
}
method julian_day : double () {
my $tm = Sys->gmtime($self->{epoch});
my $jd = $self->_jd($tm);
return $jd;
}
method mjd : double () {
return (double)$self->julian_day - 2_400_000.5;
}
method _jd : double ($tm : Sys::Time::Tm) {
my $y = $tm->tm_year + 1900;
my $m = $tm->tm_mon + 1;
my $d = $tm->tm_mday;
my $h = $tm->tm_hour;
my $n = $tm->tm_min;
my $s = $tm->tm_sec;
if ($m > 2) {
$m = $m - 3;
}
else {
$y = $y - 1;
$m = $m + 9;
}
my $J = (double)(int)( 365.25 *( $y + 4712) )
+ (double)(int)( (30.6 * $m) + 0.5)
+ 59
+ $d
- 0.5;
my $G = 38 - (double)(int)( 0.75 * (double)(int)(49 + ($y / 100)));
# Calculate the actual Julian Date
my $JD = $J + $G;
my $ret = $JD + ($h + ($n + (double)$s / 60) / 60) / 24;
unless ($ret isa double) {
die "Unexpected Error.";
}
return $ret;
}
method week : int () {
my $J = $self->julian_day;
# Julian day is independent of time zone so add on tzoffset
# if we are using local time here since we want the week day
# to reflect the local time rather than UTC
if ($self->{is_localtime}) {
$J += ((double)$self->tzoffset->{seconds} / (24*3600));
}
# Now that we have the Julian day including fractions
# convert it to an integer Julian Day Number using nearest
# int (since the day changes at midday we convert all Julian
# dates to following midnight).
my $J_int = (int)($J+0.5);
my $d4 = ((($J_int + 31741 - ($J_int % 7)) % 146097) % 36524) % 1461;
my $L = $d4 / 1460;
my $d1 = (($d4 - $L) % 365) + $L;
return $d1 / 7 + 1;
}
method is_leap_year : int () {
my $year = $self->year;
return &_is_leap_year($year);
}
method month_last_day : int () {
my $MON_LAST = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
my $year = $self->year;
my $_mon = $self->_mon;
my $add_leap_year = 0;
if ($_mon == 1) {
$add_leap_year = &_is_leap_year($year);
}
return $MON_LAST->[$_mon] + $add_leap_year;
}
native method strftime : string ($format : string = undef);
method cdate : string () {
return $self->strftime("%a %b %d %H:%M:%S %Y");
}
method add : Time::Piece ($tsec : Time::Seconds) {
return &new($self->{epoch} + (long)$tsec->{seconds}, $self->{is_localtime});
}
method subtract : Time::Piece ($tsec : Time::Seconds) {
return &new($self->{epoch} - (long)$tsec->{seconds}, $self->{is_localtime});
}
method subtract_tp : Time::Seconds ($tp :Time::Piece) {
return Time::Seconds->new($self->{epoch} - $tp->{epoch});
}
method compare : int ($tp : Time::Piece) {
return $self->{epoch} <=> $tp->epoch;
}
method add_months : Time::Piece ($num_months : int) {
my $final_month = $self->_mon + $num_months;
my $num_years = 0;
if ($final_month > 11 || $final_month < 0) {
if ($final_month < 0 && $final_month % 12 == 0) {
$num_years = (int)($final_month / 12) + 1;
}
else {
$num_years = (int)($final_month / 12);
}
if ($final_month < 0) {
$num_years--;
}
$final_month = $final_month % 12;
}
my $new_tp = $self->clone;
$new_tp->{tm}->set_tm_mon($final_month);
$new_tp->{tm}->set_tm_year($self->{tm}->tm_year + $num_years);
return $new_tp;
}
method add_years : Time::Piece ($years : int) {
return $self->add_months($years * 12);
}
method truncate : Time::Piece ($options : object[]) {
Fn->check_option_names($options, ["to"]);
my $options_h = Hash->new($options);
my $to = $options_h->{"to"}->(string);
unless ($to) {
die "The \"to\" option must be defined.";
}
my $tp_truncate = $self->clone;
my $tm = $tp_truncate->{tm};
if ($to eq "second") {
$tm->set_tm_sec(0);
}
elsif ($to eq "minute") {
$tm->set_tm_sec(0);
$tm->set_tm_min(0);
}
elsif ($to eq "hour") {
$tm->set_tm_sec(0);
$tm->set_tm_min(0);
$tm->set_tm_hour(0);
}
elsif ($to eq "day") {
$tm->set_tm_sec(0);
$tm->set_tm_min(0);
$tm->set_tm_hour(0);
$tm->set_tm_mday(1);
}
elsif ($to eq "month") {
$tm->set_tm_sec(0);
$tm->set_tm_min(0);
$tm->set_tm_hour(0);
$tm->set_tm_mday(1);
$tm->set_tm_mon(0);
}
elsif ($to eq "quarter") {
$tm->set_tm_sec(0);
$tm->set_tm_min(0);
$tm->set_tm_hour(0);
$tm->set_tm_mday(1);
$tm->set_tm_mon(($self->_mon / 3) * 3);
}
elsif ($to eq "year") {
$tm->set_tm_sec(0);
$tm->set_tm_min(0);
$tm->set_tm_hour(0);
$tm->set_tm_mday(1);
$tm->set_tm_mon(0);
$tm->set_tm_year($self->_year);
}
else {
die "The value of the \"$to\" option is invalid.";
}
my $epoch = 0L;
if ($tp_truncate->{is_localtime}) {
$epoch = Time::Local->timelocal($tm);
}
else {
$epoch = Time::Local->timegm($tm);
}
$tp_truncate->{epoch} = $epoch;
return $tp_truncate;
}
method clone : Time::Piece () {
my $clone_tp = &new($self->{epoch}, $self->{is_localtime});
return $clone_tp;
}
}