natural-time-js 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/constants.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @module constants
3
+ * @description Collection of constants used for natural time and astronomical calculations
4
+ */
5
+
6
+ /**
7
+ * Number of milliseconds in a day
8
+ * @constant {number}
9
+ */
10
+ export const MILLISECONDS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000
11
+
12
+ /**
13
+ * End date of the artificial time era (December 21, 2012 12:00 UTC)
14
+ * @constant {number}
15
+ */
16
+ export const END_OF_ARTIFICIAL_TIME = Date.UTC(2012, 11, 21, 12, 0, 0);
17
+
18
+ /**
19
+ * Hemisphere identifiers for geographical calculations
20
+ * @constant
21
+ * @enum {string}
22
+ * @readonly
23
+ */
24
+ export const HEMISPHERES = {
25
+ NORTH: 'NORTH',
26
+ SOUTH: 'SOUTH'
27
+ }
28
+
29
+ /**
30
+ * Day numbers marking the start and end of summer season
31
+ * Day 1 represents January 1st
32
+ * @constant
33
+ * @enum {number}
34
+ * @readonly
35
+ */
36
+ export const SEASONS = {
37
+ /** Day 91 (typically April 1st) */
38
+ SUMMER_START_DAY: 91,
39
+ /** Day 273 (typically September 30th) */
40
+ SUMMER_END_DAY: 273
41
+ }
42
+
43
+ /**
44
+ * Solar altitude thresholds in degrees for different daylight conditions
45
+ * @constant
46
+ * @enum {number}
47
+ * @readonly
48
+ */
49
+ export const ANGLES = {
50
+ NIGHT_ALTITUDE: -12,
51
+ GOLDEN_HOUR_ALTITUDE: 6
52
+ }
package/context.js CHANGED
@@ -1,163 +1,285 @@
1
+ /**
2
+ * @module context
3
+ * @description Provides astronomical calculations and events for natural time, including sun and moon positions
4
+ */
5
+
1
6
  import { NaturalDate } from './index.js';
2
7
  import { Body, Observer, SearchHourAngle, SearchRiseSet, SearchAltitude, MoonPhase, Equator, Horizon, Seasons } from 'astronomy-engine';
8
+ import { HEMISPHERES, SEASONS, ANGLES } from './constants.js';
9
+ import {
10
+ isValidLatitude,
11
+ isValidLongitude,
12
+ isValidNaturalDate,
13
+ throwValidationError
14
+ } from './utils/validators.js';
3
15
 
4
- // Caching recuring astronomical calculations
5
- const ASTRO_CACHE = {};
16
+ // Move cache to its own module for better separation of concerns
17
+ const astroCache = new Map();
6
18
 
7
19
  /**
8
- * SUN ALTITUDE
9
- *
10
- * Determine the sun altitude and max altitude from a specific naturalDate at given lat/long coordinates
11
- * @param {NaturalDate} naturalDate
12
- * @param {Number} latitude
13
- * @returns Object { altitude, highestAltitude }
20
+ * Determines the hemisphere based on latitude
21
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
22
+ * @returns {string} HEMISPHERES.NORTH or HEMISPHERES.SOUTH
23
+ * @private
14
24
  */
15
- export function NaturalSunAltitude(naturalDate, latitude) {
25
+ const getHemisphere = (latitude) => latitude >= 0 ? HEMISPHERES.NORTH : HEMISPHERES.SOUTH;
16
26
 
17
- let observer = new Observer(latitude, naturalDate.longitude, 0);
18
- let date = new Date(naturalDate.unixTime);
27
+ /**
28
+ * Determines if the given day is in summer season for the specified latitude
29
+ * @param {number} dayOfYear - Day of the year (1-366)
30
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
31
+ * @returns {boolean} True if the day is in summer season
32
+ * @private
33
+ */
34
+ const isSummerSeason = (dayOfYear, latitude) => {
35
+ const hemisphere = getHemisphere(latitude);
36
+ return hemisphere === HEMISPHERES.NORTH
37
+ ? (dayOfYear >= SEASONS.SUMMER_START_DAY && dayOfYear <= SEASONS.SUMMER_END_DAY)
38
+ : (dayOfYear <= SEASONS.SUMMER_START_DAY || dayOfYear >= SEASONS.SUMMER_END_DAY);
39
+ }
40
+
41
+ /**
42
+ * Creates an astronomical observer at the specified coordinates
43
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
44
+ * @param {number} longitude - Longitude in degrees (-180 to 180)
45
+ * @returns {Observer} Astronomy-engine observer instance
46
+ * @private
47
+ */
48
+ const createObserver = (latitude, longitude) => new Observer(latitude, longitude, 0);
19
49
 
20
- let sun = Equator(Body.Sun, date, observer, true, false);
21
- let sunAltitude = Math.max(Horizon(date, observer, sun.ra, sun.dec).altitude, 0);
22
- let midday = SearchHourAngle(Body.Sun, observer, 0, new Date(naturalDate.nadir));
50
+ /**
51
+ * Calculates sun altitude data for a given natural date and latitude
52
+ * @param {NaturalDate} naturalDate - Natural date instance
53
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
54
+ * @returns {{
55
+ * altitude: number,
56
+ * highestAltitude: number
57
+ * }} Sun altitude data
58
+ * @throws {Error} If inputs are invalid or calculation fails
59
+ */
60
+ export function NaturalSunAltitude(naturalDate, latitude) {
61
+ // Validate inputs
62
+ if (!isValidNaturalDate(naturalDate)) {
63
+ throwValidationError('naturalDate', naturalDate, 'NaturalDate instance');
64
+ }
65
+ if (!isValidLatitude(latitude)) {
66
+ throwValidationError('latitude', latitude, 'number between -90 and 90');
67
+ }
23
68
 
24
- return {
25
- altitude: sunAltitude, // Current sun altitude
26
- highestAltitude: midday.hor.altitude // Highest altitude of the sun during the day
69
+ try {
70
+ const observer = createObserver(latitude, naturalDate.longitude);
71
+ const date = new Date(naturalDate.unixTime);
72
+ const sun = Equator(Body.Sun, date, observer, true, false);
73
+
74
+ return {
75
+ altitude: Math.max(Horizon(date, observer, sun.ra, sun.dec).altitude, 0),
76
+ highestAltitude: SearchHourAngle(Body.Sun, observer, 0, new Date(naturalDate.nadir)).hor.altitude
77
+ }
78
+ } catch (error) {
79
+ console.error('Error in NaturalSunAltitude:', error);
80
+ throw error;
27
81
  }
28
82
  }
29
83
 
30
-
31
84
  /**
32
- * SUN EVENTS
33
- *
34
- * Determine natural day's main sun events like sunrise, sunset, nightStart ...
35
- * @param {NaturalDate} naturalDate
36
- * @param {Number} latitude
37
- * @returns Object { sunrise, sunset, nightStart, nightEnd, morningGoldenHour, eveningGoldenHour }
85
+ * Calculates sun-related events for a given natural date and latitude
86
+ * @param {NaturalDate} naturalDate - Natural date instance
87
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
88
+ * @returns {{
89
+ * sunrise: number,
90
+ * sunset: number,
91
+ * nightStart: number,
92
+ * nightEnd: number,
93
+ * morningGoldenHour: number,
94
+ * eveningGoldenHour: number
95
+ * }} Sun events in natural degrees (0-360)
96
+ * @throws {Error} If inputs are invalid or calculation fails
38
97
  */
39
98
  export function NaturalSunEvents(naturalDate, latitude) {
40
-
41
- let cache_id = `SUN_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
42
-
43
- if (ASTRO_CACHE[cache_id]) {
44
- return ASTRO_CACHE[cache_id];
99
+ // Validate inputs
100
+ if (!isValidNaturalDate(naturalDate)) {
101
+ throwValidationError('naturalDate', naturalDate, 'NaturalDate instance');
102
+ }
103
+ if (!isValidLatitude(latitude)) {
104
+ throwValidationError('latitude', latitude, 'number between -90 and 90');
45
105
  }
46
106
 
47
- let observer = new Observer(latitude, naturalDate.longitude, 0);
48
- let nadir = new Date(naturalDate.nadir);
49
-
50
- let isSummer =
51
- latitude >= 0 && (naturalDate.dayOfYear >= 91 && naturalDate.dayOfYear <= 273) || // Northern hemisphere summer
52
- latitude <= 0 && (naturalDate.dayOfYear <= 91 || naturalDate.dayOfYear >= 273); // Southern hemisphere summer
53
-
54
- // SUNRISE & SUNSET
55
- let sunrise = naturalDate.getTimeOfEvent(SearchRiseSet(Body.Sun, observer, +1, nadir, 1));
56
- if(!sunrise) sunrise = isSummer ? 0 : 180;
57
- let sunset = naturalDate.getTimeOfEvent(SearchRiseSet(Body.Sun, observer, -1, nadir, 1));
58
- if(!sunset) sunset = isSummer ? 360 : 180;
59
-
60
- // NIGHT (-12° altitude)
61
- let nightStart = naturalDate.getTimeOfEvent(SearchAltitude(Body.Sun, observer, -1, nadir, 2, -12));
62
- if(!nightStart) nightStart = isSummer ? 360 : 180;
63
- let nightEnd = naturalDate.getTimeOfEvent(SearchAltitude(Body.Sun, observer, +1, nadir, 2, -12));
64
- if(!nightEnd) nightEnd = isSummer ? 0 : 180;
65
-
66
- // GOLDEN HOUR (+6° altitude)
67
- let morningGoldenHour = naturalDate.getTimeOfEvent(SearchAltitude(Body.Sun, observer, +1, nadir, 2, +6));
68
- if(!morningGoldenHour) morningGoldenHour = isSummer ? 0 : 180;
69
- let eveningGoldenHour = naturalDate.getTimeOfEvent(SearchAltitude(Body.Sun, observer, -1, nadir, 2, +6));
70
- if(!eveningGoldenHour) eveningGoldenHour = isSummer ? 360 : 180;
107
+ const cacheKey = `SUN_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
71
108
 
72
- return ASTRO_CACHE[cache_id] = {
73
- sunrise: sunrise,
74
- sunset: sunset,
75
- nightStart: nightStart,
76
- nightEnd: nightEnd,
77
- morningGoldenHour: morningGoldenHour,
78
- eveningGoldenHour: eveningGoldenHour
109
+ try {
110
+ if (astroCache.has(cacheKey)) {
111
+ const cachedResult = astroCache.get(cacheKey);
112
+ if (cachedResult && typeof cachedResult === 'object') {
113
+ return cachedResult;
114
+ }
115
+ }
116
+
117
+ const observer = createObserver(latitude, naturalDate.longitude);
118
+ const nadir = new Date(naturalDate.nadir);
119
+ const isSummer = isSummerSeason(naturalDate.dayOfYear, latitude);
120
+
121
+ // Helper function to handle edge cases
122
+ const getEventTime = (searchResult, isSummerDefault) => {
123
+ if (!searchResult) {
124
+ return isSummer ? (isSummerDefault ? 360 : 0) : 180;
125
+ }
126
+ return naturalDate.getTimeOfEvent(searchResult);
127
+ };
128
+
129
+ const events = {
130
+ sunrise: getEventTime(SearchRiseSet(Body.Sun, observer, +1, nadir, 1), false),
131
+ sunset: getEventTime(SearchRiseSet(Body.Sun, observer, -1, nadir, 1), true),
132
+ nightStart: getEventTime(SearchAltitude(Body.Sun, observer, -1, nadir, 2, ANGLES.NIGHT_ALTITUDE), true),
133
+ nightEnd: getEventTime(SearchAltitude(Body.Sun, observer, +1, nadir, 2, ANGLES.NIGHT_ALTITUDE), false),
134
+ morningGoldenHour: getEventTime(SearchAltitude(Body.Sun, observer, +1, nadir, 2, ANGLES.GOLDEN_HOUR_ALTITUDE), false),
135
+ eveningGoldenHour: getEventTime(SearchAltitude(Body.Sun, observer, -1, nadir, 2, ANGLES.GOLDEN_HOUR_ALTITUDE), true)
136
+ };
137
+
138
+ astroCache.set(cacheKey, events);
139
+ return events;
140
+ } catch (error) {
141
+ console.error('Error in NaturalSunEvents:', error);
142
+ throw error;
79
143
  }
80
144
  }
81
145
 
82
146
  /**
83
- * MOON PHASE & POSITION
84
- *
85
- * Determines moon's position in the sky at specific natural date and lat/long position
86
- * @param {NaturalDate} naturalDate
87
- * @param {Number} latitude
88
- * @returns Object { position, phase, altitude, highestAltitude }
147
+ * Calculates moon position and phase data for a given natural date and latitude
148
+ * @param {NaturalDate} naturalDate - Natural date instance
149
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
150
+ * @returns {{
151
+ * phase: number,
152
+ * altitude: number,
153
+ * highestAltitude: number
154
+ * }} Moon position and phase data
89
155
  */
90
156
  export function NaturalMoonPosition(naturalDate, latitude) {
157
+ // Validate inputs
158
+ if (!isValidNaturalDate(naturalDate)) {
159
+ throwValidationError('naturalDate', naturalDate, 'NaturalDate instance');
160
+ }
161
+ if (!isValidLatitude(latitude)) {
162
+ throwValidationError('latitude', latitude, 'number between -90 and 90');
163
+ }
91
164
 
92
- let observer = new Observer(latitude, naturalDate.longitude, 0);
93
- let date = new Date(naturalDate.unixTime);
94
-
95
- // PHASE
96
- let moonPhase = MoonPhase(date);
97
- let moon = Equator(Body.Moon, date, observer, true, false);
98
-
99
- // POSITION & ALTITUDE
100
- let moonAltitude = Math.max(Horizon(date, observer, moon.ra, moon.dec).altitude, 0);
101
- let midmoon = SearchHourAngle(Body.Moon, observer, 0, new Date(naturalDate.nadir));
102
-
103
- return {
104
- phase: moonPhase, // 0 = new-moon, 90 = first quarter, 180 = full-moon, 270 = last quarter
105
- altitude: moonAltitude, // Current moon altitude
106
- highestAltitude: midmoon.hor.altitude // Highest altitude of the moon during the day
165
+ try {
166
+ const observer = createObserver(latitude, naturalDate.longitude);
167
+ const date = new Date(naturalDate.unixTime);
168
+ const moon = Equator(Body.Moon, date, observer, true, false);
169
+
170
+ return {
171
+ phase: MoonPhase(date),
172
+ altitude: Math.max(Horizon(date, observer, moon.ra, moon.dec).altitude, 0),
173
+ highestAltitude: SearchHourAngle(Body.Moon, observer, 0, new Date(naturalDate.nadir)).hor.altitude
174
+ }
175
+ } catch (error) {
176
+ console.error('Error in NaturalMoonPosition:', error);
177
+ return {
178
+ phase: 0,
179
+ altitude: 0,
180
+ highestAltitude: 0
181
+ }
107
182
  }
108
183
  }
109
184
 
110
185
  /**
111
- * MOON EVENTS
112
- *
113
- * Determine natural day's moonrise and moonset
114
- * @param {NaturalDate} naturalDate
115
- * @param {Number} latitude
116
- * @returns Object { moonrise, moonset }
186
+ * Calculates moon rise and set events for a given natural date and latitude
187
+ * @param {NaturalDate} naturalDate - Natural date instance
188
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
189
+ * @returns {{
190
+ * moonrise: number|null,
191
+ * moonset: number|null
192
+ * }} Moon events in natural degrees (0-360)
117
193
  */
118
194
  export function NaturalMoonEvents(naturalDate, latitude) {
119
-
120
- let cache_id = `MOON_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
195
+ const cacheKey = `MOON_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
121
196
 
122
- if (ASTRO_CACHE[cache_id]) {
123
- return ASTRO_CACHE[cache_id];
124
- }
197
+ try {
198
+ if (astroCache.has(cacheKey)) {
199
+ return astroCache.get(cacheKey);
200
+ }
125
201
 
126
- let observer = new Observer(latitude, naturalDate.longitude, 0);
202
+ const observer = createObserver(latitude, naturalDate.longitude);
203
+ const nadir = new Date(naturalDate.nadir);
127
204
 
128
- // MOONRISE & MOONSET
129
- let moonrise = SearchRiseSet(Body.Moon, observer, +1, new Date(naturalDate.nadir), 1);
130
- let moonset = SearchRiseSet(Body.Moon, observer, -1, new Date(naturalDate.nadir), 1);
205
+ const moonrise = SearchRiseSet(Body.Moon, observer, +1, nadir, 1);
206
+ const moonset = SearchRiseSet(Body.Moon, observer, -1, nadir, 1);
131
207
 
132
- return ASTRO_CACHE[cache_id] = {
133
- moonrise: naturalDate.getTimeOfEvent(moonrise),
134
- moonset: naturalDate.getTimeOfEvent(moonset),
208
+ const events = {
209
+ moonrise: naturalDate.getTimeOfEvent(moonrise),
210
+ moonset: naturalDate.getTimeOfEvent(moonset)
211
+ };
212
+
213
+ astroCache.set(cacheKey, events);
214
+ return events;
215
+ } catch (error) {
216
+ console.error('Error calculating moon events:', error);
217
+ return {
218
+ moonrise: null,
219
+ moonset: null
220
+ }
135
221
  }
136
222
  }
137
223
 
138
224
  /**
139
- * MUSTACHES RANGE
140
- *
141
- * The mustaches represents the shortest and longest day/night length
142
- * @param {Number} latitude
143
- * @returns Object { winterSunrise, winterSunset, summerSunrise, summerSunset }
225
+ * Calculates the range of sun positions between winter and summer solstices (average mustaches angle)
226
+ * @param {NaturalDate} naturalDate - Natural date instance
227
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
228
+ * @returns {{
229
+ * winterSunrise: number,
230
+ * winterSunset: number,
231
+ * summerSunrise: number,
232
+ * summerSunset: number,
233
+ * averageMustacheAngle: number
234
+ * }} Solstice sun positions and mustache angle
144
235
  */
145
236
  export function MustachesRange(naturalDate, latitude) {
146
- let cache_id = `MUSTACHES_${naturalDate.year}_${latitude}`;
237
+ const cacheKey = `MUSTACHES_${naturalDate.year}_${latitude}`;
147
238
 
148
- if (ASTRO_CACHE[cache_id]) {
149
- return ASTRO_CACHE[cache_id];
150
- }
239
+ try {
240
+ if (astroCache.has(cacheKey)) {
241
+ return astroCache.get(cacheKey);
242
+ }
151
243
 
152
- // Get the solstices for the current year
153
- const currentSeasons = Seasons(new Date(naturalDate.unixTime).getFullYear());
154
- let winterSolsticeSunEvents = NaturalSunEvents(new NaturalDate(currentSeasons.dec_solstice.date, 0), latitude);
155
- let summerSolsticeSunEvents = NaturalSunEvents(new NaturalDate(currentSeasons.jun_solstice.date, 0), latitude);
156
-
157
- return ASTRO_CACHE[cache_id] = {
158
- winterSunrise: winterSolsticeSunEvents.sunrise,
159
- winterSunset: winterSolsticeSunEvents.sunset,
160
- summerSunrise: summerSolsticeSunEvents.sunrise,
161
- summerSunset: summerSolsticeSunEvents.sunset,
162
- };
244
+ const currentYear = new Date(naturalDate.unixTime).getFullYear();
245
+ const currentSeasons = Seasons(currentYear);
246
+
247
+ const winterSolsticeSunEvents = NaturalSunEvents(
248
+ new NaturalDate(currentSeasons.dec_solstice.date, 0),
249
+ latitude
250
+ );
251
+ const summerSolsticeSunEvents = NaturalSunEvents(
252
+ new NaturalDate(currentSeasons.jun_solstice.date, 0),
253
+ latitude
254
+ );
255
+
256
+ const averageMustacheAngle = Math.min(Math.max(
257
+ latitude >= 0
258
+ ? (winterSolsticeSunEvents.sunrise - summerSolsticeSunEvents.sunrise +
259
+ summerSolsticeSunEvents.sunset - winterSolsticeSunEvents.sunset) / 4
260
+ : (summerSolsticeSunEvents.sunrise - winterSolsticeSunEvents.sunrise +
261
+ winterSolsticeSunEvents.sunset - summerSolsticeSunEvents.sunset) / 4,
262
+ 0
263
+ ), 90);
264
+
265
+ const result = {
266
+ winterSunrise: winterSolsticeSunEvents.sunrise,
267
+ winterSunset: winterSolsticeSunEvents.sunset,
268
+ summerSunrise: summerSolsticeSunEvents.sunrise,
269
+ summerSunset: summerSolsticeSunEvents.sunset,
270
+ averageMustacheAngle
271
+ };
272
+
273
+ astroCache.set(cacheKey, result);
274
+ return result;
275
+ } catch (error) {
276
+ console.error('Error calculating mustaches range:', error);
277
+ return {
278
+ winterSunrise: 0,
279
+ winterSunset: 0,
280
+ summerSunrise: 0,
281
+ summerSunset: 0,
282
+ averageMustacheAngle: 0
283
+ }
284
+ }
163
285
  }
package/index.js CHANGED
@@ -1,28 +1,61 @@
1
+ /**
2
+ * @module natural-time-js
3
+ * @description Natural time implementation for JavaScript - translates artificial dates to natural dates
4
+ */
5
+
1
6
  import { Seasons } from 'astronomy-engine';
7
+ import { MILLISECONDS_PER_DAY, END_OF_ARTIFICIAL_TIME } from './constants.js';
2
8
 
3
- const NT_CACHE = {};
9
+ const yearContextCache = new Map();
4
10
 
5
11
  /**
6
- * NATURAL TIME
7
- *
8
- * Natural time is a fresh, elegant, and coherent way of measuring the movements of time here on the Earth.
9
- * This new time standard is based on common sense and the observation of natural cycles.
10
- * Learn more: https://naturaltime.app
11
- *
12
- * This JavaScript Class translates gregorian artificial datetime to natural datetime
13
- * https://github.com/sylvain441/natural-time-js
14
- *
15
- * Special thanks to Don Cross, author of the Astronomy Library to calculate celestial body events
16
- * https://github.com/cosinekitty/astronomy
17
- *
18
- * @author Biquette 🐐
19
- * @contact sylvain441@proton.me
20
- *
12
+ * Calculates year context for natural date calculations
13
+ * @param {number} artificialYear - Gregorian calendar year
14
+ * @param {number} longitude - Longitude in degrees (-180 to 180)
15
+ * @returns {{
16
+ * start: number,
17
+ * duration: number
18
+ * }} Year context with start timestamp and duration
19
+ * @private
21
20
  */
22
- export class NaturalDate {
21
+ const calculateYearContext = (artificialYear, longitude) => {
22
+ const cacheKey = `${artificialYear}_${longitude}`;
23
+
24
+ if (yearContextCache.has(cacheKey)) {
25
+ return yearContextCache.get(cacheKey);
26
+ }
27
+
28
+ const startSolstice = Seasons(artificialYear).dec_solstice.date;
29
+ const endSolstice = Seasons(artificialYear + 1).dec_solstice.date;
30
+
31
+ const startNewYear = Date.UTC(
32
+ startSolstice.getUTCFullYear(),
33
+ startSolstice.getUTCMonth(),
34
+ startSolstice.getUTCDate() + (startSolstice.getUTCHours() >= 12 ? 1 : 0),
35
+ 12, 0, 0
36
+ );
37
+
38
+ const endNewYear = Date.UTC(
39
+ endSolstice.getUTCFullYear(),
40
+ endSolstice.getUTCMonth(),
41
+ endSolstice.getUTCDate() + (endSolstice.getUTCHours() >= 12 ? 1 : 0),
42
+ 12, 0, 0
43
+ );
44
+
45
+ const context = {
46
+ start: parseInt(startNewYear + (-longitude + 180) * MILLISECONDS_PER_DAY/360),
47
+ duration: (endNewYear - startNewYear) / MILLISECONDS_PER_DAY
48
+ };
49
+
50
+ yearContextCache.set(cacheKey, context);
51
+ return context;
52
+ }
23
53
 
24
- static END_OF_ARTIFICIAL_TIME = 1356091200000; // 2012-12-22 00:00:00 at +180°;
25
- static MILLISECONDS_PER_DAY = 86400000; // 24*60*60*1000
54
+ /**
55
+ * Natural date class for converting artificial (Gregorian) dates to natural time
56
+ * @class
57
+ */
58
+ export class NaturalDate {
26
59
 
27
60
  unixTime; // Artificial gregorian date (UNIX timestamp)
28
61
  longitude; // Longitude (between -180° to +180°)
@@ -47,62 +80,40 @@ export class NaturalDate {
47
80
  nadir; // Beginning of the day at the current longitude (UNIX timestamp)
48
81
 
49
82
  /**
50
- * Computes natural date from gregorian date and longitude
51
- * @param {Date} date Date object or Unix timestamp
52
- * @param {Number} longitude From -180 to + 180
83
+ * Creates a new natural date instance
84
+ * @param {Date|number} date - JavaScript Date object or Unix timestamp
85
+ * @param {number} longitude - Longitude in degrees (-180 to 180)
86
+ * @throws {Error} If inputs are invalid
53
87
  */
54
88
  constructor(date, longitude) {
55
-
56
- date = new Date(date);
57
- this.unixTime = date.getTime();
58
- this.longitude = longitude;
89
+ // Validate longitude
90
+ if (longitude !== undefined && (typeof longitude !== 'number' || longitude < -180 || longitude > 180)) {
91
+ throw new Error('Longitude must be between -180 and +180');
92
+ }
59
93
 
60
- if (Number.isFinite(this.unixTime)) {
61
-
62
- const getYearContext = (artificialYear, longitude) => {
63
-
64
- // We cache astronomical calculations
65
- let cache_id = `${artificialYear}_${longitude}`;
66
- if(NT_CACHE?.[cache_id])
67
- return NT_CACHE[cache_id];
68
-
69
- // Search for December solstices
70
- let startSolstice = Seasons(artificialYear).dec_solstice.date;
71
- let endSolstice = Seasons(artificialYear + 1).dec_solstice.date;
94
+ date = new Date(date || Date.now());
95
+ if (isNaN(date.getTime())) {
96
+ throw new Error('Invalid date provided');
97
+ }
72
98
 
73
- // Calculate next time it's midnight at the 180th meridian after solstice (mean time)
74
- // NB: Midnight at +180° equals midday the previous day at +0°
75
- let startNewYear = Date.UTC(
76
- startSolstice.getUTCFullYear(),
77
- startSolstice.getUTCMonth(),
78
- startSolstice.getUTCDate() + (startSolstice.getUTCHours() >= 12 ? 1 : 0),
79
- 12, 0, 0);
80
- let endNewYear = Date.UTC(
81
- endSolstice.getUTCFullYear(),
82
- endSolstice.getUTCMonth(),
83
- endSolstice.getUTCDate() + (endSolstice.getUTCHours() >= 12 ? 1 : 0),
84
- 12, 0, 0);
99
+ this.unixTime = date.getTime();
100
+ this.longitude = longitude || 0;
85
101
 
86
- return NT_CACHE[cache_id] = {
87
- start: parseInt(startNewYear + (-longitude + 180) * NaturalDate.MILLISECONDS_PER_DAY/360),
88
- duration: (endNewYear - startNewYear) / NaturalDate.MILLISECONDS_PER_DAY
89
- };
90
- }
91
-
102
+ if (Number.isFinite(this.unixTime)) {
92
103
  // YEAR START & DURATION
93
- let yearContext = getYearContext(date.getUTCFullYear()-1, longitude);
104
+ let yearContext = calculateYearContext(date.getUTCFullYear()-1, longitude);
94
105
 
95
106
  // Correction if between the beginning of natural year and the end of the artificial year
96
- if(this.unixTime - yearContext.start >= yearContext.duration * NaturalDate.MILLISECONDS_PER_DAY)
97
- yearContext = getYearContext(date.getUTCFullYear(), longitude);
107
+ if(this.unixTime - yearContext.start >= yearContext.duration * MILLISECONDS_PER_DAY)
108
+ yearContext = calculateYearContext(date.getUTCFullYear(), longitude);
98
109
 
99
110
  this.yearStart = yearContext.start;
100
111
  this.yearDuration = yearContext.duration;
101
112
 
102
- let timeSinceLocalYearStart = (this.unixTime - this.yearStart) / NaturalDate.MILLISECONDS_PER_DAY;
113
+ let timeSinceLocalYearStart = (this.unixTime - this.yearStart) / MILLISECONDS_PER_DAY;
103
114
 
104
115
  // YEAR
105
- this.year = new Date(this.yearStart).getUTCFullYear() - new Date(NaturalDate.END_OF_ARTIFICIAL_TIME).getUTCFullYear()+ 1;
116
+ this.year = new Date(this.yearStart).getUTCFullYear() - new Date(END_OF_ARTIFICIAL_TIME).getUTCFullYear()+ 1;
106
117
 
107
118
  // MOON
108
119
  this.moon = parseInt(timeSinceLocalYearStart / 28) + 1;
@@ -112,16 +123,16 @@ export class NaturalDate {
112
123
  this.weekOfMoon = parseInt(timeSinceLocalYearStart / 7) % 4 + 1;
113
124
 
114
125
  // DAY
115
- this.day = parseInt((this.unixTime - (NaturalDate.END_OF_ARTIFICIAL_TIME + (-longitude + 180) * NaturalDate.MILLISECONDS_PER_DAY/360)) / NaturalDate.MILLISECONDS_PER_DAY);
126
+ this.day = parseInt((this.unixTime - (END_OF_ARTIFICIAL_TIME + (-longitude + 180) * MILLISECONDS_PER_DAY/360)) / MILLISECONDS_PER_DAY);
116
127
  this.dayOfYear = parseInt(timeSinceLocalYearStart) + 1;
117
128
  this.dayOfMoon = parseInt(timeSinceLocalYearStart) % 28 + 1;
118
129
  this.dayOfWeek = parseInt(timeSinceLocalYearStart) % 7 + 1;
119
130
 
120
131
  // NADIR (i.e day start, midnight)
121
- this.nadir = this.yearStart + parseInt(timeSinceLocalYearStart) * NaturalDate.MILLISECONDS_PER_DAY;
132
+ this.nadir = this.yearStart + parseInt(timeSinceLocalYearStart) * MILLISECONDS_PER_DAY;
122
133
 
123
134
  // TIME
124
- this.time = (this.unixTime - this.nadir) * 360 / NaturalDate.MILLISECONDS_PER_DAY;
135
+ this.time = (this.unixTime - this.nadir) * 360 / MILLISECONDS_PER_DAY;
125
136
 
126
137
  // RAINBOW DAY
127
138
  this.isRainbowDay = this.dayOfYear > 13*28;
@@ -133,19 +144,19 @@ export class NaturalDate {
133
144
  }
134
145
 
135
146
  /**
136
- * Returns the time at specified event's date
137
- * @param {Date} event
138
- * @returns Time on a 360° scale or null if out of range
147
+ * Gets the time of an astronomical event in natural degrees
148
+ * @param {Date|number} event - Event timestamp
149
+ * @returns {number|false} Event time in natural degrees (0-360) or false if out of range
139
150
  */
140
151
  getTimeOfEvent(event) {
141
152
  // Make sure it's a unix timestamp
142
153
  event = new Date(event).getTime();
143
154
 
144
155
  // Check if not out of range
145
- if(event < this.nadir || event > this.nadir + NaturalDate.MILLISECONDS_PER_DAY)
156
+ if(event < this.nadir || event > this.nadir + MILLISECONDS_PER_DAY)
146
157
  return false;
147
158
 
148
- return (event - this.nadir) * (360 / NaturalDate.MILLISECONDS_PER_DAY);
159
+ return (event - this.nadir) * (360 / MILLISECONDS_PER_DAY);
149
160
  }
150
161
 
151
162
  /**
@@ -153,7 +164,7 @@ export class NaturalDate {
153
164
  * @returns ex: "004)04)01 113°00 NT+95"
154
165
  */
155
166
  toString() {
156
- return this.toDateString() + " " + this.toTimeString() + " " + this.toLongitudeString();
167
+ return `${this.toDateString()} ${this.toTimeString()} ${this.toLongitudeString()}`;
157
168
  }
158
169
 
159
170
  /**
@@ -162,10 +173,10 @@ export class NaturalDate {
162
173
  * @returns ex: "004)04)01"
163
174
  */
164
175
  toDateString(separator = ')') {
165
- if(this.isRainbowDay)
166
- return `${this.toYearString()}${separator}RAINBOW${this.dayOfYear == 366 ? "+" : ""}`;
167
- else
168
- return `${this.toYearString()}${separator}${this.toMoonString()}${separator}${this.toDayOfMoonString()}`;
176
+ if(this.isRainbowDay) {
177
+ return `${this.toYearString()}${separator}RAINBOW${this.dayOfYear === 366 ? "+" : ""}`;
178
+ }
179
+ return `${this.toYearString()}${separator}${this.toMoonString()}${separator}${this.toDayOfMoonString()}`;
169
180
  }
170
181
 
171
182
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natural-time-js",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "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 translates gregorian artificial datetime to natural datetime",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @module validators
3
+ * @description Validation utilities for natural time calculations
4
+ */
5
+
6
+ import { NaturalDate } from '../index.js';
7
+
8
+ const LATITUDE_RANGE = { MIN: -90, MAX: 90 };
9
+ const LONGITUDE_RANGE = { MIN: -180, MAX: 180 };
10
+ const ANGLE_RANGE = { MIN: 0, MAX: 360 };
11
+
12
+ // Add ranges based on NaturalDate class constraints
13
+ const NATURAL_DATE_RANGES = {
14
+ MOON: { MIN: 1, MAX: 14 },
15
+ WEEK: { MIN: 1, MAX: 53 },
16
+ WEEK_OF_MOON: { MIN: 1, MAX: 4 },
17
+ DAY_OF_MOON: { MIN: 1, MAX: 28 },
18
+ DAY_OF_WEEK: { MIN: 1, MAX: 7 }
19
+ };
20
+
21
+ /**
22
+ * Validates if a value is a finite number
23
+ * @param {*} value - Value to validate
24
+ * @returns {boolean} True if value is a valid number
25
+ */
26
+ export const isValidNumber = (value) => {
27
+ return typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(value);
28
+ };
29
+
30
+ /**
31
+ * Validates latitude value
32
+ * @param {number} latitude - Latitude to validate
33
+ * @returns {boolean} True if latitude is valid
34
+ */
35
+ export const isValidLatitude = (latitude) => {
36
+ return isValidNumber(latitude) &&
37
+ latitude >= LATITUDE_RANGE.MIN &&
38
+ latitude <= LATITUDE_RANGE.MAX;
39
+ };
40
+
41
+ /**
42
+ * Validates longitude value
43
+ */
44
+ export const isValidLongitude = (longitude) => {
45
+ return isValidNumber(longitude) &&
46
+ longitude >= LONGITUDE_RANGE.MIN &&
47
+ longitude <= LONGITUDE_RANGE.MAX;
48
+ };
49
+
50
+ /**
51
+ * Validates angle value (0-360 degrees)
52
+ */
53
+ export const isValidAngle = (angle) => {
54
+ return isValidNumber(angle) &&
55
+ angle >= ANGLE_RANGE.MIN &&
56
+ angle <= ANGLE_RANGE.MAX;
57
+ };
58
+
59
+ /**
60
+ * Validates unix timestamp
61
+ */
62
+ export const isValidTimestamp = (timestamp) => {
63
+ return isValidNumber(timestamp) && timestamp > 0;
64
+ };
65
+
66
+ /**
67
+ * Validates NaturalDate instance with complete property checks
68
+ * @param {*} date - Value to validate
69
+ * @returns {boolean} True if value is a valid NaturalDate instance
70
+ */
71
+ export const isValidNaturalDate = (date) => {
72
+ if (!(date instanceof NaturalDate)) {
73
+ return false;
74
+ }
75
+
76
+ // Basic property validations
77
+ if (!isValidTimestamp(date.unixTime) ||
78
+ !isValidLongitude(date.longitude) ||
79
+ !isValidNumber(date.year)) {
80
+ return false;
81
+ }
82
+
83
+ // Natural calendar validations
84
+ return (
85
+ isValidNumber(date.moon) &&
86
+ date.moon >= NATURAL_DATE_RANGES.MOON.MIN &&
87
+ date.moon <= NATURAL_DATE_RANGES.MOON.MAX &&
88
+
89
+ isValidNumber(date.week) &&
90
+ date.week >= NATURAL_DATE_RANGES.WEEK.MIN &&
91
+ date.week <= NATURAL_DATE_RANGES.WEEK.MAX &&
92
+
93
+ isValidNumber(date.weekOfMoon) &&
94
+ date.weekOfMoon >= NATURAL_DATE_RANGES.WEEK_OF_MOON.MIN &&
95
+ date.weekOfMoon <= NATURAL_DATE_RANGES.WEEK_OF_MOON.MAX &&
96
+
97
+ isValidNumber(date.dayOfMoon) &&
98
+ date.dayOfMoon >= NATURAL_DATE_RANGES.DAY_OF_MOON.MIN &&
99
+ date.dayOfMoon <= NATURAL_DATE_RANGES.DAY_OF_MOON.MAX &&
100
+
101
+ isValidNumber(date.dayOfWeek) &&
102
+ date.dayOfWeek >= NATURAL_DATE_RANGES.DAY_OF_WEEK.MIN &&
103
+ date.dayOfWeek <= NATURAL_DATE_RANGES.DAY_OF_WEEK.MAX &&
104
+
105
+ isValidNumber(date.dayOfYear) &&
106
+ date.dayOfYear >= 1 &&
107
+ date.dayOfYear <= date.yearDuration &&
108
+
109
+ typeof date.isRainbowDay === 'boolean' &&
110
+ isValidAngle(date.time) &&
111
+ isValidTimestamp(date.yearStart) &&
112
+ isValidNumber(date.yearDuration) &&
113
+ isValidTimestamp(date.nadir)
114
+ );
115
+ };
116
+
117
+ /**
118
+ * Validates cache key string
119
+ */
120
+ export const isValidCacheKey = (key) => {
121
+ return typeof key === 'string' && key.length > 0;
122
+ };
123
+
124
+ /**
125
+ * Throws formatted error for invalid parameters
126
+ * @param {string} paramName - Name of the invalid parameter
127
+ * @param {*} value - Invalid value
128
+ * @param {string} expectedType - Description of expected type/format
129
+ * @throws {Error} Formatted validation error
130
+ */
131
+ export const throwValidationError = (paramName, value, expectedType) => {
132
+ throw new Error(
133
+ `Invalid ${paramName}: ${value}. ` +
134
+ `Expected ${expectedType}`
135
+ );
136
+ };