natural-time-js
Version:
Natural time is a fresh, elegant, and coherent way of measuring the movements of time here on the Earth. This new time standard is based on common sense and the observation of natural cycles. Learn more: https://naturaltime.app. This JavaScript Class tran
260 lines • 11.3 kB
JavaScript
/**
* @module natural-time-js/core
* @description This module provides the core NaturalDate class that bridges between artificial Gregorian time and Natural Time.
*/
import { Seasons } from 'astronomy-engine';
/**
* Cache for year context calculations to improve performance
* @private
*/
const yearContextCache = new Map();
/**
* Calculates year context for natural date calculations.
*
* The year context includes:
* - The start timestamp of the natural year (winter solstice)
* - The duration of the year in days (365 or 366)
*
* This function handles the conversion between Gregorian calendar years
* and natural years, accounting for longitude adjustments.
*
* @param artificialYear - Gregorian calendar year
* @param longitude - Longitude in degrees (-180 to 180)
* @returns Year context object with start timestamp and duration
* @private
*/
const calculateYearContext = (artificialYear, longitude) => {
const cacheKey = `${artificialYear}_${longitude}`;
if (yearContextCache.has(cacheKey)) {
return yearContextCache.get(cacheKey);
}
const startSolstice = Seasons(artificialYear).dec_solstice.date;
const endSolstice = Seasons(artificialYear + 1).dec_solstice.date;
const startNewYear = Date.UTC(startSolstice.getUTCFullYear(), startSolstice.getUTCMonth(), startSolstice.getUTCDate() + (startSolstice.getUTCHours() >= 12 ? 1 : 0), 12, 0, 0);
const endNewYear = Date.UTC(endSolstice.getUTCFullYear(), endSolstice.getUTCMonth(), endSolstice.getUTCDate() + (endSolstice.getUTCHours() >= 12 ? 1 : 0), 12, 0, 0);
const context = {
start: parseInt(String(startNewYear + (-longitude + 180) * NaturalDate.MILLISECONDS_PER_DAY / 360)),
duration: (endNewYear - startNewYear) / NaturalDate.MILLISECONDS_PER_DAY
};
yearContextCache.set(cacheKey, context);
return context;
};
/**
* Natural date class for converting artificial (Gregorian) dates to natural time.
*
* The NaturalDate class provides a complete implementation of the natural time system,
* which is based on natural cycles:
* - Years begin at the winter solstice
* - Each year has 13 moons (months) of 28 days each, plus 1-2 "rainbow days"
* - Weeks are 7 days
* - Time is measured in 360 degrees for a full day cycle
*
* This class handles the conversion between Gregorian calendar dates and natural time,
* accounting for longitude adjustments to provide location-specific natural time.
*/
export class NaturalDate {
/**
* Creates a new natural date instance.
*
* Converts a Gregorian date to natural time based on the specified longitude.
* The longitude is used to adjust the natural time for the local position on Earth.
*
* @param date - JavaScript Date object, Unix timestamp, or date string
* @param longitude - Longitude in degrees (-180 to 180)
* @throws {Error} If inputs are invalid
*
* @example
* // Create a natural date for the current time in Paris (longitude 2.3522)
* const parisNaturalDate = new NaturalDate(new Date(), 2.3522);
*
* @example
* // Create a natural date for a specific time in Tokyo (longitude 139.6503)
* const tokyoNaturalDate = new NaturalDate('2023-06-21T12:00:00', 139.6503);
*/
constructor(date, longitude) {
// Validate longitude
if (longitude !== undefined && (typeof longitude !== 'number' || longitude < -180 || longitude > 180)) {
throw new Error('Longitude must be between -180 and +180');
}
const dateObj = new Date(date || Date.now());
if (isNaN(dateObj.getTime())) {
throw new Error('Invalid date provided');
}
this.unixTime = dateObj.getTime();
this.longitude = longitude || 0;
if (Number.isFinite(this.unixTime)) {
// YEAR START & DURATION
let yearContext = calculateYearContext(dateObj.getUTCFullYear() - 1, this.longitude);
// Correction if between the beginning of natural year and the end of the artificial year
if (this.unixTime - yearContext.start >= yearContext.duration * NaturalDate.MILLISECONDS_PER_DAY)
yearContext = calculateYearContext(dateObj.getUTCFullYear(), this.longitude);
this.yearStart = yearContext.start;
this.yearDuration = yearContext.duration;
const timeSinceLocalYearStart = (this.unixTime - this.yearStart) / NaturalDate.MILLISECONDS_PER_DAY;
// YEAR
this.year = new Date(this.yearStart).getUTCFullYear() - new Date(NaturalDate.END_OF_ARTIFICIAL_TIME).getUTCFullYear() + 1;
// MOON
this.moon = Math.floor(timeSinceLocalYearStart / 28) + 1;
// HEPTAD
this.week = Math.floor(timeSinceLocalYearStart / 7) + 1;
this.weekOfMoon = Math.floor(timeSinceLocalYearStart / 7) % 4 + 1;
// DAY
this.day = Math.floor((this.unixTime - (NaturalDate.END_OF_ARTIFICIAL_TIME + (-this.longitude + 180) * NaturalDate.MILLISECONDS_PER_DAY / 360)) / NaturalDate.MILLISECONDS_PER_DAY);
this.dayOfYear = Math.floor(timeSinceLocalYearStart) + 1;
this.dayOfMoon = Math.floor(timeSinceLocalYearStart) % 28 + 1;
this.dayOfWeek = Math.floor(timeSinceLocalYearStart) % 7 + 1;
// NADIR (i.e day start, midnight)
this.nadir = this.yearStart + Math.floor(timeSinceLocalYearStart) * NaturalDate.MILLISECONDS_PER_DAY;
// TIME
this.time = (this.unixTime - this.nadir) * 360 / NaturalDate.MILLISECONDS_PER_DAY;
// RAINBOW DAY
this.isRainbowDay = this.dayOfYear > 13 * 28;
return;
}
throw new Error('Argument must be a Date object or a Unix timestamp');
}
/**
* Gets the time of an astronomical event in natural degrees.
*
* Converts an event timestamp to natural degrees (0-360°) within the current natural day.
* Returns false if the event occurs outside the current natural day.
*
* This is useful for calculating the position of celestial events like sunrise, sunset,
* moonrise, or moonset within the natural time system.
*
* @param event - Event timestamp (Date object, Unix timestamp, or date string)
* @returns Event time in natural degrees (0-360°) or false if out of range
*
* @example
* // Calculate sunrise time in natural degrees
* const naturalDate = new NaturalDate(new Date(), 0);
* const sunrise = '2023-01-01T06:00:00'; // Example sunrise at 6:00 AM
* const sunriseInDegrees = naturalDate.getTimeOfEvent(sunrise);
* console.log(`Sunrise occurs at ${sunriseInDegrees}°`);
*/
getTimeOfEvent(event) {
// Make sure it's a unix timestamp
const eventTime = new Date(event).getTime();
// Check if not out of range
if (eventTime < this.nadir || eventTime > this.nadir + NaturalDate.MILLISECONDS_PER_DAY)
return false;
return (eventTime - this.nadir) * (360 / NaturalDate.MILLISECONDS_PER_DAY);
}
/**
* Exports current date as a full ISO formatted string.
*
* The format is: "YYY)MM)DD TTT°DD NT±LLL.L"
* - YYY: Natural year (padded to 3 digits)
* - MM: Moon number (padded to 2 digits)
* - DD: Day of moon (padded to 2 digits)
* - TTT: Time in degrees (padded to 3 digits)
* - DD: Decimal degrees (2 digits)
* - ±LLL.L: Longitude with sign and 1 decimal
*
* @returns {string} Formatted natural date string
*/
toString() {
return `${this.toDateString()} ${this.toTimeString()} ${this.toLongitudeString()}`;
}
/**
* Exports the date part of the natural date.
*
* @param separator - Separator character between components (default: ')')
* @returns Formatted date string
*/
toDateString(separator = ')') {
if (this.isRainbowDay) {
const isSecondRainbowDay = this.dayOfYear === 366;
return `${this.toYearString()}${separator}RAINBOW${isSecondRainbowDay ? '+' : ''}`;
}
return `${this.toYearString()}${separator}${this.toMoonString()}${separator}${this.toDayOfMoonString()}`;
}
/**
* Exports the time part of the natural date.
*
* @param decimals - Number of decimal places for time (default: 2)
* @param rounding - Rounding increment for time (default: 1)
* @returns Formatted time string
*/
toTimeString(decimals = 2, rounding = 0.01) {
let time = this.time;
// Round to the nearest decimal increment
if (rounding > 0) {
time = Math.round(time / rounding) * rounding;
// Handle edge case where rounding pushes us to 360°
if (time >= 360) {
time = 0;
}
}
// Format the integer part to 3 digits
const integerPart = Math.floor(time).toString().padStart(3, '0');
// Format the decimal part if needed
const decimalPart = decimals > 0
? (time % 1).toFixed(decimals).substring(2).padEnd(decimals, '0')
: '';
return `${integerPart}°${decimalPart}`;
}
/**
* Exports the longitude part of the natural date.
*
* @param decimals - Number of decimal places for longitude (default: 1)
* @returns Formatted longitude string
*/
toLongitudeString(decimals = 1) {
// Consider longitudes very close to 0 (within 0.5 degrees) as NTZ
if (Math.abs(this.longitude) < 0.5) {
return 'NTZ';
}
const prefix = 'NT';
const sign = this.longitude >= 0 ? '+' : '-';
const absLongitude = Math.abs(this.longitude);
const integerPart = Math.floor(absLongitude).toString().padStart(1, '0');
const decimalPart = decimals > 0
? (absLongitude % 1).toFixed(decimals).substring(2)
: '';
return `${prefix}${sign}${integerPart}${decimals > 0 ? '.' + decimalPart : ''}`;
}
/**
* Exports the year part of the natural date.
*
* @returns Formatted year string
*/
toYearString() {
const absYear = Math.abs(this.year);
const sign = this.year < 0 ? '-' : '';
return `${sign}${absYear.toString().padStart(3, '0')}`;
}
/**
* Exports the moon part of the natural date.
*
* @returns Formatted moon string
*/
toMoonString() {
return this.moon.toString().padStart(2, '0');
}
/**
* Exports the day of moon part of the natural date.
*
* @returns Formatted day of moon string
*/
toDayOfMoonString() {
return this.dayOfMoon.toString().padStart(2, '0');
}
}
/**
* Number of milliseconds in a day
*/
NaturalDate.MILLISECONDS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000
/**
* End date of the artificial time era (December 21, 2012 12:00 UTC)
* This is the reference point for natural time calculations
*/
NaturalDate.END_OF_ARTIFICIAL_TIME = Date.UTC(2012, 11, 21, 12, 0, 0);
/**
* Clears the internal year context cache.
* Exposed for testing or advanced usage without exposing the cache itself.
*/
export function resetYearContextCache() {
yearContextCache.clear();
}
//# sourceMappingURL=NaturalDate.js.map