0
votes

The question is how to convert dates between different representations using integer arithmetic, specifically between a "Days since year zero ( Jan 1, 0000 )" representation, and either Year/Day or Year/Month/Day forms, in the Julian calendar. The different representations are useful for date input, date display and date arithmetic.

Specifically, a date such as June 5th, 2020 is represented in Year/Month/Day representation as

2020 * 512 + 6 * 32 + 5

or in Year/Day representation as

2020 * 512 + 157 ( June 5th is day 157 in a leap year ).

Yesterday, I wanted to write code to do this, and surprisingly didn't find much help online, so I thought I would document my solution here ( I will be answering my own question ).

1

1 Answers

0
votes

First here is code for converting from Year/Day to Days ( the language is very similar to Microsoft SQL Server T-SQL, except variable names do not begin with '@', and there is a 'bool" data type ) :

CREATE FUNCTION [date].[YearDayToDays]( yd int ) RETURNS int AS
BEGIN
  -- Given a date in Year/Day representation stored as y * 512 + d where 1 <= d <= 366 ( so d is day in year )
  -- returns the number of days since "day zero" (1 Jan 0000)
  -- using the Gregorian calendar where days divisible by 4 are leap years, except if divisible by 100, except if divisible by 400.

  DECLARE y int, d int, cycle int

  -- Extract y and d from yd.
  SET y = yd / 512, d = yd % 512 - 1

  SET cycle = y / 400, y = y % 400 -- The Gregorian calendar repeats every 400 years.
 
  -- Result days come from cycles, from years having at least 365 days, from leap years and finally d.
  -- 146097 is the number of the days in a 400 year cycle ( 400 * 365 + 97 leap years ).
  RETURN cycle * 146097 
    + y * 365 
    + ( y + 3 ) / 4 - ( y + 99 ) / 100 + ( y + 399 ) / 400
    + d
END

Now the opposite conversion, Days to Year/Day:

CREATE FUNCTION [date].[DaysToYearDay]( days int ) returns int as
begin
  -- Given a date represented by the number of days since 1 Jan 0000
  -- calculate a date in Year/Day representation stored as
  -- y * 512 + d where d is 1..366
  
  DECLARE y int, d int, cycle int

  -- 146097 is the number of the days in a 400 year cycle ( 400 * 365 + 97 leap years )
  SET cycle = days / 146097
  SET days = days % 146097

  SET y = days / 365
  SET d = days % 365

  -- Need to adjust d to allow for leap years.
  -- Leap years are 0, 4, 8, 12 ... 96, not 100, 104 ... not 200... not 300, 400, 404 ... not 500.
  -- Adjustment as function of y is 0 => 0, 1 => 1, 2 =>1, 3 => 1, 4 => 1, 5 => 2 ..

  SET d = d - ( y + 3 ) / 4 - ( y + 99 ) / 100 + ( y + 399 ) / 400
  
  IF d < 0
  BEGIN
    SET y = y - 1
    SET d = d + CASE WHEN date.IsLeapYear( y ) THEN 366 ELSE 365 END
  END

  RETURN date.YearDay( cycle * 400 + y, d + 1 )
END

The auxiliary function date.IsLeapYear:

CREATE FUNCTION [date].[IsLeapYear]( y int ) RETURNS bool AS
BEGIN
  RETURN y % 4 = 0 AND ( y % 100 != 0 OR y % 400 = 0 )
END

and date.YearDay :

CREATE FUNCTION [date].[YearMonthDay]( year int, month int, day int ) RETURNS int AS
BEGIN
  RETURN year * 512 + month * 32 + day
END

Conversion from Year/Day to Year/Month/Day:

CREATE FUNCTION [date].[YearDayToYearMonthDay]( yd int ) returns int AS
BEGIN
  DECLARE y int, d int, leap bool, fdm int, m int, dim int

  SET y = yd / 512
  SET d = yd % 512 - 1

  SET leap = date.IsLeapYear( y )

  -- Jan = 0..30, Feb = 0..27 or 0..28  
  IF NOT leap AND d >= 59 SET d = d + 1

  SET fdm = CASE 
    WHEN d < 31 THEN 0 -- Jan
    WHEN d < 60 THEN 31 -- Feb
    WHEN d < 91 THEN 60 -- Mar
    WHEN d < 121 THEN 91 -- Apr
    WHEN d < 152 THEN 121 -- May
    WHEN d < 182 THEN 152 -- Jun
    WHEN d < 213 THEN 182 -- Jul
    WHEN d < 244 THEN 213 -- Aug
    WHEN d < 274 THEN 244 -- Sep
    WHEN d < 305 THEN 274 -- Oct
    WHEN d < 335 THEN 305 -- Nov
    ELSE 335 -- Dec
    END

  SET dim = d - fdm

  SET m = ( d - dim + 28 ) / 31

  RETURN date.YearMonthDay( y, m+1, dim+1 )
END

The auxiliary function date.YearMonthDay:

CREATE FUNCTION [date].[YearMonthDay]( year int, month int, day int ) RETURNS int AS
BEGIN
  RETURN year * 512 + month * 32 + day
END

Finally conversion from Year/Month/Day to Year/Day:

CREATE FUNCTION [date].[YearMonthDayToYearDay]( ymd int ) RETURNS int AS
BEGIN
  DECLARE y int, m int, d int

  -- Extract y, m, d from ymd
  SET d = ymd % 32, m = ymd / 32  
  SET y = m / 16, m = m % 16

  -- Incorporate m into d ( assuming Feb has 29 days ).
  SET d = d + CASE
    WHEN m = 1 THEN 0 -- Jan
    WHEN m = 2 THEN 31 -- Feb
    WHEN m = 3 THEN 60 -- Mar
    WHEN m = 4 THEN 91 -- Apr
    WHEN m = 5 THEN 121 -- May
    WHEN m = 6 THEN 152 -- Jun
    WHEN m = 7 THEN 182 -- Jul
    WHEN m = 8 THEN 213 -- Aug
    WHEN m = 9 THEN 244 -- Sep
    WHEN m = 10 THEN 274 -- Oct
    WHEN m = 11 THEN 305 -- Nov
    ELSE 335 -- Dec
    END

  -- Allow for Feb being only 28 days in a non-leap-year.
  IF m >= 3 AND NOT date.IsLeapYear( y ) SET d = d - 1

  RETURN date.YearDay( y, d )
END

I hope it's useful to someone, and I hope it's correct - I have tested by generating test ranges of days to check the generated calendar looks correct, with the correct number of days in each month, especially February. The functions do not check whether the input is valid, that is assumed. This is part of a project to implement SQL in C#.