# SDL: time: Correct rounding with negative time values

**URL:** <https://discourse.libsdl.org/t/sdl-time-correct-rounding-with-negative-time-values/69383>\
**Category:** SDL Commits\
**Created:** [September 24, 2026, 1:34pm UTC](https://discourse.libsdl.org/t/sdl-time-correct-rounding-with-negative-time-values/69383 "2026-09-24T13:34:04Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![SDLPushPostBot](https://discourse.libsdl.org/user_avatar/discourse.libsdl.org/sdlpushpostbot/32/4370_2.png) [@SDLPushPostBot](https://discourse.libsdl.org/u/SDLPushPostBot)\
**Post date:** [September 24, 2026, 1:34pm UTC](https://discourse.libsdl.org/t/sdl-time-correct-rounding-with-negative-time-values/69383/1 "2026-09-24T13:34:05Z")

</div>

> <https://github.com/libsdl-org/SDL/commit/fc13ba8aeaafd1a05f664e77e0292c5b4cfc3295>
>
> When dealing with negative values from before the epoch, increasingly negative v…alues represent points further back in time. However, integer rounding goes toward zero, which can cause a lost second when converting negative times with nanosecond values less than one system time unit. Ensure that small, negative nanosecond values don't cause a lost second.
> 
> The automated tests have been extended to test more time values as well.

```diff
From fc13ba8aeaafd1a05f664e77e0292c5b4cfc3295 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Wed, 23 Sep 2026 12:29:50 -0400
Subject: [PATCH] time: Correct rounding with negative time values

When dealing with negative values from before the epoch, increasingly negative values represent points further back in time. However, integer rounding goes toward zero, which can cause a lost second when converting negative times with nanosecond values less than one system time unit. Ensure that small, negative nanosecond values don't cause a lost second.

The automated tests have been extended to test more time values as well.
---
 src/time/n3ds/SDL_systime.c | 9 +-
 src/time/ngage/SDL_systime.cpp | 11 ++-
 src/time/psp/SDL_systime.c | 8 ++
 src/time/unix/SDL_systime.c | 8 +-
 src/time/vita/SDL_systime.c | 10 ++-
 src/time/windows/SDL_systime.c | 11 ++-
 test/testautomation_time.c | 150 +++++++++++++++++++++------------
 7 files changed, 149 insertions(+), 58 deletions(-)

diff --git a/src/time/n3ds/SDL_systime.c b/src/time/n3ds/SDL_systime.c
index d78c5ecf7c401..f9731377014dc 100644
--- a/src/time/n3ds/SDL_systime.c
+++ b/src/time/n3ds/SDL_systime.c
@@ -127,6 +127,13 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
         return SDL_InvalidParamError("dt");
     }
 
+ int rem_ns = (int)(ticks % SDL_NS_PER_SECOND);
+ if (rem_ns < 0) {
+ // Prevent rounding errors if the remaining nanoseconds are less than one unit of system time.
+ ticks -= SDL_NS_PER_SECOND;
+ rem_ns += SDL_NS_PER_SECOND;
+ }
+
     const int days = (int)(SDL_NS_TO_SECONDS(ticks) / SDL_SECONDS_PER_DAY);
     civil_from_days(days, &dt->year, &dt->month, &dt->day);
 
@@ -136,7 +143,7 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
     dt->minute = rem / 60;
     rem -= dt->minute * 60;
     dt->second = rem;
- dt->nanosecond = ticks % SDL_NS_PER_SECOND;
+ dt->nanosecond = rem_ns;
     dt->utc_offset = 0; // Unknown
 
     SDL_CivilToDays(dt->year, dt->month, dt->day, &dt->day_of_week, NULL);
diff --git a/src/time/ngage/SDL_systime.cpp b/src/time/ngage/SDL_systime.cpp
index a3a22de9bb9fa..f5b2efec10cdb 100644
--- a/src/time/ngage/SDL_systime.cpp
+++ b/src/time/ngage/SDL_systime.cpp
@@ -179,6 +179,15 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
         return SDL_InvalidParamError("dt");
     }
 
+ int rem_ns = (int)(ticks % SDL_NS_PER_SECOND);
+ if (rem_ns < 0) {
+ // Prevent rounding errors if the remaining nanoseconds are less than one unit of system time.
+ if (-rem_ns < 1000) {
+ ticks -= 1000;
+ }
+ rem_ns += SDL_NS_PER_SECOND;
+ }
+
     long long unixMicros = ticks / 1000LL;
 
     unixMicros += SYMBIAN_UNIX_EPOCH_OFFSET_US;
@@ -201,7 +210,7 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
     dt->hour = dtSym.Hour();
     dt->minute = dtSym.Minute();
     dt->second = dtSym.Second();
- dt->nanosecond = (int)(ticks % 1000000000LL);
+ dt->nanosecond = rem_ns;
     dt->day_of_week = s60Time.DayNoInWeek();
     dt->utc_offset = 0;
 
diff --git a/src/time/psp/SDL_systime.c b/src/time/psp/SDL_systime.c
index fdce0c7aee57b..f45a969411c5a 100644
--- a/src/time/psp/SDL_systime.c
+++ b/src/time/psp/SDL_systime.c
@@ -105,6 +105,14 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
 
     const u32 res = sceRtcGetTickResolution();
     const u32 div = (SDL_NS_PER_SECOND / res);
+ int rem_ns = (int)(ticks % SDL_NS_PER_SECOND);
+ if (rem_ns < 0) {
+ // Prevent rounding errors if the remaining nanoseconds are less than one unit of system time.
+ if (-rem_ns < (int)div) {
+ ticks -= (int)div;
+ }
+ rem_ns += SDL_NS_PER_SECOND;
+ }
     const u64 sceTicks = (u64)((ticks / div) + (DELTA_EPOCH_0001_OFFSET * div));
 
     if (localTime) {
diff --git a/src/time/unix/SDL_systime.c b/src/time/unix/SDL_systime.c
index b5ae564407c1a..ac5f3bab17998 100644
--- a/src/time/unix/SDL_systime.c
+++ b/src/time/unix/SDL_systime.c
@@ -164,6 +164,12 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
         return SDL_InvalidParamError("dt");
     }
 
+ int rem_ns = (int)(ticks % SDL_NS_PER_SECOND);
+ if (rem_ns < 0) {
+ // Prevent rounding errors if the remaining nanoseconds are less than one unit of system time.
+ ticks -= SDL_NS_PER_SECOND;
+ rem_ns += SDL_NS_PER_SECOND;
+ }
     const time_t tval = (time_t)SDL_NS_TO_SECONDS(ticks);
 
     if (localTime) {
@@ -187,7 +193,7 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
         dt->hour = tm->tm_hour;
         dt->minute = tm->tm_min;
         dt->second = tm->tm_sec;
- dt->nanosecond = ticks % SDL_NS_PER_SECOND;
+ dt->nanosecond = rem_ns;
         dt->day_of_week = tm->tm_wday;
 
         /* tm_gmtoff wasn't formally standardized until POSIX.1-2024, but practically it has been available on desktop
diff --git a/src/time/vita/SDL_systime.c b/src/time/vita/SDL_systime.c
index 1dfd340106766..cc019356a4e54 100644
--- a/src/time/vita/SDL_systime.c
+++ b/src/time/vita/SDL_systime.c
@@ -110,6 +110,14 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
 
     const unsigned int res = sceRtcGetTickResolution();
     const unsigned int div = (SDL_NS_PER_SECOND / res);
+ int rem_ns = (int)(ticks % SDL_NS_PER_SECOND);
+ if (rem_ns < 0) {
+ // Prevent rounding errors if the remaining nanoseconds are less than one unit of system time.
+ if (-rem_ns < (int)div) {
+ ticks -= (int)div;
+ }
+ rem_ns += SDL_NS_PER_SECOND;
+ }
     sceTicks.tick = (Uint64)((ticks / div) + (DELTA_EPOCH_0001_OFFSET * div));
 
     if (localTime) {
@@ -127,7 +135,7 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
             dt->hour = t.hour;
             dt->minute = t.minute;
             dt->second = t.second;
- dt->nanosecond = ticks % SDL_NS_PER_SECOND;
+ dt->nanosecond = rem_ns;
             dt->utc_offset = (int)(((Sint64)sceLocalTicks.tick - (Sint64)sceTicks.tick) / (Sint64)res);
 
             SDL_CivilToDays(dt->year, dt->month, dt->day, &dt->day_of_week, NULL);
diff --git a/src/time/windows/SDL_systime.c b/src/time/windows/SDL_systime.c
index e36ad018418f1..a64be804cbd90 100644
--- a/src/time/windows/SDL_systime.c
+++ b/src/time/windows/SDL_systime.c
@@ -119,6 +119,15 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
         return SDL_InvalidParamError("dt");
     }
 
+ int rem_ns = (int)(ticks % SDL_NS_PER_SECOND);
+ if (rem_ns < 0) {
+ // Prevent rounding errors if the remaining nanoseconds are less than one unit of system time.
+ if (-rem_ns < 100) {
+ ticks -= 100;
+ }
+ rem_ns += SDL_NS_PER_SECOND;
+ }
+
     SDL_TimeToWindows(ticks, &low, &high);
     ft.dwLowDateTime = (DWORD)low;
     ft.dwHighDateTime = (DWORD)high;
@@ -144,7 +153,7 @@ bool SDL_TimeToDateTime(SDL_Time ticks, SDL_DateTime *dt, bool localTime)
             dt->hour = st->wHour;
             dt->minute = st->wMinute;
             dt->second = st->wSecond;
- dt->nanosecond = ticks % SDL_NS_PER_SECOND;
+ dt->nanosecond = rem_ns;
             dt->day_of_week = st->wDayOfWeek;
 
             return true;
diff --git a/test/testautomation_time.c b/test/testautomation_time.c
index 2b599d0872e60..4325ef817b8c3 100644
--- a/test/testautomation_time.c
+++ b/test/testautomation_time.c
@@ -5,8 +5,26 @@
 #include <SDL3/SDL.h>
 #include <SDL3/SDL_test.h>
 
-/* 2000-01-01T16:35:42 UTC */
-#define JAN_1_2000_NS SDL_SECONDS_TO_NS(946744542)
+/* 1970-01-01T00:00:00.00 UTC */
+#define JAN_1_1970_NS 0
+
+/* 1970-01-01T00:00:00.01 UTC */
+#define JAN_1_1970_1_NS 1
+
+/* 1969-12-31T23:59:59.999999997 UTC */
+#define DEC_31_1969_NS -3
+
+/* 2000-01-01T16:35:42.10 UTC */
+#define JAN_1_2000_NS SDL_SECONDS_TO_NS(946744542) + 10
+
+/* 2000-02-29T00:00:00.00 UTC (leap year) */
+#define FEB_29_2000_NS SDL_SECONDS_TO_NS(951782400)
+
+/* 1955-11-05T01:21:59.03 */
+#define NOV_5_1955_NS SDL_SECONDS_TO_NS(-446769481) + 3
+
+/* 1955-11-05T01:21:59.500000000 */
+#define NOV_5_1955_1_NS SDL_SECONDS_TO_NS(-446769481) + 500000000
 
 /* Test case functions */
 
@@ -30,70 +48,96 @@ static int SDLCALL time_getRealtimeClock(void *arg)
  */
 static int SDLCALL time_dateTimeConversion(void *arg)
 {
+ static const struct
+ {
+ SDL_Time ticks;
+ const char *desc;
+
+ /* Expected values */
+ int year;
+ int month;
+ int day;
+ int hour;
+ int minute;
+ int second;
+ int nanosecond;
+ int day_of_week;
+ } TimeTest[] = {
+ { JAN_1_1970_NS, "1970-01-01T00:00:00.00 (Thu)", 1970, 1, 1, 0, 0, 0, 0, 4 },
+ { JAN_1_1970_1_NS, "1970-01-01T00:00:00.01 (Thu)", 1970, 1, 1, 0, 0, 0, 1, 4 },
+ { DEC_31_1969_NS, "1969-12-31T23:59:59.999999997 (Wed)", 1969, 12, 31, 23, 59, 59, 999999997, 3 },
+ { JAN_1_2000_NS, "2000-01-01T16:35:42.10 (Sat)", 2000, 1, 1, 16, 35, 42, 10, 6 },
+ { FEB_29_2000_NS, "2000-02-29T00:00:00.00 (Tue)", 2000, 2, 29, 0, 0, 0, 0, 2 },
+ { NOV_5_1955_NS, "1955-11-05T01:21:59.03 (Sat)", 1955, 11, 5, 1, 21, 59, 3, 6 },
+ { NOV_5_1955_1_NS, "1955-11-05T01:21:59.500000000 (Sat)", 1955, 11, 5, 1, 21, 59, 500000000, 6 }
+ };
     int result;
- SDL_Time ticks[2];
+ SDL_Time ticks;
     SDL_DateTime dt;
 
- ticks[0] = JAN_1_2000_NS;
-
- result = SDL_TimeToDateTime(ticks[0], &dt, false);
- SDLTest_AssertPass("Call to SDL_TimeToUTCDateTime()");
- SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
- SDLTest_AssertCheck(dt.year == 2000, "Check year value, expected 2000, got: %i", dt.year);
- SDLTest_AssertCheck(dt.month == 1, "Check month value, expected 1, got: %i", dt.month);
- SDLTest_AssertCheck(dt.day == 1, "Check day value, expected 1, got: %i", dt.day);
- SDLTest_AssertCheck(dt.hour == 16, "Check hour value, expected 16, got: %i", dt.hour);
- SDLTest_AssertCheck(dt.minute == 35, "Check hour value, expected 35, got: %i", dt.minute);
- SDLTest_AssertCheck(dt.second == 42, "Check hour value, expected 42, got: %i", dt.second);
-
- result = SDL_DateTimeToTime(&dt, &ticks[1]);
- SDLTest_AssertPass("Call to SDL_DateTimeToTime()");
- SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
-
- result = ticks[0] == ticks[1];
- SDLTest_AssertCheck(result, "Check that original and converted SDL_Time values match: ticks0 = %" SDL_PRIs64 ", ticks1 = %" SDL_PRIs64, ticks[0], ticks[1]);
-
- /* Local time unknown, so just verify success. */
- result = SDL_TimeToDateTime(ticks[0], &dt, true);
- SDLTest_AssertPass("Call to SDL_TimeToLocalDateTime()");
- SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
-
- /* Convert back and verify result. */
- result = SDL_DateTimeToTime(&dt, &ticks[1]);
- SDLTest_AssertPass("Call to SDL_DateTimeToTime()");
- SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
-
- result = ticks[0] == ticks[1];
- SDLTest_AssertCheck(result, "Check that original and converted SDL_Time values match: ticks0 = %" SDL_PRIs64 ", ticks1 = %" SDL_PRIs64, ticks[0], ticks[1]);
-
- /* Advance the time one day. */
- ++dt.day;
- if (dt.day > SDL_GetDaysInMonth(dt.year, dt.month)) {
- dt.day = 1;
- ++dt.month;
- }
- if (dt.month > 12) {
- dt.month = 1;
- ++dt.year;
+ for (int i = 0; i < SDL_arraysize(TimeTest); ++i) {
+ result = SDL_TimeToDateTime(TimeTest[i].ticks, &dt, false);
+ SDLTest_Log("Testing time conversion for %s", TimeTest[i].desc);
+ SDLTest_AssertPass("Call to SDL_TimeToUTCDateTime()");
+ SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
+ SDLTest_AssertCheck(dt.year == TimeTest[i].year, "Check year value, expected %i, got: %i", TimeTest[i].year, dt.year);
+ SDLTest_AssertCheck(dt.month == TimeTest[i].month, "Check month value, expected %i, got: %i", TimeTest[i].month, dt.month);
+ SDLTest_AssertCheck(dt.day == TimeTest[i].day, "Check day value, expected %i, got: %i", TimeTest[i].day, dt.day);
+ SDLTest_AssertCheck(dt.hour == TimeTest[i].hour, "Check hour value, expected %i, got: %i", TimeTest[i].hour, dt.hour);
+ SDLTest_AssertCheck(dt.minute == TimeTest[i].minute, "Check hour value, expected %i, got: %i", TimeTest[i].minute, dt.minute);
+ SDLTest_AssertCheck(dt.second == TimeTest[i].second, "Check second value, expected %i, got: %i", TimeTest[i].second, dt.second);
+ SDLTest_AssertCheck(dt.nanosecond == TimeTest[i].nanosecond, "Check nanosecond value, expected %i, got: %i", TimeTest[i].nanosecond, dt.nanosecond);
+ SDLTest_AssertCheck(dt.day_of_week == TimeTest[i].day_of_week, "Check day of week, expected %i, got: %i", TimeTest[i].day_of_week, dt.day_of_week);
+
+ result = SDL_DateTimeToTime(&dt, &ticks);
+ SDLTest_AssertPass("Call to SDL_DateTimeToTime()");
+ SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
+
+ result = TimeTest[i].ticks == ticks;
+ SDLTest_AssertCheck(result, "Check that original and converted SDL_Time values match: original = %" SDL_PRIs64 ", converted = %" SDL_PRIs64, TimeTest[i].ticks, ticks);
+
+ /* Local time unknown, so just verify success. */
+ result = SDL_TimeToDateTime(TimeTest[i].ticks, &dt, true);
+ SDLTest_AssertPass("Call to SDL_TimeToLocalDateTime()");
+ SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
+
+ /* Convert back and verify result. */
+ result = SDL_DateTimeToTime(&dt, &ticks);
+ SDLTest_AssertPass("Call to SDL_DateTimeToTime()");
+ SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
+
+ result = TimeTest[i].ticks == ticks;
+ SDLTest_AssertCheck(result, "Check that original and converted SDL_Time values match: original = %" SDL_PRIs64 ", converted = %" SDL_PRIs64, TimeTest[i].ticks, ticks);
+
+ /* Advance the time one day. */
+ ++dt.day;
+ if (dt.day > SDL_GetDaysInMonth(dt.year, dt.month)) {
+ dt.day = 1;
+ ++dt.month;
+ }
+ if (dt.month > 12) {
+ dt.month = 1;
+ ++dt.year;
+ }
+
+ result = SDL_DateTimeToTime(&dt, &ticks);
+ SDLTest_AssertPass("Call to SDL_DateTimeToTime() (one day advanced)");
+ SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
+
+ result = (TimeTest[i].ticks + (Sint64)SDL_SECONDS_TO_NS(86400)) == ticks;
+ SDLTest_AssertCheck(result, "Check that the difference is exactly 86400 seconds, got: %" SDL_PRIs64, (Sint64)SDL_NS_TO_SECONDS(ticks - TimeTest[i].ticks));
     }
 
- result = SDL_DateTimeToTime(&dt, &ticks[1]);
- SDLTest_AssertPass("Call to SDL_DateTimeToTime() (one day advanced)");
- SDLTest_AssertCheck(result == true, "Check result value, expected true, got: %i", result);
-
- result = (ticks[0] + (Sint64)SDL_SECONDS_TO_NS(86400)) == ticks[1];
- SDLTest_AssertCheck(result, "Check that the difference is exactly 86400 seconds, got: %" SDL_PRIs64, (Sint64)SDL_NS_TO_SECONDS(ticks[1] - ticks[0]));
-
     /* Check dates that overflow/underflow an SDL_Time */
     dt.year = 2400;
     dt.month = 1;
     dt.day = 1;
- result = SDL_DateTimeToTime(&dt, &ticks[0]);
+ result = SDL_DateTimeToTime(&dt, &ticks);
     SDLTest_AssertPass("Call to SDL_DateTimeToTime() (year overflows an SDL_Time)");
     SDLTest_AssertCheck(result == false, "Check result value, expected false, got: %i", result);
 
     dt.year = 1601;
- result = SDL_DateTimeToTime(&dt, &ticks[0]);
+ result = SDL_DateTimeToTime(&dt, &ticks);
     SDLTest_AssertPass("Call to SDL_DateTimeToTime() (year underflows an SDL_Time)");
     SDLTest_AssertCheck(result == false, "Check result value, expected false, got: %i", result);
 

```
