Skip to content

Commit

Permalink
Add Datetime utilities (OpenZeppelin#1531)
Browse files Browse the repository at this point in the history
Add date and datetime utility functions to convert from civil date
time to Unix time.
  • Loading branch information
lesserhatch committed Mar 10, 2022
1 parent cc1c180 commit aa6f9cf
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 3 deletions.
26 changes: 26 additions & 0 deletions contracts/mocks/DatetimeMock.sol
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);
}
}
115 changes: 115 additions & 0 deletions contracts/utils/Datetime.sol
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");
}
}
31 changes: 28 additions & 3 deletions docs/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Want to keep track of some numbers that increment by 1 every time you want anoth

=== Base64

xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation.
xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation.

This is specially useful to build URL-safe tokenURIs for both xref:api:token/ERC721.adoc#IERC721Metadata-tokenURI-uint256-[`ERC721`] or xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`ERC1155`]. This library provides a clever way to serve URL-safe https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/[Data URI] compliant strings to serve on-chain data structures.

Expand All @@ -120,7 +120,7 @@ contract My721Token is ERC721 {
using Strings for uint256;
constructor() ERC721("My721Token", "MTK") {}
...
function tokenURI(uint256 tokenId)
Expand All @@ -138,7 +138,7 @@ contract My721Token is ERC721 {
return string(
abi.encodePacked(
"data:application/json;base64,",
"data:application/json;base64,",
Base64.encode(dataURI)
)
);
Expand Down Expand Up @@ -184,3 +184,28 @@ await instance.multicall([
instance.contract.methods.bar().encodeABI()
]);
----

=== Datetime

xref:api:utils.adoc#Datetime[`Datetime`] util allows you to convert human readable dates and times into unix time.

Consider this example demonstrating datetime:
[source, solidity]
----
// contracts/Widget.sol
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/utils/Datetime.sol";
contract Widget {
int256 _birthday;
function setBirthday(
int32 _year,
uint8 _month,
uint8 _day
) public {
_birthday = Datetime.date(_year, _month, _day);
}
}
----
79 changes: 79 additions & 0 deletions test/utils/Datetime.test.js
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');
});
});
});

0 comments on commit aa6f9cf

Please sign in to comment.