ts-py-datetime
Version:
Datetime handling the python way (or as close as possible), now with TypeScript
406 lines (405 loc) • 14 kB
JavaScript
import * as d3 from 'd3-time-format';
import { date, time, timedelta } from '.';
import { MAXYEAR, MAXYEAR_ORDINAL, MINYEAR, MINYEAR_ORDINAL, } from '../constants';
import { DatetimeIntervals, } from '../models/datetime';
import { isParams } from '../utils/utils';
export class datetime {
/**
* A datetime object is a single object containing all the information from a date object and a time object.
* @param {number} year
* @param {number} month
* @param {number} day
* @param {number} [hour=0]
* @param {number} [minute=0]
* @param {number} [second=0]
* @param {number} [millisecond=0]
* @param {boolean} [utc=false]
*/
constructor(year, month, day, hour = 0, minute = 0, second = 0, millisecond = 0, utc = false) {
this.year = 1970;
this.month = 1;
this.day = 1;
this.hour = 0;
this.minute = 0;
this.second = 0;
this.millisecond = 0;
this.utc = false;
const args = {
year: year,
month,
day,
hour,
minute,
second,
millisecond,
utc,
};
if (isParams(year)) {
delete args.year;
Object.assign(args, year);
}
if (!args.year || !args.month || !args.day) {
throw SyntaxError('Missing required argument year, month, or day');
}
for (const arg in args) {
if (DatetimeIntervals.includes(arg) &&
!Number.isInteger(args[arg])) {
throw TypeError(`Argument ${arg} value ${args[arg]} is not an integer`);
}
}
if (args.year < MINYEAR || args.year > MAXYEAR) {
throw RangeError(`year ${args.year} is out of range`);
}
if (args.month < 1 || args.month > 12) {
throw RangeError(`month ${args.month} is out of range`);
}
if (args.day < 1 ||
args.day > new Date(args.year, args.month, 0).getDate()) {
throw RangeError(`day ${day} is out of range for month`);
}
if ((args.hour ?? 0) < 0 || (args.hour ?? 0) > 23) {
throw RangeError('hour must be in 0..23');
}
if ((args.minute ?? 0) < 0 || (args.minute ?? 0) > 59) {
throw RangeError('minute must be in 0..59');
}
if ((args.second ?? 0) < 0 || (args.second ?? 0) > 59) {
throw RangeError('second must be in 0..59');
}
if ((args.millisecond ?? 0) < 0 || (args.millisecond ?? 0) > 999) {
throw RangeError('millisecond must be in 0..999');
}
Object.assign(this, args);
}
/**
* Return date object with same year, month, and day.
* @returns {date}
*/
date() {
return new date(this.year, this.month, this.day);
}
/**
* Return time object with same hour, minute, second, and millisecond.
* @returns {time}
*/
time() {
return new time(this.hour, this.minute, this.second, this.millisecond);
}
/**
* Return a datetime with the same attributes,
* except for those attributes given new values by whichever arguments are specified.
* @param {number} [year=this.year]
* @param {number} [month=this.month]
* @param {number} [day=this.day]
* @param {number} [hour=this.hour]
* @param {number} [minute=this.minute]
* @param {number} [second=this.second]
* @param {number} [millisecond=this.millisecond]
* @returns {datetime}
*/
replace(year = this.year, month = this.month, day = this.day, hour = this.hour, minute = this.minute, second = this.second, millisecond = this.millisecond) {
const args = {
year: year,
month,
day,
hour,
minute,
second,
millisecond,
};
if (isParams(year)) {
delete args.year;
Object.assign(args, year);
}
return new datetime({
year: args.year ?? this.year,
month: args.month ?? this.month,
day: args.day ?? this.day,
hour: args.hour ?? this.hour,
minute: args.minute ?? this.minute,
second: args.second ?? this.second,
millisecond: args.millisecond ?? this.millisecond,
});
}
/**
* Return the proleptic Gregorian ordinal of the date.
* @returns {number}
*/
toordinal() {
return this.date().toordinal();
}
/**
* Return the POSIX timestamp corresponding to the datetime instance.
* @returns {number}
*/
timestamp() {
let value;
if (this.utc) {
value = Date.UTC(this.year, this.month - 1, this.day || 1, this.hour || 0, this.minute || 0, this.second || 0, this.millisecond || 0);
}
else {
value = this.jsDate.getTime();
}
return value / 1000;
}
/**
* Return the day of the week as an integer, where Monday is 0 and Sunday is 6.
* @returns {number}
*/
weekday() {
return this.date().weekday();
}
/**
* Return the day of the week as an integer, where Monday is 1 and Sunday is 7.
* @returns {number}
*/
isoweekday() {
return this.weekday() + 1;
}
/**
* Return an array with three components: year, week, and weekday.
* @returns {[number, number, number]}
*/
isocalendar() {
const [year, week, weekday] = d3
.utcFormat('%G-%V-%u')(this.jsDate)
.split('-');
return [Number(year), Number(week), Number(weekday)];
}
/**
* Return a string representing the date and time in ISO 8601 format.
* @param {string} [sep='T'] A one-character separator, placed between the date and time portions of the result.
* @param {TimeSpec} [timespec='auto'] Specifies the number of additional components of the time to include.
* @returns {string}
*/
isoformat(sep = 'T', timespec = 'auto') {
const args = {
sep: sep,
timespec,
};
if (isParams(sep)) {
delete args.sep;
Object.assign(args, sep);
args.sep = args.sep ?? 'T';
}
let format;
switch (args.timespec) {
case 'hours':
format = `%Y-%m-%d${args.sep}%H`;
break;
case 'minutes':
format = `%Y-%m-%d${args.sep}%H:%M`;
break;
case 'seconds':
format = `%Y-%m-%d${args.sep}%H:%M:%S`;
break;
case 'milliseconds':
format = `%Y-%m-%d${args.sep}%H:%M:%S.%f`;
break;
case 'auto':
default:
format = `%Y-%m-%d${args.sep}%H:%M:%S${this.millisecond ? '.%f' : ''}`;
break;
}
return this.strftime(format);
}
/**
* For a datetime instance d, d.valueOf() is equivalent to d.timestamp().
* @returns {number}
*/
valueOf() {
return this.timestamp();
}
/**
* For a datetime instance d, d.toString() is equivalent to d.isoformat(' ').
* @returns {string}
*/
toString() {
return this.isoformat(' ');
}
/**
* Return a string representing the date and time, such as Wed Dec 4 20:30:40 2002.
* @returns {string}
*/
ctime() {
return d3.timeFormat('%a %b %-e %H:%M:%S %Y')(this.jsDate);
}
/**
* Return a string representing the date and time, controlled by an explicit format string
* @param {string} format
* @returns {string}
*/
strftime(format) {
if (this.utc) {
return d3.utcFormat(format)(this.jsDate);
}
else {
return d3.timeFormat(format)(this.jsDate);
}
}
/** Return this object as a JS Date object */
get jsDate() {
if (this.utc) {
return new Date(this.valueOf() * 1000);
}
else {
return new Date(this.year, this.month - 1, this.day || 1, this.hour || 0, this.minute || 0, this.second || 0, this.millisecond || 0);
}
}
/** The earliest representable datetime POSIX timestamp, equal to -59011416000 seconds. */
static get min() {
return new datetime(MINYEAR, 1, 1);
}
/** The latest representable datetime POSIX timestamp, equal to 253402300799.999 seconds. */
static get max() {
return new datetime(MAXYEAR, 12, 31, 23, 59, 59, 999);
}
/** The smallest possible difference between non-equal datetime objects, 1 millisecond, in seconds. */
static get resolution() {
return timedelta.resolution;
}
/**
* Return the current local date and time.
* @returns {datetime}
*/
static today() {
return datetime.now();
}
/**
* Return the current local date and time.
* Functionally equivalent to dt.datetime.today() but is considered the preferred syntax.
* @returns {datetime}
*/
static now() {
return datetime.fromjsdate(new Date());
}
/**
* Return the current UTC date and time.
* @returns {datetime}
*/
static utcnow() {
return datetime.utcfromjsdate(new Date());
}
/**
* Return the local date and time corresponding to the POSIX timestamp.
* @param {number} timestamp
* @returns {datetime}
*/
static fromtimestamp(timestamp) {
const jsdate = new Date(timestamp * 1000);
return datetime.fromjsdate(jsdate);
}
/**
* Return the UTC datetime corresponding to the POSIX timestamp.
* @param {number} timestamp
* @returns {datetime}
*/
static utcfromtimestamp(timestamp) {
const jsdate = new Date(timestamp * 1000);
return datetime.utcfromjsdate(jsdate);
}
/**
* Return the local date and time corresponding to the JS Date object.
* @param {Date} jsdate
* @returns {datetime}
*/
static fromjsdate(jsdate) {
return new datetime({
year: jsdate.getFullYear(),
month: jsdate.getMonth() + 1,
day: jsdate.getDate(),
hour: jsdate.getHours(),
minute: jsdate.getMinutes(),
second: jsdate.getSeconds(),
millisecond: jsdate.getMilliseconds(),
});
}
/**
* Return the UTC datetime corresponding to the JS Date object.
* @param {Date} jsdate
* @returns {datetime}
*/
static utcfromjsdate(jsdate) {
return new datetime({
year: jsdate.getUTCFullYear(),
month: jsdate.getUTCMonth() + 1,
day: jsdate.getUTCDate(),
hour: jsdate.getUTCHours(),
minute: jsdate.getUTCMinutes(),
second: jsdate.getUTCSeconds(),
millisecond: jsdate.getUTCMilliseconds(),
utc: true,
});
}
/**
* Return the datetime corresponding to the proleptic Gregorian ordinal, where January 1 of year 1 has oridinal 1.
* @param {number} ordinal
* @returns {datetime}
*/
static fromordinal(ordinal) {
if (ordinal < MINYEAR_ORDINAL || ordinal > MAXYEAR_ORDINAL) {
throw RangeError(`ordinal ${ordinal} is out of range`);
}
return datetime.fromtimestamp(date.min.valueOf() +
new timedelta({ days: ordinal - MINYEAR_ORDINAL }).valueOf());
}
/**
* Return a new datetime object whose date components are equal to the given date object's,
* and whose time components are equal to the given time object's.
* If the date argument is a datetime object, its time components are ignored.
* @param {date | datetime} date
* @param {time} time
* @returns {datetime}
*/
static combine(date, time) {
return new datetime({
year: date.year,
month: date.month,
day: date.day,
hour: time.hour,
minute: time.minute,
second: time.second,
millisecond: time.millisecond,
});
}
/**
* Return a datetime corresponding to a date_string in any valid ISO 8601 format.
* @param {string} date_string
* @returns {datetime}
*/
static fromisoformat(date_string) {
const d = d3.isoParse(date_string);
if (d) {
return datetime.fromjsdate(d);
}
throw SyntaxError('Unable to parse date string');
}
/**
* Return a datetime corresponding to the ISO calendar date specified by the year, week, and day.
* The non-date components of the datetime are populated with their normal default values.
* @param {number} year
* @param {number} week
* @param {number} day
* @returns {datetime}
*/
static fromisocalendar(year, week, day) {
return datetime.strptime(`${year}-${week}-${day}`, '%G-%V-%u');
}
/**
* Return a datetime corresponding to date_string, parsed according to format.
* @param {string} date_string
* @param {string} format
* @param {boolean} [utc=false]
* @returns {datetime}
*/
static strptime(date_string, format, utc = false) {
const parser = utc ? d3.utcParse : d3.timeParse;
const parsed = parser(format)(date_string);
if (!parsed) {
throw Error(`'${date_string}' does not match format '${format}'`);
}
return utc
? datetime.utcfromjsdate(parsed)
: datetime.fromjsdate(parsed);
}
}