mruby-time: add descriptive comments

The comments were written by Google Jules.
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-06-02 16:04:54 +09:00
parent 91c59ced99
commit 70956e9683
+138 -55
View File
@@ -131,6 +131,12 @@ struct timeval {
};
# endif
/*
* Polyfill for gettimeofday on Windows platforms that may not have it (e.g., older MSVC).
* Retrieves the current system time as FILETIME, converts it to Unix epoch,
* and then splits it into seconds and microseconds.
* The timezone argument (tz) is not supported.
*/
static int
gettimeofday(struct timeval *tv, void *tz)
{
@@ -215,23 +221,23 @@ timegm(struct tm *tm)
*/
#ifndef MRB_NO_STDIO
static const char mon_names[12][4] = {
static const char mon_names[MONTHS_PER_YEAR][4] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
};
static const char wday_names[7][4] = {
static const char wday_names[7][4] = { /* Consider defining DAYS_PER_WEEK = 7 if used elsewhere */
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
};
#endif
struct mrb_time {
time_t sec;
time_t usec;
enum mrb_timezone timezone;
struct tm datetime;
time_t sec; /* Seconds since the Epoch */
time_t usec; /* Microsecond fraction of the second */
enum mrb_timezone timezone; /* Timezone setting (MRB_TIMEZONE_UTC or MRB_TIMEZONE_LOCAL) */
struct tm datetime; /* Cache for broken-down time based on sec, usec, and timezone. Updated by time_update_datetime. */
};
static const struct mrb_data_type time_type = { "Time", mrb_free };
static const struct mrb_data_type time_type = { "Time", mrb_free }; /* mrb_free is the standard C free() */
#define MRB_TIME_T_UINT (~(time_t)0 > 0)
#define MRB_TIME_MIN ( \
@@ -243,7 +249,13 @@ static const struct mrb_data_type time_type = { "Time", mrb_free };
(sizeof(time_t) <= 4 ? INT32_MAX : INT64_MAX) \
)
/* return true if time_t is fit in mrb_int */
/*
* Checks if a time_t value `v` can be represented as an mrb_int without overflow or precision loss.
* This is important because mruby integers (mrb_int) might be smaller than time_t on some platforms.
* - If mrb_int can fully encompass the range of time_t, it's always TRUE.
* - Otherwise, it checks if `v` falls within the representable range of mrb_int.
* - Considers if time_t is unsigned (MRB_TIME_T_UINT).
*/
static mrb_bool
fixable_time_t_p(time_t v)
{
@@ -334,6 +346,13 @@ mrb_to_time_t(mrb_state *mrb, mrb_value obj, time_t *usec)
return t;
}
/*
* Converts a time_t value `t` into an appropriate mruby numeric value.
* - If `t` fits in mrb_int (checked by fixable_time_t_p), returns an mrb_int_value.
* - Otherwise, if MRB_USE_BIGINT is defined, returns a BigInt.
* - Otherwise, if MRB_NO_FLOAT is not defined, returns a Float.
* - Otherwise, raises an ArgumentError if the time value is too large to represent.
*/
static mrb_value
time_value_from_time_t(mrb_state *mrb, time_t t)
{
@@ -376,7 +395,13 @@ time_update_datetime(mrb_state *mrb, struct mrb_time *self, int dealloc)
return NULL;
}
#ifdef NO_GMTIME_R
self->datetime = *aid; /* copy data */
/*
* If reentrant gmtime_r/localtime_r are not available (NO_GMTIME_R is defined),
* standard gmtime/localtime are used. These functions often return a pointer
* to a static internal buffer. To avoid this buffer being overwritten by subsequent
* calls, the data pointed to by `aid` must be copied into `self->datetime`.
*/
self->datetime = *aid; /* copy data from static buffer */
#endif
return self;
@@ -392,30 +417,46 @@ time_wrap(mrb_state *mrb, struct RClass *tc, struct mrb_time *tm)
static struct mrb_time*
time_alloc_time(mrb_state *mrb, time_t sec, time_t usec, enum mrb_timezone timezone)
{
struct mrb_time *tm = (struct mrb_time*)mrb_malloc(mrb, sizeof(struct mrb_time));
tm->sec = sec;
tm->usec = usec;
struct mrb_time *time_obj = (struct mrb_time*)mrb_malloc(mrb, sizeof(struct mrb_time));
time_obj->sec = sec;
time_obj->usec = usec;
if (!MRB_TIME_T_UINT && tm->usec < 0) {
long sec2 = (long)NDIV(tm->usec,1000000); /* negative div */
tm->usec -= sec2 * 1000000;
tm->sec += sec2;
/* Normalize seconds and microseconds. */
/* This is only necessary if time_t is signed and usec is negative. */
if (!MRB_TIME_T_UINT && time_obj->usec < 0) {
/*
* If usec is negative, adjust seconds downwards.
* NDIV calculates division rounded towards negative infinity.
* For example, NDIV(-1, USECS_PER_SEC) is -1, so 1 second is subtracted.
* NDIV(-1000001, USECS_PER_SEC) is -2, so 2 seconds are subtracted.
*/
long sec_adjustment = (long)NDIV(time_obj->usec, USECS_PER_SEC);
time_obj->usec -= sec_adjustment * USECS_PER_SEC; /* Becomes positive or zero */
time_obj->sec += sec_adjustment;
}
else if (tm->usec >= 1000000) {
long sec2 = (long)(tm->usec / 1000000);
tm->usec -= sec2 * 1000000;
tm->sec += sec2;
/* Handle positive microsecond overflow. */
else if (time_obj->usec >= USECS_PER_SEC) {
/* If usec is USECS_PER_SEC or more, adjust seconds upwards. */
long sec_adjustment = (long)(time_obj->usec / USECS_PER_SEC);
time_obj->usec -= sec_adjustment * USECS_PER_SEC; /* Reduce to < USECS_PER_SEC */
time_obj->sec += sec_adjustment;
}
tm->timezone = timezone;
time_update_datetime(mrb, tm, TRUE);
time_obj->timezone = timezone;
/* Update the datetime struct; this also handles potential deallocation on error. */
time_update_datetime(mrb, time_obj, TRUE);
return tm;
return time_obj;
}
/*
* Allocates and initializes an mrb_time structure from mruby values for seconds and microseconds.
* It first converts the mruby values to time_t using mrb_to_time_t,
* then calls time_alloc_time to perform the actual allocation and normalization.
*/
static struct mrb_time*
time_alloc(mrb_state *mrb, mrb_value sec, mrb_value usec, enum mrb_timezone timezone)
{
time_t tsec, tusec;
time_t tsec, tusec; /* Variables to hold converted seconds and microseconds */
tsec = mrb_to_time_t(mrb, sec, &tusec);
tusec += mrb_to_time_t(mrb, usec, NULL);
@@ -423,22 +464,39 @@ time_alloc(mrb_state *mrb, mrb_value sec, mrb_value usec, enum mrb_timezone time
return time_alloc_time(mrb, tsec, tusec, timezone);
}
/*
* Creates a new Time object from C-native time_t seconds and microseconds.
* This is a lower-level constructor compared to time_make.
*/
static mrb_value
time_make_time(mrb_state *mrb, struct RClass *c, time_t sec, time_t usec, enum mrb_timezone timezone)
{
return time_wrap(mrb, c, time_alloc_time(mrb, sec, usec, timezone));
}
/*
* Creates a new Time object from mruby values representing seconds and microseconds.
* This is a higher-level constructor that handles mruby type conversions.
*/
static mrb_value
time_make(mrb_state *mrb, struct RClass *c, mrb_value sec, mrb_value usec, enum mrb_timezone timezone)
{
return time_wrap(mrb, c, time_alloc(mrb, sec, usec, timezone));
}
/*
* Retrieves the current system time and creates a new mrb_time object.
* It uses different strategies based on platform capabilities:
* 1. timespec_get (C11 standard, if TIME_UTC is defined)
* 2. clock_gettime (POSIX standard, if USE_CLOCK_GETTIME is defined)
* 3. gettimeofday (Commonly available POSIX function, or our polyfill on Windows)
* 4. time(NULL) (Standard C, second precision only; microseconds are faked if called rapidly)
* The new Time object is initialized to the local timezone.
*/
static struct mrb_time*
current_mrb_time(mrb_state *mrb)
{
struct mrb_time tmzero = {0};
struct mrb_time tmzero = {0}; /* Used to initialize the new mrb_time struct */
time_t sec, usec;
#if defined(TIME_UTC) && !defined(__ANDROID__)
@@ -446,26 +504,29 @@ current_mrb_time(mrb_state *mrb)
struct timespec ts;
timespec_get(&ts, TIME_UTC);
sec = ts.tv_sec;
usec = ts.tv_nsec / 1000;
usec = ts.tv_nsec / NSECS_PER_USEC;
}
#elif defined(USE_CLOCK_GETTIME)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
sec = ts.tv_sec;
usec = ts.tv_nsec / 1000;
usec = ts.tv_nsec / NSECS_PER_USEC;
}
#elif defined(NO_GETTIMEOFDAY)
{
static time_t last_sec = 0, last_usec = 0;
sec = time(NULL);
if (sec != last_sec) {
if (sec != last_sec) { /* Time has advanced by at least one second */
last_sec = sec;
last_usec = 0;
}
else {
/* add 1 usec to differentiate two times */
else { /* Called multiple times within the same second */
/* Add 1 usec to differentiate two Time objects created in rapid succession.
* This is a simple way to ensure distinctness when second-level precision is the best available.
* Note: This might lead to microsecond values that don't reflect actual time but ensure uniqueness.
*/
last_usec += 1;
}
usec = last_usec;
@@ -562,13 +623,21 @@ time_mktime(mrb_state *mrb, mrb_int ayear, mrb_int amonth, mrb_int aday,
}
time_t nowsecs = (*mk)(&nowtime);
/*
* Handle mktime/timegm failure:
* If mk() returns -1, it usually indicates an error or an out-of-range date.
* A special case is when the time is exactly one second before the epoch (Epoch-1).
* Some mktime implementations might return -1 for this valid time.
* The code tries to detect this by adding one second to tm_sec and calling mk() again.
* If the result is 0 (Epoch), then the original time was indeed Epoch-1.
*/
if (nowsecs == (time_t)-1) {
nowtime.tm_sec += 1; /* maybe Epoch-1 sec */
nowsecs = (*mk)(&nowtime);
if (nowsecs != 0) { /* check if Epoch */
nowtime.tm_sec += 1; /* Check if it was Epoch-1 by trying Epoch */
nowsecs = (*mk)(&nowtime); /* Call mktime/timegm again */
if (nowsecs != 0) { /* If it's not Epoch, then the original time was invalid */
mrb_raise(mrb, E_ARGUMENT_ERROR, "Not a valid time");
}
nowsecs = (time_t)-1; /* valid Epoch-1 */
nowsecs = (time_t)-1; /* Reset to Epoch-1, which is a valid time_t */
}
return time_alloc_time(mrb, nowsecs, ausec, timezone);
@@ -890,22 +959,23 @@ time_init(mrb_state *mrb, mrb_value self)
mrb_int ayear = 0, amonth = 1, aday = 1, ahour = 0,
amin = 0, asec = 0, ausec = 0;
mrb_int n = mrb_get_args(mrb, "|iiiiiii",
mrb_int n = mrb_get_args(mrb, "|iiiiiii", /* year, month, day, hour, minute, second, microsecond (all optional) */
&ayear, &amonth, &aday, &ahour, &amin, &asec, &ausec);
struct mrb_time *tm = (struct mrb_time*)DATA_PTR(self);
if (tm) {
mrb_free(mrb, tm);
if (tm) { /* If Time object is being re-initialized (e.g. time_obj.send(:initialize, ...)) */
mrb_free(mrb, tm); /* Free existing data */
}
mrb_data_init(self, NULL, &time_type);
mrb_data_init(self, NULL, &time_type); /* Prepare for new data */
if (n == 0) {
tm = current_mrb_time(mrb);
if (n == 0) { /* Time.new (no arguments) */
tm = current_mrb_time(mrb); /* Get current time */
}
else {
else { /* Time.new(year, [mon, day, hour, min, sec, usec]) */
/* Create time from specified components in local timezone */
tm = time_mktime(mrb, ayear, amonth, aday, ahour, amin, asec, ausec, MRB_TIMEZONE_LOCAL);
}
mrb_data_init(self, tm, &time_type);
mrb_data_init(self, tm, &time_type); /* Attach the new mrb_time struct to the mruby object */
return self;
}
@@ -1059,59 +1129,72 @@ time_hash(mrb_state *mrb, mrb_value self)
return mrb_int_value(mrb, hash);
}
#define wday_impl(num) \
struct mrb_time *tm = time_get_ptr(mrb, self);\
return mrb_bool_value(tm->datetime.tm_wday == (num));
static mrb_value
time_sunday(mrb_state *mrb, mrb_value self)
{
wday_impl(0);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 0);
}
static mrb_value
time_monday(mrb_state *mrb, mrb_value self)
{
wday_impl(1);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 1);
}
static mrb_value
time_tuesday(mrb_state *mrb, mrb_value self)
{
wday_impl(2);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 2);
}
static mrb_value
time_wednesday(mrb_state *mrb, mrb_value self)
{
wday_impl(3);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 3);
}
static mrb_value
time_thursday(mrb_state *mrb, mrb_value self)
{
wday_impl(4);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 4);
}
static mrb_value
time_friday(mrb_state *mrb, mrb_value self)
{
wday_impl(5);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 5);
}
static mrb_value
time_saturday(mrb_state *mrb, mrb_value self)
{
wday_impl(6);
struct mrb_time *tm = time_get_ptr(mrb, self);
return mrb_bool_value(tm->datetime.tm_wday == 6);
}
void
mrb_mruby_time_gem_init(mrb_state* mrb)
{
/*
* Initializes the Time class in the mruby state.
* - Defines the Time class (ISO 15.2.19.2).
* - Sets its instance type to MRB_TT_CDATA, meaning instances carry a C data pointer.
* - Includes the Comparable module.
* - Defines class methods (e.g., Time.at, Time.now, Time.gm, Time.local).
* - Defines instance methods (e.g., +, -, <=>, to_s, year, month, day, etc.).
* Many instance methods are aliased (e.g., day and mday).
* Ruby standard library method references (e.g., 15.2.19.6.1) are from an older ISO Ruby spec.
*/
/* ISO 15.2.19.2 */
struct RClass *tc = mrb_define_class_id(mrb, MRB_SYM(Time), mrb->object_class);
MRB_SET_INSTANCE_TT(tc, MRB_TT_CDATA);
mrb_include_module(mrb, tc, mrb_module_get_id(mrb, MRB_SYM(Comparable)));
MRB_SET_INSTANCE_TT(tc, MRB_TT_CDATA); /* Time instances will hold a C pointer (struct mrb_time) */
mrb_include_module(mrb, tc, mrb_module_get_id(mrb, MRB_SYM(Comparable))); /* Include Comparable module */
mrb_define_class_method_id(mrb, tc, MRB_SYM(at), time_at_m, MRB_ARGS_ARG(1, 1)); /* 15.2.19.6.1 */
mrb_define_class_method_id(mrb, tc, MRB_SYM(gm), time_gm, MRB_ARGS_ARG(1,6)); /* 15.2.19.6.2 */
mrb_define_class_method_id(mrb, tc, MRB_SYM(local), time_local, MRB_ARGS_ARG(1,6)); /* 15.2.19.6.3 */