A gas-efficient Solidity date and time library.
Instead of using loops and lookup tables, this date conversions library uses formulae to convert year/month/day hour:minute:second to a Unix timestamp and back.
See BokkyPooBah’s Gas-Efficient Solidity DateTime Library for more explanations.
Thank you to Alex Kampa, James Zaki and Adrian Guerrera for helping to validate this library. Thanks also to Oleksii Matiiasevych for asking about leap seconds.
- History
- Bug Bounty Scope And Donations
- Deployment
- Questions And Answers
- Conventions
- Functions
- _daysFromDate
- _daysToDate
- timestampFromDate
- timestampFromDateTime
- timestampToDate
- timestampToDateTime
- isValidDate
- isValidDateTime
- isLeapYear
- _isLeapYear
- isWeekDay
- isWeekEnd
- getDaysInMonth
- _getDaysInMonth
- getDayOfWeek
- getYear
- getMonth
- getDay
- getHour
- getMinute
- getSecond
- addYears
- addMonths
- addDays
- addHours
- addMinutes
- addSeconds
- subYears
- subMonths
- subDays
- subHours
- subMinutes
- subSeconds
- diffYears
- diffMonths
- diffDays
- diffHours
- diffMinutes
- diffSeconds
- Gas Cost
- Algorithm
- Testing
- References
| Version | Date | Notes |
|---|---|---|
| v1.00-pre-release | May 25 2018 | "Rarefaction" pre-release. I'm currently trying to get this library audited, so don't use in production mode yet. |
| v1.00-pre-release-a | Jun 2 2018 | "Rarefaction" pre-release a. Added the contracts/BokkyPooBahsDateTimeContract.sol wrapper for convenience. Alex Kampa conducted a range of tests on the library. |
| v1.00-pre-release-b | Jun 4 2018 | "Rarefaction" pre-release b. Replaced public function with internal for easier EtherScan verification - a83e13b. Deployed contracts/BokkyPooBahsDateTimeContract.sol with the inlined contracts/BokkyPooBahsDateTimeLibrary.sol to the Ropsten network at address 0x07239bb079094481bfaac91ca842426860021aaa |
| v1.00-pre-release-c | June 8 2018 | "Rarefaction" pre-release c. Added require(year >= 1970) to _daysFromDate(...) in 4002b27 as highlighted in James Zaki's audit |
| v1.00-pre-release-d | Sep 1 2018 | "Rarefaction" pre-release d. Added isValidDate(...) and isValidDateTime(...) in 380061b as highlighted in Adrian Guerrera's audit |
| v1.00 | Sep 2 2018 | "Rarefaction" release |
| v1.01 | Feb 14 2019 | "Notoryctes" release. Upgraded contracts to Solidity 0.5.x. Updated to MIT Licence |
| v1.01 | Feb 17 2019 | Bug bounty added |
Details of the bug bounty program for this project can be found at BokkyPooBah's Hall Of Fame And Bug Bounties. Please consider donating to support the bug bounty, and the development and maintenance of decentralised applications.
The scope of the bug bounty for this project follows:
v1.00 of the source code for BokkyPooBahsDateTimeContract.sol and TestDateTime.sol has been deployed to:
The Ropsten network:
- BokkyPooBahsDateTimeContract.sol at 0x947cc35992e6723de50bf704828a01fd2d5d6641
- TestDateTime.sol at 0xa068fe3e029a972ecdda2686318806d2b19875d1
Mainnet:
- BokkyPooBahsDateTimeContract.sol at 0x23d23d8f243e57d0b924bff3a3191078af325101
- TestDateTime.sol at 0x78f96b2d5f717fa9ad416957b79d825cc4cce69d
For each of the deployed contracts above, you can click on the Read Contract tab to test out the date/time/timestamp functions.
User /u/_dredge asked the following questions:
Some Muslim countries have a Friday/Saturday weekend. Workday(1-7) may be more useful.
I presume leap seconds and such details are taken care of in the Unix timecode.
Some additional ideas for functions below.
Quarter calculations
weekNumber(1-53)
Complete years / months / weeks / days
Nearest years / months / weeks / days
Regarding regions and systems where Friday/Saturday are weekends, please use the function getDayOfWeek(timestamp) that returns 1 (= Monday, ..., 7 (= Sunday) to determine whether you should treat a day as a weekday or weekend.
See the next question regarding the leap seconds.
Regarding the additional ideas, thanks!
Asked by Oleksii Matiiasevych and /u/_dredge above.
For example, a leap second was inserted on Jan 01 1999.
From the first answer to Unix time and leap seconds:
The number of seconds per day are fixed with Unix timestamps.
The Unix time number is zero at the Unix epoch, and increases by exactly 86400 per day since the epoch.
So it cannot represent leap seconds. The OS will slow down the clock to accommodate for this. The leap seconds is simply not existent as far a Unix timestamps are concerned.
And from the second answer to Unix time and leap seconds:
Unix time is easy to work with, but some timestamps are not real times, and some timestamps are not unique times.
That is, there are some duplicate timestamps representing two different seconds in time, because in unix time the sixtieth second might have to repeat itself (as there can't be a sixty-first second). Theoretically, they could also be gaps in the future because the sixtieth second doesn't have to exist, although no skipping leap seconds have been issued so far.
Rationale for unix time: it's defined so that it's easy to work with. Adding support for leap seconds to the standard libraries is very tricky. ...
This library aims to replicate the Unix time functionality and assumes that leap seconds are handled by the underlying operating system.
Asked by Adrian Guerrera.
2345 is just an arbitrary number chosen for the year limit to test to. The algorithms should still work beyond this date.
Asked by Adrian Guerrera. Specifically, the functions _daysFromDate, timestampFromDate and timestampFromDateTime.
The date and time inputs should be validated before the values are passed to these functions. The validation functions isValidDate(...) and isValidDateTime(...) have now been added for this purpose.
This library provides a cheap conversion between the timestamp and year/month/day hour:minute:second formats. There is no requirement for this library to store a packed structure to represent a DateTime as this data can be stored as a uint256 and converted on the fly to year/month/day hour:minute:second.
The formulae for converting between a timestamp and year/month/day hour:minute:second format uses a mathematically simple algorithm without any loops. The gas cost is relatively constant (as there are no loops) and the mathematical computations are relative cheap (compared to using loops and looking up data from storage).
Most likely. The aim of this first version is for the conversions to be computed correctly.
From Gas Cost, timestampToDateTime(…) has an execution gas cost of 3,101 gas, and timestampFromDateTime(…) has an execution gas cost of 2,566 gas.
All dates, times and Unix timestamps are UTC.
| Unit | Range | Notes |
|---|---|---|
| timestamp | >= 0 | Unix timestamp, number of seconds since 1970/01/01 00:00:00 UTC |
| year | 1970 ... 2345 | |
| month | 1 ... 12 | |
| day | 1 ... 31 | |
| hour | 0 ... 23 | |
| minute | 0 ... 59 | |
| second | 0 ... 59 | |
| dayOfWeek | 1 ... 7 | 1 = Monday, ..., 7 = Sunday |
| year/month/day | 1970/01/01 ... 2345/12/31 |
_days, _months and _years variable names are _-prefixed as the non-prefixed versions are reserved words in Solidity.
All functions operate on the uint timestamp data type, except for functions prefixed with _.
Calculate the number of days _days from 1970/01/01 to year/month/day.
function_daysFromDate(uintyear,uintmonth,uintday)publicpurereturns(uint_days)NOTE This function does not validate the year/month/day input. Use isValidDate(...) to validate the input if necessary.
Calculate year/month/day from the number of days _days since 1970/01/01 .
function_daysToDate(uint_days)publicpurereturns(uintyear,uintmonth,uintday)Calculate the timestamp to year/month/day.
functiontimestampFromDate(uintyear,uintmonth,uintday)publicpurereturns(uinttimestamp)NOTE This function does not validate the year/month/day input. Use isValidDate(...) to validate the input if necessary.
Calculate the timestamp to year/month/dayhour:minute:second UTC.
functiontimestampFromDateTime(uintyear,uintmonth,uintday,uinthour,uintminute,uintsecond)publicpurereturns(uinttimestamp)NOTE This function does not validate the year/month/dayhour:minute:second input. Use isValidDateTime(...) to validate the input if necessary.
Calculate year/month/day from timestamp.
functiontimestampToDate(uinttimestamp)publicpurereturns(uintyear,uintmonth,uintday)Calculate year/month/dayhour:minute:second from timestamp.
functiontimestampToDateTime(uinttimestamp)publicpurereturns(uintyear,uintmonth,uintday,uinthour,uintminute,uintsecond)Is the date specified by year/month/day a valid date?
function isValidDate(uint year, uint month, uint day) internal pure returns (bool valid)
Is the date/time specified by year/month/dayhour:minute:second a valid date/time?
function isValidDateTime(uint year, uint month, uint day, uint hour, uint minute, uint second) internal pure returns (bool valid)
Is the year specified by timestamp a leap year?
function isLeapYear(uint timestamp) public pure returns (bool leapYear)
Is the specified year (e.g. 2018) a leap year?
function _isLeapYear(uint year) public pure returns (bool leapYear)
Is the day specified by timestamp a weekday (Monday, ..., Friday)?
functionisWeekDay(uinttimestamp)publicpurereturns(boolweekDay)Is the day specified by timestamp a weekend (Saturday, Sunday)?
functionisWeekEnd(uinttimestamp)publicpurereturns(boolweekEnd)Return the day in the month daysInMonth for the month specified by timestamp.
functiongetDaysInMonth(uinttimestamp)publicpurereturns(uintdaysInMonth)Return the day in the month daysInMonth (1, ..., 31) for the month specified by the year/month.
function_getDaysInMonth(uintyear,uintmonth)publicpurereturns(uintdaysInMonth)Return the day of the week dayOfWeek (1 = Monday, ..., 7 = Sunday) for the date specified by timestamp.
functiongetDayOfWeek(uinttimestamp)publicpurereturns(uintdayOfWeek)Get the year of the date specified by timestamp.
functiongetYear(uinttimestamp)publicpurereturns(uintyear)Get the month of the date specified by timestamp.
functiongetMonth(uinttimestamp)publicpurereturns(uintmonth)Get the day of the month day (1, ..., 31) of the date specified timestamp.
functiongetDay(uinttimestamp)publicpurereturns(uintday)Get the hour of the date and time specified by timestamp.
functiongetHour(uinttimestamp)publicpurereturns(uinthour)Get the minute of the date and time specified by timestamp.
functiongetMinute(uinttimestamp)publicpurereturns(uintminute)Get the second of the date and time specified by timestamp.
functiongetSecond(uinttimestamp)publicpurereturns(uintsecond)Add _years years to the date and time specified by timestamp.
Note that the resulting day of the month will be adjusted if it exceeds the valid number of days in the month. For example, if the original date is 2020/02/29 and an additional year is added to this date, the resulting date will be an invalid date of 2021/02/29. The resulting date is then adjusted to 2021/02/28.
functionaddYears(uinttimestamp,uint_years)publicpurereturns(uintnewTimestamp)Add _months months to the date and time specified by timestamp.
Note that the resulting day of the month will be adjusted if it exceeds the valid number of days in the month. For example, if the original date is 2019/01/31 and an additional month is added to this date, the resulting date will be an invalid date of 2019/02/31. The resulting date is then adjusted to 2019/02/28.
functionaddMonths(uinttimestamp,uint_months)publicpurereturns(uintnewTimestamp)Add _days days to the date and time specified by timestamp.
functionaddDays(uinttimestamp,uint_days)publicpurereturns(uintnewTimestamp)Add _hours hours to the date and time specified by timestamp.
functionaddHours(uinttimestamp,uint_hours)publicpurereturns(uintnewTimestamp)Add _minutes minutes to the date and time specified by timestamp.
functionaddMinutes(uinttimestamp,uint_minutes)publicpurereturns(uintnewTimestamp)Add _seconds seconds to the date and time specified by timestamp.
functionaddSeconds(uinttimestamp,uint_seconds)publicpurereturns(uintnewTimestamp)Subtract _years years from the date and time specified by timestamp.
Note that the resulting day of the month will be adjusted if it exceeds the valid number of days in the month. For example, if the original date is 2020/02/29 and a year is subtracted from this date, the resulting date will be an invalid date of 2019/02/29. The resulting date is then adjusted to 2019/02/28.
functionsubYears(uinttimestamp,uint_years)publicpurereturns(uintnewTimestamp)Subtract _months months from the date and time specified by timestamp.
Note that the resulting day of the month will be adjusted if it exceeds the valid number of days in the month. For example, if the original date is 2019/03/31 and a month is subtracted from this date, the resulting date will be an invalid date of 2019/02/31. The resulting date is then adjusted to 2019/02/28.
functionsubMonths(uinttimestamp,uint_months)publicpurereturns(uintnewTimestamp)Subtract _days days from the date and time specified by timestamp.
functionsubDays(uinttimestamp,uint_days)publicpurereturns(uintnewTimestamp)Subtract _hours hours from the date and time specified by timestamp.
functionsubHours(uinttimestamp,uint_hours)publicpurereturns(uintnewTimestamp)Subtract _minutes minutes from the date and time specified by timestamp.
functionsubMinutes(uinttimestamp,uint_minutes)publicpurereturns(uintnewTimestamp)Subtract _seconds seconds from the date and time specified by timestamp.
functionsubSeconds(uinttimestamp,uint_seconds)publicpurereturns(uintnewTimestamp)Calculate the number of years between the dates specified by fromTimeStamp and toTimestamp.
Note that this calculation is computed as getYear(toTimestamp) - getYear(fromTimestamp), rather that subtracting the years (since 1970/01/01) represented by both {to|from}Timestamp.
functiondiffYears(uintfromTimestamp,uinttoTimestamp)publicpurereturns(uint_years)Calculate the number of months between the dates specified by fromTimeStamp and toTimestamp.
Note that this calculation is computed as getYear(toTimestamp) * 12 + getMonth(toTimestamp) - getYear(fromTimestamp) * 12 - getMonth(fromTimestamp), rather that subtracting the months (since 1970/01/01) represented by both {to|from}Timestamp.
functiondiffMonths(uintfromTimestamp,uinttoTimestamp)publicpurereturns(uint_months)Calculate the number of days between the dates specified by fromTimeStamp and toTimestamp.
Note that this calculation is computed as (toTimestamp - fromTimestamp) / SECONDS_PER_DAY, rather that subtracting the days (since 1970/01/01) represented by both {to|from}Timestamp.
functiondiffDays(uintfromTimestamp,uinttoTimestamp)publicpurereturns(uint_days)Calculate the number of hours between the dates specified by fromTimeStamp and toTimestamp.
Note that this calculation is computed as (toTimestamp - fromTimestamp) / SECONDS_PER_HOUR, rather that subtracting the hours (since 1970/01/01) represented by both {to|from}Timestamp.
functiondiffHours(uintfromTimestamp,uinttoTimestamp)publicpurereturns(uint_hours)Calculate the number of minutes between the dates specified by fromTimeStamp and toTimestamp.
Note that this calculation is computed as (toTimestamp - fromTimestamp) / SECONDS_PER_MINUTE, rather that subtracting the minutes (since 1970/01/01) represented by both {to|from}Timestamp.
functiondiffMinutes(uintfromTimestamp,uinttoTimestamp)publicpurereturns(uint_minutes)Calculate the number of seconds between the dates specified by fromTimeStamp and toTimestamp.
Note that this calculation is computed as toTimestamp - fromTimestamp.
functiondiffSeconds(uintfromTimestamp,uinttoTimestamp)publicpurereturns(uint_seconds)From executing the following function, the transaction gas cost is 24,693
>testDateTime.timestampToDateTime(1527120000)[2018,5,24,0,0,0]>testDateTime.timestampToDateTime.estimateGas(1527120000)24693From Remix, the execution gas cost is 3,101 .
From my latest testing with Remix using Solidity 0.4.24:
From executing the following function, the transaction gas cost is 25,054
>testDateTime.timestampFromDateTime(2018,05,24,1,2,3)1527123723>testDateTime.timestampFromDateTime.estimateGas(2018,05,24,1,2,3)25054From Remix, the execution gas cost is 2,566 .
From my latest testing with Remix using Solidity 0.4.24:
Remix gas estimates using Solidity 0.4.24:
{
"Creation": {
"codeDepositCost": "908400",
"executionCost": "942",
"totalCost": "909342"
},
"External": {
"DOW_FRI()": "1130",
"DOW_MON()": "1262",
"DOW_SAT()": "1064",
"DOW_SUN()": "360",
"DOW_THU()": "1108",
"DOW_TUE()": "250",
"DOW_WED()": "778",
"OFFSET19700101()": "932",
"SECONDS_PER_DAY()": "690",
"SECONDS_PER_HOUR()": "580",
"SECONDS_PER_MINUTE()": "1196",
"_daysFromDate(uint256,uint256,uint256)": "638",
"_daysToDate(uint256)": "1280",
"_getDaysInMonth(uint256,uint256)": "885",
"_isLeapYear(uint256)": "1115",
"_now()": "997",
"_nowDateTime()": "1562",
"addDays(uint256,uint256)": "772",
"addHours(uint256,uint256)": "662",
"addMinutes(uint256,uint256)": "838",
"addMonths(uint256,uint256)": "1851",
"addSeconds(uint256,uint256)": "896",
"addYears(uint256,uint256)": "1846",
"diffDays(uint256,uint256)": "1207",
"diffHours(uint256,uint256)": "503",
"diffMinutes(uint256,uint256)": "316",
"diffMonths(uint256,uint256)": "infinite",
"diffSeconds(uint256,uint256)": "712",
"diffYears(uint256,uint256)": "infinite",
"getDay(uint256)": "1114",
"getDayOfWeek(uint256)": "415",
"getDaysInMonth(uint256)": "1120",
"getHour(uint256)": "491",
"getMinute(uint256)": "1373",
"getMonth(uint256)": "1378",
"getSecond(uint256)": "810",
"getYear(uint256)": "1337",
"isLeapYear(uint256)": "1633",
"isValidDate(uint256,uint256,uint256)": "942",
"isValidDateTime(uint256,uint256,uint256,uint256,uint256,uint256)": "1269",
"isWeekDay(uint256)": "1284",
"isWeekEnd(uint256)": "624",
"subDays(uint256,uint256)": "1146",
"subHours(uint256,uint256)": "288",
"subMinutes(uint256,uint256)": "992",
"subMonths(uint256,uint256)": "2363",
"subSeconds(uint256,uint256)": "1336",
"subYears(uint256,uint256)": "1871",
"timestampFromDate(uint256,uint256,uint256)": "718",
"timestampFromDateTime(uint256,uint256,uint256,uint256,uint256,uint256)": "1077",
"timestampToDate(uint256)": "infinite",
"timestampToDateTime(uint256)": "1945"
}
}The formulae to convert year/month/day hour:minute:second to a Unix timestamp and back use the algorithms from Converting Between Julian Dates and Gregorian Calendar Dates. These algorithms were originally designed by Fliegel and van Flandern (1968).
Note that these algorithms depend on negative numbers, so Solidity unsigned integers uint are converted to signed integers int to compute the date conversions and the results are converted back to uint for general use.
The Fortran algorithm follows:
INTEGER FUNCTION JD (YEAR,MONTH,DAY)
C
C---COMPUTES THE JULIAN DATE (JD) GIVEN A GREGORIAN CALENDAR
C DATE (YEAR,MONTH,DAY).
C
INTEGER YEAR,MONTH,DAY,I,J,K
C
I= YEAR
J= MONTH
K= DAY
C
JD= K-32075+1461*(I+4800+(J-14)/12)/4+367*(J-2-(J-14)/12*12)
2 /12-3*((I+4900+(J-14)/12)/100)/4
C
RETURN
END
Translating this formula, and subtracting an offset (2,440,588) so 1970/01/01 is day 0:
days = day
- 32075
+ 1461 * (year + 4800 + (month - 14) / 12) / 4
+ 367 * (month - 2 - (month - 14) / 12 * 12) / 12
- 3 * ((year + 4900 + (month - 14) / 12) / 100) / 4
- offset
The Fortran algorithm follows:
SUBROUTINE GDATE (JD, YEAR,MONTH,DAY)
C
C---COMPUTES THE GREGORIAN CALENDAR DATE (YEAR,MONTH,DAY)
C GIVEN THE JULIAN DATE (JD).
C
INTEGER JD,YEAR,MONTH,DAY,I,J,K
C
L= JD+68569
N= 4*L/146097
L= L-(146097*N+3)/4
I= 4000*(L+1)/1461001
L= L-1461*I/4+31
J= 80*L/2447
K= L-2447*J/80
L= J/11
J= J+2-12*L
I= 100*(N-49)+I+L
C
YEAR= I
MONTH= J
DAY= K
C
RETURN
END
Translating this formula and adding an offset (2,440,588) so 1970/01/01 is day 0:
int L = days + 68569 + offset
int N = 4 * L / 146097
L = L - (146097 * N + 3) / 4
year = 4000 * (L + 1) / 1461001
L = L - 1461 * year / 4 + 31
month = 80 * L / 2447
dd = L - 2447 * month / 80
L = month / 11
month = month + 2 - 12 * L
year = 100 * (N - 49) + year + L
Details of the testing environment can be found in test.
The DateTime library calculations have been tested for the date range 1970/01/01 to 2345/12/01 for periodically sampled dates.
The following functions were tested using the script test/01_test1.sh with the summary results saved in test/test1results.txt and the detailed output saved in test/test1output.txt:
- Deploy contracts/BokkyPooBahsDateTimeLibrary.sol library
- Deploy contracts/TestDateTime.sol contract
- Test
isValidDate(...) - Test
isValidDateTime(...) - Test
isLeapYear(...) - Test
_isLeapYear(...) - Test
isWeekDay(...) - Test
isWeekEnd(...) - Test
getDaysInMonth(...) - Test
_getDaysInMonth(...) - Test
getDayOfWeek(...) - Test
get{Year|Month|Day|Hour|Minute|Second}(...) - Test
add{Years|Months|Days|Hours|Minutes|Seconds}(...) - Test
sub{Years|Months|Days|Hours|Minutes|Seconds}(...) - Test
diff{Years|Months|Days|Hours|Minutes|Seconds}(...) - For a range of Unix timestamps from 1970/01/01 to 2345/12/21
- Generate the year/month/day hour/minute/second from the Unix timestamp using
timestampToDateTime(...) - Generate the Unix timestamp from the calculated year/month/day hour/minute/second using
timestampFromDateTime(...) - Compare the year/month/day hour/minute/second to the JavaScript Date calculation
- Generate the year/month/day hour/minute/second from the Unix timestamp using
A copy of the webpage with the algorithm Converting Between Julian Dates and Gregorian Calendar Dates has been saved to docs/ConvertingBetweenJulianDatesAndGregorianCalendarDates.pdf as some people have had difficulty accessing this page.
Enjoy!
(c) BokkyPooBah / Bok Consulting Pty Ltd - Feb 17 2019. The MIT Licence.


