I am working on a batch file for doing date/time maths. One challenge is to determine the (proleptic) Gregorian year from the given number of days that have elapsed since day zero (that is, 1st of January, 0 AD, 00:00:00 o'clock), particularly when regarding leap years.
As per definition, a year is considered as leap year when its number is a multiple of four, but it is not when its number is a multiple of 100, unless its number is a multiple of 400. This results in an average duration of a year of 365 97/400 = 365.2425 days.
However, simply dividing the number of days by 365.2425 and doing some rounding does not result in the correct result for the year. For example, the number of days 695421
represents the 1st of January 1904, so the expected year is 1904
, but performing an integer division like floor(695421 / 365.2425)
results in 1903
.
Therefore I developed an iterative algorithm that loops through years, beginning at year 0, computing the number of days per year regarding leap years, subtracting that number from the given number of days and incrementing a year counter, until the remaining number of days is less than the number of days of the currently iterated year, upon which the loop is left.
Here is the code of the routine called :CALC_YEARS
-- including numerous explanatory remarks (the //
and /*
/*/
styles are used just for nice syntax highlighting here on this site). Note that the :MAIN_LOOP
section is just there for convenient execution, so it is not subject to review:
@echo off
setlocal EnableExtensions DisableDelayedExpansion
:MAIN_LOOP
echo/
set "NUMDAYS="
set /P NUMDAYS="Number of days: "
if not defined NUMDAYS goto :QUIT_LOOP
call :CALC_YEARS YEAR YEARDAYS REST %NUMDAYS%
echo Gregorian year: %YEAR% ^(consisting of %YEARDAYS% days^)
echo Remaining days: %REST%
goto :MAIN_LOOP
:QUIT_LOOP
endlocal
exit /B
:CALC_YEARS rtn_year rtn_year_days rtn_rest_days val_days
::/*
::This routine calculates the (proleptic) Gregorian year from the given number
::of days elapsed since day zero.
::Parameters:
:: rtn_year variable that holds the returned year
:: rtn_year_days variable that holds the number of days of the resulting year
:: rtn_rest_days variable that holds the remaining days
:: val_days number of days to convert to year
::*/
setlocal DisableDelayedExpansion
rem // Validate arguments and convert them to numbers:
set "DAYS=%~4"
if not defined DAYS (
>&2 echo ERROR: too few arguments given!
exit /B 1
)
set /A "DAYS+=0"
if %DAYS% LSS 0 (
>&2 echo ERROR: number of days out of range!
exit /B 1
)
if %DAYS% GEQ 3652425 (
>&2 echo ERROR: number of days out of range!
exit /B 1
)
rem // Estimate the year (by dividing days by 365):
setlocal EnableDelayedExpansion
set /A "YLIM=DAYS/365"
rem // Reset year counter and loop through years:
set /A "YEAR=0, YDAY=365+1"
for /L %%I in (0,1,%YLIM%) do (
rem /* Determine number of days of the iterated year
rem (hence checking whether it is a leap year): */
set /A "Y004=%%I/4*4, Y100=%%I/100*100, Y400=%%I/400*400"
set /A "YDAY=365"
if %%I EQU !Y400! (
set /A "YDAY+=1"
) else if %%I EQU !Y004! if %%I NEQ !Y100! (
set /A "YDAY+=1"
)
rem /* Check whether the given number of days is greater than the number
rem of days of the iterated year (thus whether enough days are left): */
if !DAYS! GEQ !YDAY! (
rem /* Enough days left, so subtract days of iterated year from days
rem and increment the year counter: */
set /A "DAYS-=YDAY, YEAR+=1"
) else (
rem // Not enough days left, hence leave loop:
goto :STOP_YEAR
)
)
:STOP_YEAR
rem // Return resulting values and terminate routine:
(
endlocal
endlocal
set "%~1=%YEAR%"
set "%~2=%YDAY%"
set "%~3=%DAYS%"
)
exit /B
Besides general coding styles, I am particularly looking for ways to improve the performance of the :CALC_YEARS
routine. Any suggestions concerning completely different algorithms are welcome, especially in case of non-iterative ones, as long as they are pure batch solutions.
As soon as I find a better approach for this myself (and I will, hopefully!), I am going to post it here as well (likely as an answer).