forked from OpenZeppelin/openzeppelin-contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Datetime utilities (OpenZeppelin#1531)
Add date and datetime utility functions to convert from civil date time to Unix time.
- Loading branch information
1 parent
cc1c180
commit aa6f9cf
Showing
4 changed files
with
248 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
import "../utils/Datetime.sol"; | ||
|
||
contract DatetimeMock { | ||
function date( | ||
int32 _year, | ||
uint8 _month, | ||
uint8 _day | ||
) external pure returns (int256) { | ||
return Datetime.date(_year, _month, _day); | ||
} | ||
|
||
function datetime( | ||
int32 _year, | ||
uint8 _month, | ||
uint8 _day, | ||
uint8 _hour, | ||
uint8 _minute, | ||
uint8 _second | ||
) public pure returns (int256) { | ||
return Datetime.datetime(_year, _month, _day, _hour, _minute, _second); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
// SPDX-License-Identifier: MIT | ||
// OpenZeppelin Contracts v4.4.1 (utils/Datetime.sol) | ||
|
||
pragma solidity ^0.8.0; | ||
|
||
/// @title Datetime utility functions | ||
/// @author Seth Charles (@lesserhatch) | ||
/// @dev All function calls are currently implemented without side effects | ||
library Datetime { | ||
/// @notice Convert civil date to Unix time | ||
/// @param _year Civil year where negative years are prior to 0 A.D. | ||
/// @param _month Numeric month representation in the range [1, 12] | ||
/// @param _day Day of month | ||
/// @dev Invalid dates will revert | ||
/// @return Number of seconds elapsed since the Unix epoch | ||
function date( | ||
int32 _year, | ||
uint32 _month, | ||
uint32 _day | ||
) public pure returns (int256) { | ||
_validDate(_year, _month, _day); | ||
return _daysFromCivil(_year, _month, _day) * 1 days; | ||
} | ||
|
||
/// @notice Convert civil date and time to Unix time | ||
/// @param _year Civil year where negative years are prior to 0 A.D. | ||
/// @param _month Numeric month representation in the range [1, 12] | ||
/// @param _day Day of month | ||
/// @param _hour Hour of day in the range [0, 23] | ||
/// @param _minute Minute of hour in the range [0, 59] | ||
/// @param _second Second of minute in the range of [0, 59] | ||
/// @dev Invalid dates and times will revert | ||
/// @return Number of seconds elapsed since the Unix epoch | ||
function datetime( | ||
int32 _year, | ||
uint32 _month, | ||
uint32 _day, | ||
uint32 _hour, | ||
uint32 _minute, | ||
uint32 _second | ||
) public pure returns (int256) { | ||
_validTime(_hour, _minute, _second); | ||
return | ||
date(_year, _month, _day) + | ||
int256(int32(_hour * 1 hours)) + | ||
int256(int32(_minute * 1 minutes)) + | ||
int256(int32(_second * 1 seconds)); | ||
} | ||
|
||
function _daysFromCivil( | ||
int256 _year, | ||
uint256 _month, | ||
uint256 _day | ||
) private pure returns (int256) { | ||
// Inspired by the days_from_civil algorithm at https://howardhinnant.github.io/date_algorithms.html | ||
if (_month <= 2) { | ||
_year = _year - 1; | ||
} | ||
|
||
int256 era; | ||
if (_year >= 0) { | ||
era = _year; | ||
} else { | ||
era = _year - 399; | ||
} | ||
era = era / 400; | ||
|
||
uint256 yoe = uint256(_year - era * 400); | ||
|
||
uint256 doy = 153; | ||
if (_month > 2) { | ||
doy = doy * (_month - 3); | ||
} else { | ||
doy = doy * (_month + 9); | ||
} | ||
doy = (doy + 2) / 5 + (_day - 1); | ||
|
||
uint256 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; | ||
|
||
return era * 146097 + int256(doe) - 719468; | ||
} | ||
|
||
function _isLeapYear(int32 _year) private pure returns (bool) { | ||
return (_year % 4 == 0 && (_year % 100 != 0 || _year % 400 == 0)); | ||
} | ||
|
||
function _validDate( | ||
int32 _year, | ||
uint32 _month, | ||
uint32 _day | ||
) private pure { | ||
require(_month > 0 && _month <= 12, "Month must be in [1, 12]"); | ||
require(_day > 0 && _day <= 31, "Invalid day"); | ||
if (_month == 2) { | ||
if (_isLeapYear(_year)) { | ||
require(_day <= 29, "Invalid day"); | ||
} else { | ||
require(_day <= 28, "Invalid day"); | ||
} | ||
} | ||
if ((_month < 8 && _month % 2 == 0) || (_month >= 8 && _month % 2 == 1)) { | ||
require(_day <= 30, "Invalid day"); | ||
} | ||
} | ||
|
||
function _validTime( | ||
uint32 _hour, | ||
uint32 _minute, | ||
uint32 _second | ||
) private pure { | ||
require(_hour < 24, "Invalid hour"); | ||
require(_minute < 60, "Invalid minute"); | ||
require(_second < 60, "Invalid second"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
const { expectRevert } = require('@openzeppelin/test-helpers'); | ||
const { expect } = require('chai'); | ||
|
||
const Datetime = artifacts.require('Datetime'); | ||
const DatetimeMock = artifacts.require('DatetimeMock'); | ||
|
||
const seconds = 1; | ||
const minutes = 60 * seconds; | ||
const hours = 60 * minutes; | ||
const days = 24 * hours; | ||
|
||
contract('Datetime', function () { | ||
before(async function () { | ||
const datetimeLib = await Datetime.new(); | ||
await DatetimeMock.link(datetimeLib); | ||
}); | ||
|
||
beforeEach(async function () { | ||
this.datetime = await DatetimeMock.new(); | ||
}); | ||
|
||
describe('leap day', function () { | ||
it('year not divisible by 4 is not a leap year', async function () { | ||
await expectRevert(this.datetime.date(1970, 2, 29), 'Invalid day'); | ||
}); | ||
|
||
it('test first leap day after unix epoch', async function () { | ||
expect(await this.datetime.date(1972, 2, 29)).to.be.bignumber.equal(`${(365 + 365 + 31 + 28) * days}`); | ||
expect(await this.datetime.date(1972, 3, 1)).to.be.bignumber.equal(`${(365 + 365 + 31 + 29) * days}`); | ||
}); | ||
|
||
it('year divisible by 400 is a leap year', async function () { | ||
expect(await this.datetime.date(2000, 2, 29)).to.be.bignumber.equal('951782400'); | ||
}); | ||
|
||
it('year divisible by 100 is not a leap year', async function () { | ||
await expectRevert(this.datetime.date(2100, 2, 29), 'Invalid day'); | ||
}); | ||
}); | ||
|
||
describe('invalid dates and times', function () { | ||
it('cannot have more than 24 hours in a day', async function () { | ||
await expectRevert(this.datetime.datetime(1970, 1, 1, 25, 0, 0), 'Invalid hour'); | ||
}); | ||
|
||
it('cannot have more than 60 minutes in an hour', async function () { | ||
await expectRevert(this.datetime.datetime(1970, 1, 1, 0, 60, 0), 'Invalid minute'); | ||
}); | ||
|
||
it('cannot have more than 60 seconds in a minute', async function () { | ||
await expectRevert(this.datetime.datetime(1970, 1, 1, 0, 0, 60), 'Invalid second'); | ||
}); | ||
|
||
it('months cannot have extra days', async function () { | ||
await expectRevert(this.datetime.date(1970, 1, 32), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 2, 29), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 3, 32), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 4, 31), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 5, 32), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 6, 31), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 7, 32), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 8, 32), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 9, 31), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 10, 32), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 11, 31), 'Invalid day'); | ||
await expectRevert(this.datetime.date(1970, 12, 32), 'Invalid day'); | ||
}); | ||
}); | ||
|
||
describe('2038 bug', function () { | ||
it('2038-01-19 3:14:07 is Y2K38', async function () { | ||
expect(await this.datetime.datetime(2038, 1, 19, 3, 14, 7)).to.be.bignumber.equal('2147483647'); | ||
}); | ||
|
||
it('2038-01-19 3:14:08 is 2**31', async function () { | ||
expect(await this.datetime.datetime(2038, 1, 19, 3, 14, 8)).to.be.bignumber.equal('2147483648'); | ||
}); | ||
}); | ||
}); |