Option 1: Stress-test mktime()
#include <stdint.h>
#include <stdio.h>
#include <time.h>
static void yd_to_ymd(int year, int day);
int main(void)
{
yd_to_ymd(1996, 300);
putchar('
');
yd_to_ymd(1997, 300);
return 0;
}
static char *fmt_ordinal(char *buffer, size_t buflen, unsigned n);
static void dump_tm(const char *tag, const struct tm *t);
static void yd_to_ymd(int year, int day)
{
struct tm t0 =
{
.tm_year = year - 1900, .tm_mday = day, .tm_mon = 0,
.tm_hour = 0, .tm_min = 0, .tm_sec = 0,
.tm_isdst = -1,
};
dump_tm("t0", &t0);
time_t uts = mktime(&t0);
printf("%ju
", (intmax_t)uts);
dump_tm("t1", &t0);
char ordinal[8];
fmt_ordinal(ordinal, sizeof(ordinal), t0.tm_mday);
char month[16];
strftime(month, sizeof(month), "%B", &t0);
char weekday[10];
strftime(weekday, sizeof(weekday), "%A", &t0);
printf("%s the %s day of the month of %s
", weekday, ordinal, month);
}
static void dump_tm(const char *tag, const struct tm *t)
{
printf("%s: %.4d-%.2d-%.2d %.2d:%.2d:%.2d (W %d; Y %3d; DST %d)
",
tag, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec,
t->tm_wday, t->tm_yday + 1, t->tm_isdst);
}
static const char *const suffixes[4] = { "th", "st", "nd", "rd" };
static inline unsigned suffix_index(unsigned n)
{
unsigned x;
x = n % 100;
if (x == 11 || x == 12 || x == 13)
x = 0;
else if ((x = x % 10) > 3)
x = 0;
return x;
}
static char *fmt_ordinal(char *buffer, size_t buflen, unsigned n)
{
unsigned x = suffix_index(n);
int len = snprintf(buffer, buflen, "%d%s", n, suffixes[x]);
if (len <= 0 || (size_t)len >= buflen)
return 0;
return(buffer);
}
The mktime()
function normalizes the date/time information in the tm_year
, tm_mon
, tm_mday
, tm_hour
, tm_min
, tm_sec
fields, using tm_isdst
to guide choices related to Winter vs Summer time — or Daylight Saving vs Standard time. It also updates the tm_yday
and tm_wday
fields to reflect what's deduced. So, by setting the structure up to describe the day of the year (e.g. 300) as the 300th day of January, mktime()
will convert that to the correct date — 26th October in a leap year such as 1996, 27th October in a non-leap year such as 1997.
The code above initializes a struct tm
value with the correct encoding for the 300th day of January in the given year (1996 in this example). It dumps that information via the dump_tm()
function. Note that this 'undoes' the encodings in the structure — it reports the value tm_year + 1900
; it reports the value tm_mon + 1
(so months are numbered 1..12 instead of 0..11), and it reports the value tm_yday + 1
so 1st January is day 1 of the year rather than day 0. It then calls the mktime()
routine to get the Unix timestamp (seconds since the Unix Epoch — which is 1970-01-01T00:00:00Z), and calls dump_tm()
to show the adjusted values.
Example output:
t0: 1996-01-300 00:00:00 (W 0; Y 1; DST -1)
846309600
t1: 1996-10-26 00:00:00 (W 6; Y 300; DST 1)
Saturday the 26th day of the month of October
t0: 1997-01-300 00:00:00 (W 0; Y 1; DST -1)
877935600
t1: 1997-10-27 00:00:00 (W 1; Y 300; DST 0)
Monday the 27th day of the month of October
If you look at the data from Option 2, you'll see that 1996-300 was 1996-10-26, and 1997-300 was 1997-10-27.
Given the timestamp 846309600, that corresponds to Sat Oct 26 00:00:00 1996 in the US/Mountain time zone, and 877935600 corresponds to Mon Oct 27 00:00:00 1997 (note that in 1996, the time zone was in daylight saving time, and in 1997 the time zone was in standard time — back then, the time zone did its 'fall back' on the last Sunday in October):
$ timestamp -l 846309600 877935600
846309600 = Sat Oct 26 00:00:00 1996
877935600 = Mon Oct 27 00:00:00 1997
$ timestamp -u 846309600 877935600
846309600 = Sat Oct 26 06:00:00 1996
877935600 = Mon Oct 27 07:00:00 1997
$ timestamp -Z -l 846309600 877935600
846309600 = Sat Oct 26 00:00:00 1996 -06:00
877935600 = Mon Oct 27 00:00:00 1997 -07:00
$ timestamp -Z -u 846309600 877935600
846309600 = Sat Oct 26 06:00:00 1996 +00:00
877935600 = Mon Oct 27 07:00:00 1997 +00:00
$ timestamp -I -Z -l 846309600 877935600
846309600 = 1996-10-26 00:00:00 -06:00
877935600 = 1997-10-27 00:00:00 -07:00
$ timestamp -I -Z -u 846309600 877935600
846309600 = 1996-10-26 06:00:00 +00:00
877935600 = 1997-10-27 07:00:00 +00:00
$
Or, if you prefer to use GNU date
, then:
$ date -d @846309600 --iso-8601=seconds
1996-10-26T00:00:00-0600
$ date -u -d @846309600 --iso-8601=seconds
1996-10-26T06:00:00+0000
$
Spelling out the year or day of the month (e.g. "one thousand, nine hundred and ninety-six") is harder; the code to do that is much larger. Making that an ordinal spelling ("… ninety-sixth") requires care too.
Option 2: DIY
I don't think the use of time()
or localtime()
is warranted. Using strftime()
and maybe mktime()
would be more sensible, but convert 'day of year' to 'month and day' is not standardized AFAIK.
If you want to convert "year (1..9999) and day of year (1..366)" to "year (1..9999), month (1..12), and day (1..31)", you probably need a leap year function, and a table of the number of days in each month, and then use a brute force the calculation.
Header yd2ymd.c
#ifndef JLSS_ID_YD2YMD_H
#define JLSS_ID_YD2YMD_H
extern int cvt_yd_to_ymd(int year, int yday, int *mon, int *day);
extern int cvt_ymd_to_yd(int year, int mon, int day, int *yday);
#endif
Source yd2ymd.c
#include "yd2ymd.h"
#include <assert.h>
#include <stdbool.h>
static const int days_in_month[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static inline bool is_leap_year(int year)
{
if (year % 4 != 0)
return false;
if (year % 100 != 0)
return true;
if (year % 400 == 0)
return true;
return false;
/* return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); */
}
#define PRECOND(x) do { if (!(x)) return -1; } while (0);
int cvt_yd_to_ymd(int year, int yday, int *mon, int *day)
{
PRECOND(year > 0 && year <= 9999);
PRECOND(yday > 0 && (yday < 366 || (yday == 366 && is_leap_year(year))));
assert(mon != 0 && day != 0);
if (is_leap_year(year))
{
if (yday == 31 + 29)
{
*mon = 2;
*day = 29;
return 0;
}
if (yday > 31 + 29)
yday--;
}
*mon = 1;
for (int i = 1; i <= 12; i++)
{
if (yday > days_in_month[i])
{
yday -= days_in_month[i];
(*mon)++;
}
else
{
*day = yday;
return 0;
}
}
assert(0 || "can't happen");
return -1;
}
int cvt_ymd_to_yd(int year, int mon, int day, int *yday)
{
PRECOND(year > 0 && year <= 9999);
PRECOND(mon >= 1 && mon <= 12);
PRECOND(day >= 1 && (day <= days_in_month[mon] ||
(day == 29 && mon == 2 && is_leap_year(year))));
assert(yday != 0);
int day_num = day;
bool leap = is_leap_year(year);
if (day == 29 && mon == 2 && leap)
{
day_num += 31;
}
else
{
if (leap && mon > 2)
day_num++;
for (int i = 1; i < mon; i++)
day_num += days_in_month[i];
}
*yday = day_num;
return 0;
}
#ifdef TEST
#include <stdio.h>
int main(void)
{
/* Check leap years */
int length = 0;
const char *pad = "";
for (int i = 1895; i < 2105; i++)
{
if (is_leap_year(i))
{
int extra = printf("%s%d", pad, i);
length += extra;
if (length > 70)
{
putchar('
');
length = 0;
pad = "";
}
else
pad = ", ";
}
}
if (length > 0)
putchar('
');
/* Check conversions from YYYY-DDD to YYYY-MM-DD and back */
const int years[] = { 1996, 1997 };
enum { NUM_YEARS = sizeof(years) / sizeof(years[0]) };
for (int i = 0; i < NUM_YEARS; i++)
{
for (int j = 1; j <= 366; j++)
{
int day;
int mon;
if (cvt_yd_to_ymd(years[i], j, &mon, &day) != 0)
fprintf(stderr, "Failed to convert year %d, day %d to YYYY-MM-DD
",
years[i], j);
else
{
printf("Y = %4d, D = %3d --> M = %2d, D = %2d; ",
years[i], j, mon, day);
int yday;
if (cvt_ymd_to_yd(years[i], mon, day, &yday) != 0)
fprintf(stderr, "Failed to convert %4d-%.2d-%.2d to YYYY-DDD
",
years[i], mon, day);
else if (yday != j)
fprintf(stderr, "Incorrect day of year (%d wanted vs %d actual)
",
j, yday);
else
printf("Y = %4d, M = %.2d, D = %.2d --> D = %3d",
years[i], mon, day, yday);
putchar('
');
}
}
}
return 0;
}
#endif
Partial output
1896, 1904, 1908, 1912, 1916, 1920, 1924, 1928, 1932, 1936, 1940, 1944, 1948
1952, 1956, 1960, 1964, 1968, 1972, 1976, 1980, 1984, 1988, 1992, 1996, 2000
2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, 2048, 2052
2056, 2060, 2064, 2068, 2072, 2076, 2080, 2084, 2088, 2092, 2096, 2104
Y = 1996, D = 1 --> M = 1, D = 1; Y = 1996, M = 01, D = 01 --> D = 1
Y = 1996, D = 2 --> M = 1, D = 2; Y = 1996, M = 01, D = 02 --> D = 2
Y = 1996, D = 3 --> M = 1, D = 3; Y = 1996, M = 01, D = 03 --> D = 3
...
Y = 1996, D = 27 --> M = 1, D = 27; Y = 1996, M = 01, D = 27 --> D = 27
Y = 1996, D = 28 --> M = 1, D = 28; Y = 1996, M = 01, D = 28 --> D = 28
Y = 1996, D = 29 --> M = 1, D = 29; Y = 1996, M = 01, D = 29 --> D = 29
Y = 1996, D = 30 --> M = 1, D = 30; Y = 1996, M = 01, D = 30 --> D = 30
Y = 1996, D = 31 --> M = 1, D = 31; Y = 1996, M = 01, D = 31 --> D = 31
Y = 1996, D = 32 --> M = 2, D = 1; Y = 1996, M = 02, D = 01 --> D = 32
Y = 1996, D = 33 --> M = 2, D = 2; Y = 1996, M = 02, D = 02 --> D = 33
Y = 1996, D = 34 --> M = 2, D = 3; Y = 1996, M = 02, D = 03 --> D = 34
...
Y = 1996, D = 58 --> M = 2, D = 27; Y = 1996, M = 02, D = 27 --> D = 58
Y = 1996, D = 59 --> M = 2, D = 28; Y = 1996, M = 02, D = 28 --> D = 59
Y = 1996, D = 60 --> M = 2, D = 29; Y = 1996, M = 02, D = 29 --> D = 60
Y = 1996, D = 61 --> M = 3, D = 1; Y = 1996, M = 03, D = 01 --> D = 61
Y = 1996, D = 62 --> M = 3, D = 2; Y = 1996, M = 03, D = 02 --> D = 62
Y = 1996, D = 63 --> M = 3, D = 3; Y = 199