lib/NanoTime.js

module.exports = NanoTime;

/**
 * @private
 */
const ZERO = BigInt(0);

/**
 * @private
 */
const THOUSAND = BigInt(1000); // eslint-disable-line no-magic-numbers

/**
 * @private
 */
const NANOSECOND = BigInt(1);

/**
 * @private
 */
const MICROSECOND = NANOSECOND * THOUSAND;

/**
 * @private
 */
const MILLISECOND = MICROSECOND * THOUSAND;

/**
 * @private
 */
const SECOND = MILLISECOND * THOUSAND;

/**
 * @private
 */
const MINUTES_IN_SECONDS = 60;

/**
 * @example
 * const NanoTime = require('pete/lib/nanotime');
 * var time = new NanoTime();
 *
 * @alias module:pete/lib/NanoTime
 * @class
 * @param {number|bigint} [value=0n]
 */
function NanoTime (value = ZERO) {
	this.value = value;

	this.seconds = 0;
	this.milliseconds = 0;
	this.microseconds = 0;
	this.nanoseconds = 0;

	this.set(value);
}

/**
 * Value of nanosecond.
 */
NanoTime.NANOSECOND = NANOSECOND;

/**
 * Value of one microsecond in nanoseconds.
 */
NanoTime.MICROSECOND = MICROSECOND;

/**
 * Value of one millisecond in nanoseconds.
 */
NanoTime.MILLISECOND = MILLISECOND;

/**
 * Value of one second in nanoseconds.
 */
NanoTime.SECOND = SECOND;

/**
 * Create NanoTime from value.
 * If called on existing NanoTime object, it will update its value instead of creating new object.
 * If value type is not supported, simply return that value.
 *
 * @static
 * @param {number|bigint|string|module:lib/nanotime~NanoTime} value
 * @return {module:lib/nanotime~NanoTime|any}
 */
NanoTime.from = function from (value) {
	if (this instanceof NanoTime) {
		this.value = ZERO;
	}

	if (typeof value === 'number') {
		return fromNumber.call(this, value);
	}
	else if (typeof value === 'bigint') {
		return fromBigInt.call(this, value);
	}
	else if (typeof value === 'string') {
		return fromString.call(this, value);
	}
	else if (typeof value === 'object') {
		return fromNanoTime.call(this, value);
	}

	return value;
};

/**
 * Set new value.
 *
 * @method
 * @see {@link NanoTime.from}
 * @param {string|number|bigint|module:lib/nanotime~NanoTime} value
 * @return {module:lib/nanotime~NanoTime|any}
 */
NanoTime.prototype.set = NanoTime.from;

/**
 * Get number of nanoseconds.
 *
 * @example
 * var time = new NanoTime(process.hrtime.bigint());
 * console.log(time.get()); // 1002003004n
 *
 * @return {bigint}
 */
NanoTime.prototype.get = function get () {
	return this.value;
};

/**
 * Get number of nanoseconds.
 *
 * @method
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf}
 * @return {bigint}
 */
NanoTime.prototype.valueOf = NanoTime.prototype.get;

/**
 * Convert to primitive type.
 *
 * @private
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive}
 * @param {string} hint
 * @return {bigint|number|string|boolean}
 */
NanoTime.prototype[Symbol.toPrimitive] = function toPrimitive (hint) {
	if (hint === 'bigint') {
		return this.value;
	}

	if (hint === 'number') {
		return this.toNumber();
	}

	if (hint === 'string') {
		return this.toString();
	}

	return this.value !== ZERO;
};

/**
 * Convert to textual representation of value.
 *
 * @example
 * var time = new NanoTime(process.hrtime.bigint());
 * console.log(time); // 1m 2s 3ms 4μs 5ns
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString}
 * @return {string}
 */
NanoTime.prototype.toString = function toString () {
	let seconds = this.seconds;
	const minutes = seconds > MINUTES_IN_SECONDS ? Math.floor(seconds / MINUTES_IN_SECONDS) : 0;

	if (minutes > 0) {
		seconds -= minutes * MINUTES_IN_SECONDS;
	}

	return `${minutes}m ${seconds}s ${this.milliseconds}ms ${this.microseconds}μs ${this.nanoseconds}ns`;
};

/**
 * Convert to Number.
 *
 * @example
 * var time = new NanoTime(process.hrtime.bigint());
 * console.log(time.toNumber()); // 1.002003004
 *
 * @return {number}
 */
NanoTime.prototype.toNumber = function toNumber () {
	/* eslint-disable */
	return Number(this.seconds + '.' +
		('000' + this.milliseconds).slice(-3) +
		('000' + this.microseconds).slice(-3) +
		('000' + this.nanoseconds).slice(-3)
	);
	/* eslint-enable */
};

/**
 * When stringified to JSON, output String representation instead of full object.
 *
 * @example
 * var time = new NanoTime(process.hrtime.bigint());
 * console.log(JSON.stringify(time)); // "1m 2s 3ms 4μs 5ns"
 *
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior}
 * @return {string}
 */
NanoTime.prototype.toJSON = function toJSON () {
	return this.toString();
};

/**
 * When stringified for inspection, output String representation instead of full object.
 *
 * @private
 * @see {@link https://nodejs.org/api/util.html#util_util_inspect_custom}
 * @return {string}
 */
NanoTime.prototype[Symbol.for('nodejs.util.inspect.custom')] = function inspect () {
	return `'${this.toString()}'`;
};

/* eslint-disable no-invalid-this */

/**
 * If called on existing NanoTime, update its value. Create new NanoTime otherwise.
 * If value type is not supported, just return that value.
 *
 * @private
 * @param {bigint} value
 * @return {module:lib/nanotime~NanoTime|any}
 */
function fromBigInt (value) {
	if (typeof value !== 'bigint') {
		return value;
	}

	if (!this || !(this instanceof NanoTime)) {
		return new NanoTime(value);
	}

	this.value = value;

	var n = 0;
	var b = ZERO;
	var sum = ZERO;

	b = value / SECOND;
	sum += b * SECOND;
	n = Number(b);
	if (n >= 1) {
		this.seconds = Math.floor(n);
	}

	b = (value - sum) / MILLISECOND;
	sum += b * MILLISECOND;
	n = Number(b);
	if (n >= 1) {
		this.milliseconds = Math.floor(n);
	}

	b = (value - sum) / MICROSECOND;
	sum += b * MICROSECOND;
	n = Number(b);
	if (n >= 1) {
		this.microseconds = Math.floor(n);
	}

	n = Number(value - sum);
	if (n >= 1) {
		this.nanoseconds = Math.floor(n);
	}

	return this;
}

/**
 * If called on existing NanoTime, update its value. Create new NanoTime otherwise.
 * If value type is not supported, just return that value.
 *
 * @private
 * @param {number} value
 * @return {module:lib/nanotime~NanoTime|any}
 */
function fromNumber (value) {
	if (typeof value !== 'number' || isNaN(value) || value === Infinity) {
		return value;
	}

	if (!this || !(this instanceof NanoTime)) {
		return new NanoTime(value);
	}

	this.seconds = Math.floor(value);

	// 9 = 3 per value * 3 values (mili-, micro- and nanoseconds)
	var n = value.toFixed(9).split('.')[1].match(/.{1,3}/g); // eslint-disable-line no-magic-numbers

	/* eslint-disable array-element-newline */
	[
		this.milliseconds = 0,
		this.microseconds = 0,
		this.nanoseconds = 0
	] = n.map(Number);
	/* eslint-enable array-element-newline */

	this.value = ZERO
		+ (BigInt(this.seconds) * SECOND)
		+ (BigInt(this.milliseconds) * MILLISECOND)
		+ (BigInt(this.microseconds) * MICROSECOND)
		+ BigInt(this.nanoseconds);

	return this;
}

/**
 * If called on existing NanoTime, update its value. Create new NanoTime otherwise.
 * If value type is not supported, just return that value.
 *
 * @private
 * @param {string} value
 * @return {module:lib/nanotime~NanoTime|any}
 */
function fromString (value) {
	if (typeof value !== 'string') {
		return value;
	}

	var n = value.match(/^(?:\s*)(?:(?<m>\d+)m ?)?(?:(?<s>\d+)s)?(?: (?<ms>\d+)ms)?(?: (?<us>\d+)(?:μ|u)s)?(?: (?<ns>\d+)ns)?(?:\s*)$/);
	if (!n) {
		return value;
	}

	if (!this || !(this instanceof NanoTime)) {
		return new NanoTime(value);
	}

	this.seconds = Number(n.groups.s || 0) + (Number(n.groups.m || 0) * MINUTES_IN_SECONDS);
	this.milliseconds = Number(n.groups.ms || 0);
	this.microseconds = Number(n.groups.us || 0);
	this.nanoseconds = Number(n.groups.ns || 0);

	this.value = ZERO
		+ (BigInt(this.seconds) * SECOND)
		+ (BigInt(this.milliseconds) * MILLISECOND)
		+ (BigInt(this.microseconds) * MICROSECOND)
		+ BigInt(this.nanoseconds);

	return this;
}

/**
 * If called on existing NanoTime, update its value. Create new NanoTime otherwise.
 * If value type is not supported, just return that value.
 *
 * @private
 * @param {module:lib/nanotime~NanoTime} value
 * @return {module:lib/nanotime~NanoTime|any}
 */
function fromNanoTime (value) {
	if (typeof value !== 'object') {
		return value;
	}

	if (typeof value.value !== 'bigint') {
		return value;
	}

	if (!(value instanceof NanoTime)) {
		return fromBigInt.call(this, value.value);
	}

	if (!this || !(this instanceof NanoTime)) {
		return new NanoTime(value);
	}

	this.value = value.value;
	this.nanoseconds = value.nanoseconds;
	this.microseconds = value.microseconds;
	this.milliseconds = value.milliseconds;
	this.seconds = value.seconds;

	return this;
}