Skip to content

Commit

Permalink
Calendar math fixes, clamp dates to supported range
Browse files Browse the repository at this point in the history
Fixes for floor division of negative numbers.

We no longer throw an error with an out-of-range date is constructed.
Instead, if an epoch timestamp is used that is too small or large, we
clamp the range to our supported min/max.
  • Loading branch information
phensley committed Aug 27, 2024
1 parent 7e2b139 commit 6e0a173
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 25 deletions.
12 changes: 10 additions & 2 deletions codegen/src/suite/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,24 @@ export const LOCALES = [
];

export const DATES: number[] = [
// Out of range negative date
-500000000000000,
// Minimum date -4712-01-01 00:00:00 UTC
-210866803200000,
// Mon, February 6, 1956 4:54:57 PM
-438678303000,
// Thursday, January 1, 1970 12:00:00 AM GMT
// Thursday, January 1, 1970 12:00:00 AM UTC
0,
// Wed, September 23, 1987 5:03:24 PM
559415004000,
// Sat, May 11, 1996 3:55:31 AM
831786931000,
// Monday, January 27, 2020 12:34:56 PM GMT
// Monday, January 27, 2020 12:34:56 PM UTC
1580128496000,
// Maximum date 8652-12-31 00:00:00 UTC
210895056000000,
// Out of range positive date
500000000000000,
];

export const ZONES: string[] = [
Expand Down
32 changes: 18 additions & 14 deletions src/main/java/com/squarespace/cldrengine/api/CalendarDate.java
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ protected String _toString(String type) {
return String.format("%s %s%04d-%02d-%02d %02d:%02d:%02d.%03d %s",
type,
neg ? "-" : "",
year,
Math.abs(year),
this.month(),
this.dayOfMonth(),
this.hourOfDay(),
Expand Down Expand Up @@ -931,8 +931,8 @@ protected long unixEpochFromJD (long jd, long msDay) {
* is relative to these.
*/
protected void computeBaseFields(long[] f) {
long jd = f[DateField.JULIAN_DAY];
checkJDRange(jd);
long jd = clamp(f[DateField.JULIAN_DAY], CalendarConstants.JD_MIN, CalendarConstants.JD_MAX);
// checkJDRange(jd);

long msDay = f[DateField.MILLIS_IN_DAY];
long ms = msDay + ((jd - CalendarConstants.JD_UNIX_EPOCH) * CalendarConstants.ONE_DAY_MS);
Expand Down Expand Up @@ -960,17 +960,21 @@ protected void computeBaseFields(long[] f) {
f[DateField.DAY_OF_WEEK] = dow;
}

protected long checkJDRange(long jd) {
// TODO: emit warning?

// throw new Error(
// `Julian day ${jd} is outside the supported range of this library: ` +
// `${ConstantsDesc.JD_MIN} to ${ConstantsDesc.JD_MAX}`);

if (jd < CalendarConstants.JD_MIN) {
return CalendarConstants.JD_MIN;
}
return jd > CalendarConstants.JD_MAX ? CalendarConstants.JD_MAX : jd;
private static long clamp(long n, long min, long max) {
return n < min ? min : (n > max ? max : n);
}

// protected long checkJDRange(long jd) {
// // TODO: emit warning?
//
//// throw new Error(
//// `Julian day ${jd} is outside the supported range of this library: ` +
//// `${ConstantsDesc.JD_MIN} to ${ConstantsDesc.JD_MAX}`);
//
// if (jd < CalendarConstants.JD_MIN) {
// return CalendarConstants.JD_MIN;
// }
// return jd > CalendarConstants.JD_MAX ? CalendarConstants.JD_MAX : jd;
// }

}
14 changes: 8 additions & 6 deletions src/main/java/com/squarespace/cldrengine/api/GregorianDate.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.squarespace.cldrengine.api;

import static com.squarespace.cldrengine.internal.MathFix.floorDiv;

import com.squarespace.cldrengine.internal.MathFix;
import com.squarespace.cldrengine.utils.MathUtil;

Expand Down Expand Up @@ -135,7 +137,7 @@ protected void computeGregorianFields(long[] f) {
if (doy >= mar1) {
corr = isLeap ? 1 : 2;
}
int month = (int)Math.floor((12 * (doy + corr) + 6) / 367);
int month = (int)MathFix.floorDiv(12 * (doy + corr) + 6, 367);
long dom = doy - MONTH_COUNT[month][isLeap ? 3 : 2] + 1;

f[DateField.EXTENDED_YEAR] = year;
Expand All @@ -152,8 +154,8 @@ protected void computeGregorianFields(long[] f) {
*/
protected void computeJulianFields(long[] f) {
long jed = f[DateField.JULIAN_DAY] - (CalendarConstants.JD_GREGORIAN_EPOCH - 2);
long eyear = (long)Math.floor((4 * jed + 1464) / 1461);
long jan1 = 365 * (eyear - 1) + (long)Math.floor((eyear - 1) / 4);
long eyear = floorDiv(4 * jed + 1464, 1461);
long jan1 = 365 * (eyear - 1) + floorDiv(eyear - 1, 4);
long doy = jed - jan1;
boolean isLeap = eyear % 4 == 0;
long corr = 0;
Expand All @@ -162,7 +164,7 @@ protected void computeJulianFields(long[] f) {
corr = isLeap ? 1 : 2;
}

int month = (int)Math.floor((12 * (doy + corr) + 6) / 365);
int month = (int)(12 * (doy + corr) + 6) / 367;
long dom = doy - MONTH_COUNT[month][isLeap ? 3 : 2] + 1;

f[DateField.EXTENDED_YEAR] = eyear;
Expand All @@ -171,7 +173,7 @@ protected void computeJulianFields(long[] f) {
f[DateField.DAY_OF_YEAR] = doy + 1;
f[DateField.IS_LEAP] = isLeap ? 1 : 0;
}

/**
* Return true if the given year is a leap year in the Gregorian calendar; false otherwise.
* Note that we switch to the Julian calendar at the Gregorian cutover year.
Expand All @@ -183,7 +185,7 @@ protected boolean leapGregorian(long year) {
}
return r;
}

private static final int[][] MONTH_COUNT = new int[][] {
new int[] { 31, 31, 0, 0 }, // Jan
new int[] { 28, 29, 31, 31 }, // Feb
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/com/squarespace/cldrengine/api/PersianDate.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.squarespace.cldrengine.api;

import com.squarespace.cldrengine.internal.MathFix;
import com.squarespace.cldrengine.utils.MathUtil;

/**
Expand Down Expand Up @@ -107,10 +108,10 @@ protected long monthStart(long eyear, double month, boolean useMonth) {
private void computePersianFields(long[] f) {
long jd = f[DateField.JULIAN_DAY];
long days = jd - CalendarConstants.JD_PERSIAN_EPOCH;
long year = 1 + (long)Math.floor((33 * days + 3) / 12053);
long favardin1 = 365 * (year - 1) + (long)Math.floor((8 * year + 21) / 33);
long year = 1 + (long)MathFix.floorDiv((33 * days + 3), 12053);
long favardin1 = 365 * (year - 1) + (long)MathFix.floorDiv((8 * year + 21), 33);
long doy = days - favardin1;
int month = (int)Math.floor(doy < 216 ? (doy / 31) : ((doy - 6) / 30));
int month = (int)(doy < 216 ? MathFix.floorDiv(doy, 31) : MathFix.floorDiv(doy - 6, 30));
long dom = doy - MONTH_COUNT[month][2] + 1;

f[DateField.ERA] = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.squarespace.cldrengine.api;

import static org.testng.Assert.assertEquals;

import org.testng.Assert;
import org.testng.annotations.Test;

import com.squarespace.cldrengine.CLDR;
import com.squarespace.cldrengine.api.CalendarDate;
import com.squarespace.cldrengine.api.GregorianDate;
import com.squarespace.cldrengine.calendars.DayOfWeek;

public class GregorianDateTest {

private static final CLDR EN = CLDR.get("en");
private static final String UTC = "Etc/UTC";
private static final String NEW_YORK = "America/New_York";

private static final long unixEpochFromJD(long jd, long msDay) {
long days = jd - CalendarConstants.JD_UNIX_EPOCH;
return days * CalendarConstants.ONE_DAY_MS + Math.round(msDay);
}

private static final CalendarDate make(long epoch, String zoneId) {
return GregorianDate.fromUnixEpoch(epoch, zoneId, DayOfWeek.SUNDAY, 1);
}

@Test
public void testMinMaxClamp() {
CalendarDate d;

long min = unixEpochFromJD(CalendarConstants.JD_MIN, 0);
long max = unixEpochFromJD(CalendarConstants.JD_MAX, 0);

d = make(min, "UTC");
assertEquals(d.toString(), "Gregorian -4712-01-01 00:00:00.000 Etc/UTC");

// Clamp to minimum date
d = make(min - CalendarConstants.ONE_DAY_MS, "UTC");
assertEquals(d.toString(), "Gregorian -4712-01-01 00:00:00.000 Etc/UTC");

d = make(max, "UTC");
assertEquals(d.toString(), "Gregorian 8652-12-31 00:00:00.000 Etc/UTC");

d = make(max + CalendarConstants.ONE_DAY_MS, "UTC");
assertEquals(d.toString(), "Gregorian 8652-12-31 00:00:00.000 Etc/UTC");
}

}

0 comments on commit 6e0a173

Please sign in to comment.