Skip to content

Commit

Permalink
Fix problem with setting the month of certain leap years.
Browse files Browse the repository at this point in the history
  • Loading branch information
GrieferAtWork committed Jan 11, 2025
1 parent 1c5874e commit c5a16aa
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 17 deletions.
54 changes: 37 additions & 17 deletions src/dex/time/libtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -1132,35 +1132,55 @@ DeeTime_SetRepr(DeeTimeObject *__restrict self,
} break;

case TIME_REPR_MONTH: { /* Month in Year */
Dee_int128_t old_months, mday, extra_nano;
Dee_int128_t old_months, nano_since_start_of_month, start_of_month;
Dee_int128_t new_months, month_delta, new_year;
Dee_int128_t new_month_length;
uint16_t new_month_length_in_days;
uint8_t old_month, new_month;
struct month const *new_year_months;
uint16_t new_month_length;
_DeeTime_GetRepr(&old_months, self, TIME_REPR_MONTHS);
_DeeTime_GetRepr(&mday, self, TIME_REPR_MDAY);

/* Figure out the accurate month number since "0" */
old_months = self->t_nanos;
time_inplace_nanosecond2month(&old_months);

/* Figure out the # of days since the start of the relevant month. */
start_of_month = old_months;
time_inplace_month2nanosecond(&start_of_month);
nano_since_start_of_month = self->t_nanos;
__hybrid_int128_sub128(nano_since_start_of_month, start_of_month);

/* Figure out the month within the relevant year, and determine delta. */
__hybrid_int128_floormod8_r(old_months, MONTHS_PER_YEAR, old_month);
++old_month; /* 1-based */
month_delta = *p_value;
__hybrid_int128_sub8(month_delta, old_month);

/* Apply delta to months */
new_months = old_months;
__hybrid_int128_add128(new_months, month_delta);
__hybrid_int128_floordivmod8(new_months, MONTHS_PER_YEAR, new_year, new_month);
new_year_months = month_info_for_year(&new_year);
new_month_length = month_getlen(&new_year_months[new_month]);

/* Clamp day-of-month to the max valid value in the context of the new month */
__hybrid_int128_dec(mday); /* We need a 0-based day-of-month */
if (__hybrid_int128_ge16(mday, new_month_length))
__hybrid_int128_set16(mday, new_month_length - 1);
/* Figure out the new, relevant year+month */
__hybrid_int128_floordivmod8(new_months, MONTHS_PER_YEAR, new_year, new_month);
new_year_months = month_info_for_year(&new_year);
new_month_length_in_days = month_getlen(&new_year_months[new_month]);
__hybrid_uint128_set16(*(Dee_uint128_t *)&new_month_length, new_month_length_in_days);
time_inplace_days2nanoseconds(&new_month_length);

/* Clamp day-of-month to the max valid value in the context of the new month.
* This is needed for:
* >> local t = Time(year: 1976, month: 1, day: 31);
* >> t.month = 2;
* >> print repr t;
* >> assert t == Time(year: 1976, month: 2, day: 29, hour: 23, minute: 59, second: 59, nanosecond: 999999999);
* Without this, the date's month would unexpectedly be in March instead. */
if (__hybrid_int128_ge128(nano_since_start_of_month, new_month_length)) {
nano_since_start_of_month = new_month_length;
__hybrid_int128_dec(nano_since_start_of_month);
}

/* Combine `new_months' and `mday' to set the total number of days */
/* Combine `new_months' and `nano_since_start_of_month' to form a new timestamp. */
time_inplace_month2nanosecond(&new_months);
time_inplace_days2nanoseconds(&mday);
__hybrid_int128_add128(new_months, mday);
extra_nano = self->t_nanos; /* Keep hour+minute+second+nanosecond */
__hybrid_int128_floormod64(extra_nano, NANOSECONDS_PER_DAY);
__hybrid_int128_add128(new_months, extra_nano);
__hybrid_int128_add128(new_months, nano_since_start_of_month);
self->t_nanos = new_months;
} break;

Expand Down
1 change: 1 addition & 0 deletions util/test/deemon-string-format-extended.dee
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ assert "x = 11" == "x = {_$1 + $1}".format({ "_$1": 10 });

/* Do some more tests on function calls done from within template strings. */
function func(a = 30, b = 40) -> a + b;
assert "x = 70" == "x = {func()}".format({ "func": func });
assert "x = 50" == "x = {func($10)}".format({ "func": func });
assert "x = 30" == "x = {func($10, $20)}".format({ "func": func });
assert "x = 30" == "x = {func($10, b: $20)}".format({ "func": func });
Expand Down
51 changes: 51 additions & 0 deletions util/test/time-month.dee
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/deemon
/* Copyright (c) 2018-2024 Griefer@Work *
* *
* This software is provided 'as-is', without any express or implied *
* warranty. In no event will the authors be held liable for any damages *
* arising from the use of this software. *
* *
* Permission is granted to anyone to use this software for any purpose, *
* including commercial applications, and to alter it and redistribute it *
* freely, subject to the following restrictions: *
* *
* 1. The origin of this software must not be misrepresented; you must not *
* claim that you wrote the original software. If you use this software *
* in a product, an acknowledgement (see the following) in the product *
* documentation is required: *
* Portions Copyright (c) 2018-2024 Griefer@Work *
* 2. Altered source versions must be plainly marked as such, and must not be *
* misrepresented as being the original software. *
* 3. This notice may not be removed or altered from any source distribution. *
*/

import * from deemon;
import * from time;

function getYMD(t: Time): (int, int, int) {
return (t.year, t.month, t.mday);
}


/* Make sure that months in leap-years are handled correctly.
*
* Before, there was an off-by-one error, where setting the
* month of a leap-year would subtract 1 year. */
assert getYMD(Time(year: 103, month: 2, day: 29)) == (103, 3, 1);
assert getYMD(Time(year: 104, month: 2, day: 29)) == (104, 2, 29);
assert getYMD(Time(year: 105, month: 2, day: 29)) == (105, 3, 1);
assert getYMD(Time(year: 1975, month: 2, day: 29)) == (1975, 3, 1);
assert getYMD(Time(year: 1976, month: 2, day: 29)) == (1976, 2, 29);
assert getYMD(Time(year: 1977, month: 2, day: 29)) == (1977, 3, 1);

/* Assert that sub-month times are properly clamped when setting a
* month while the sub-month time would not fit into the new month. */
local t = Time(year: 1977, month: 1, day: 31);
t.month = 2;
assert t == Time(year: 1977, month: 2, day: 28, hour: 23, minute: 59, second: 59, nanosecond: 999999999);

/* Also make sure that month-day clamping correctly makes use of leap days. */
local t = Time(year: 1976, month: 1, day: 31);
t.month = 2;
assert t == Time(year: 1976, month: 2, day: 29, hour: 23, minute: 59, second: 59, nanosecond: 999999999);

0 comments on commit c5a16aa

Please sign in to comment.