natural-time-js 1.0.5 → 1.1.0

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/context.js CHANGED
@@ -1,171 +1,320 @@
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 {
9
+ isValidLatitude,
10
+ isValidLongitude,
11
+ isValidNaturalDate,
12
+ throwValidationError
13
+ } from './utils/validators.js';
3
14
 
4
- // Caching recuring astronomical calculations
5
- const ASTRO_CACHE = {};
15
+ /**
16
+ * Hemisphere identifiers for geographical calculations
17
+ * @constant
18
+ * @enum {string}
19
+ * @readonly
20
+ */
21
+ export const HEMISPHERES = {
22
+ NORTH: 'NORTH',
23
+ SOUTH: 'SOUTH'
24
+ };
6
25
 
7
26
  /**
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 }
27
+ * Day numbers marking the start and end of summer season
28
+ * Day 1 represents January 1st
29
+ * @constant
30
+ * @enum {number}
31
+ * @readonly
14
32
  */
15
- export function NaturalSunAltitude(naturalDate, latitude) {
33
+ export const SEASONS = {
34
+ /** Day 91 (typically April 1st) */
35
+ SUMMER_START_DAY: 91,
36
+ /** Day 273 (typically September 30th) */
37
+ SUMMER_END_DAY: 273
38
+ };
16
39
 
17
- let observer = new Observer(latitude, naturalDate.longitude, 0);
18
- let date = new Date(naturalDate.unixTime);
40
+ /**
41
+ * Solar altitude thresholds in degrees for different daylight conditions
42
+ * @constant
43
+ * @enum {number}
44
+ * @readonly
45
+ */
46
+ export const ANGLES = {
47
+ NIGHT_ALTITUDE: -12,
48
+ GOLDEN_HOUR_ALTITUDE: 6
49
+ };
19
50
 
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));
51
+ // Move cache to its own module for better separation of concerns
52
+ const astroCache = new Map();
23
53
 
24
- return {
25
- altitude: sunAltitude, // Current sun altitude
26
- highestAltitude: midday.hor.altitude // Highest altitude of the sun during the day
27
- }
28
- }
54
+ /**
55
+ * Determines the hemisphere based on latitude
56
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
57
+ * @returns {string} HEMISPHERES.NORTH or HEMISPHERES.SOUTH
58
+ * @private
59
+ */
60
+ const getHemisphere = (latitude) => latitude >= 0 ? HEMISPHERES.NORTH : HEMISPHERES.SOUTH;
29
61
 
62
+ /**
63
+ * Determines if the given day is in summer season for the specified latitude
64
+ * @param {number} dayOfYear - Day of the year (1-366)
65
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
66
+ * @returns {boolean} True if the day is in summer season
67
+ * @private
68
+ */
69
+ const isSummerSeason = (dayOfYear, latitude) => {
70
+ const hemisphere = getHemisphere(latitude);
71
+ return hemisphere === HEMISPHERES.NORTH
72
+ ? (dayOfYear >= SEASONS.SUMMER_START_DAY && dayOfYear <= SEASONS.SUMMER_END_DAY)
73
+ : (dayOfYear <= SEASONS.SUMMER_START_DAY || dayOfYear >= SEASONS.SUMMER_END_DAY);
74
+ }
30
75
 
31
76
  /**
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 }
77
+ * Creates an astronomical observer at the specified coordinates
78
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
79
+ * @param {number} longitude - Longitude in degrees (-180 to 180)
80
+ * @returns {Observer} Astronomy-engine observer instance
81
+ * @private
38
82
  */
39
- export function NaturalSunEvents(naturalDate, latitude) {
83
+ const createObserver = (latitude, longitude) => new Observer(latitude, longitude, 0);
40
84
 
41
- let cache_id = `SUN_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
42
-
43
- if (ASTRO_CACHE[cache_id]) {
44
- return ASTRO_CACHE[cache_id];
85
+ /**
86
+ * Calculates sun altitude data for a given natural date and latitude
87
+ * @param {NaturalDate} naturalDate - Natural date instance
88
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
89
+ * @returns {{
90
+ * altitude: number,
91
+ * highestAltitude: number
92
+ * }} Sun altitude data
93
+ * @throws {Error} If inputs are invalid or calculation fails
94
+ */
95
+ export function NaturalSunAltitude(naturalDate, latitude) {
96
+ // Validate inputs
97
+ if (!isValidNaturalDate(naturalDate)) {
98
+ throwValidationError('naturalDate', naturalDate, 'NaturalDate instance');
99
+ }
100
+ if (!isValidLatitude(latitude)) {
101
+ throwValidationError('latitude', latitude, 'number between -90 and 90');
45
102
  }
46
103
 
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;
71
-
72
- return ASTRO_CACHE[cache_id] = {
73
- sunrise: sunrise,
74
- sunset: sunset,
75
- nightStart: nightStart,
76
- nightEnd: nightEnd,
77
- morningGoldenHour: morningGoldenHour,
78
- eveningGoldenHour: eveningGoldenHour
104
+ try {
105
+ const observer = createObserver(latitude, naturalDate.longitude);
106
+ const date = new Date(naturalDate.unixTime);
107
+ const sun = Equator(Body.Sun, date, observer, true, false);
108
+
109
+ return {
110
+ altitude: Math.max(Horizon(date, observer, sun.ra, sun.dec).altitude, 0),
111
+ highestAltitude: SearchHourAngle(Body.Sun, observer, 0, new Date(naturalDate.nadir)).hor.altitude
112
+ }
113
+ } catch (error) {
114
+ console.error('Error in NaturalSunAltitude:', error);
115
+ throw error;
79
116
  }
80
117
  }
81
118
 
82
119
  /**
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 }
120
+ * Calculates sun-related events for a given natural date and latitude
121
+ * @param {NaturalDate} naturalDate - Natural date instance
122
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
123
+ * @returns {{
124
+ * sunrise: number,
125
+ * sunset: number,
126
+ * nightStart: number,
127
+ * nightEnd: number,
128
+ * morningGoldenHour: number,
129
+ * eveningGoldenHour: number
130
+ * }} Sun events in natural degrees (0-360)
131
+ * @throws {Error} If inputs are invalid or calculation fails
89
132
  */
90
- export function NaturalMoonPosition(naturalDate, latitude) {
133
+ export function NaturalSunEvents(naturalDate, latitude) {
134
+ // Validate inputs
135
+ if (!isValidNaturalDate(naturalDate)) {
136
+ throwValidationError('naturalDate', naturalDate, 'NaturalDate instance');
137
+ }
138
+ if (!isValidLatitude(latitude)) {
139
+ throwValidationError('latitude', latitude, 'number between -90 and 90');
140
+ }
91
141
 
92
- let observer = new Observer(latitude, naturalDate.longitude, 0);
93
- let date = new Date(naturalDate.unixTime);
142
+ const cacheKey = `SUN_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
143
+
144
+ try {
145
+ if (astroCache.has(cacheKey)) {
146
+ const cachedResult = astroCache.get(cacheKey);
147
+ if (cachedResult && typeof cachedResult === 'object') {
148
+ return cachedResult;
149
+ }
150
+ }
151
+
152
+ const observer = createObserver(latitude, naturalDate.longitude);
153
+ const nadir = new Date(naturalDate.nadir);
154
+ const isSummer = isSummerSeason(naturalDate.dayOfYear, latitude);
94
155
 
95
- // PHASE
96
- let moonPhase = MoonPhase(date);
97
- let moon = Equator(Body.Moon, date, observer, true, false);
156
+ // Helper function to handle edge cases
157
+ const getEventTime = (searchResult, isSummerDefault) => {
158
+ if (!searchResult) {
159
+ return isSummer ? (isSummerDefault ? 360 : 0) : 180;
160
+ }
161
+ return naturalDate.getTimeOfEvent(searchResult);
162
+ };
98
163
 
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));
164
+ const events = {
165
+ sunrise: getEventTime(SearchRiseSet(Body.Sun, observer, +1, nadir, 1), false),
166
+ sunset: getEventTime(SearchRiseSet(Body.Sun, observer, -1, nadir, 1), true),
167
+ nightStart: getEventTime(SearchAltitude(Body.Sun, observer, -1, nadir, 2, ANGLES.NIGHT_ALTITUDE), true),
168
+ nightEnd: getEventTime(SearchAltitude(Body.Sun, observer, +1, nadir, 2, ANGLES.NIGHT_ALTITUDE), false),
169
+ morningGoldenHour: getEventTime(SearchAltitude(Body.Sun, observer, +1, nadir, 2, ANGLES.GOLDEN_HOUR_ALTITUDE), false),
170
+ eveningGoldenHour: getEventTime(SearchAltitude(Body.Sun, observer, -1, nadir, 2, ANGLES.GOLDEN_HOUR_ALTITUDE), true)
171
+ };
102
172
 
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
173
+ astroCache.set(cacheKey, events);
174
+ return events;
175
+ } catch (error) {
176
+ console.error('Error in NaturalSunEvents:', error);
177
+ throw error;
107
178
  }
108
179
  }
109
180
 
110
181
  /**
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 }
182
+ * Calculates moon position and phase data for a given natural date and latitude
183
+ * @param {NaturalDate} naturalDate - Natural date instance
184
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
185
+ * @returns {{
186
+ * phase: number,
187
+ * altitude: number,
188
+ * highestAltitude: number
189
+ * }} Moon position and phase data
117
190
  */
118
- export function NaturalMoonEvents(naturalDate, latitude) {
191
+ export function NaturalMoonPosition(naturalDate, latitude) {
192
+ // Validate inputs
193
+ if (!isValidNaturalDate(naturalDate)) {
194
+ throwValidationError('naturalDate', naturalDate, 'NaturalDate instance');
195
+ }
196
+ if (!isValidLatitude(latitude)) {
197
+ throwValidationError('latitude', latitude, 'number between -90 and 90');
198
+ }
119
199
 
120
- let cache_id = `MOON_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
121
-
122
- if (ASTRO_CACHE[cache_id]) {
123
- return ASTRO_CACHE[cache_id];
200
+ try {
201
+ const observer = createObserver(latitude, naturalDate.longitude);
202
+ const date = new Date(naturalDate.unixTime);
203
+ const moon = Equator(Body.Moon, date, observer, true, false);
204
+
205
+ return {
206
+ phase: MoonPhase(date),
207
+ altitude: Math.max(Horizon(date, observer, moon.ra, moon.dec).altitude, 0),
208
+ highestAltitude: SearchHourAngle(Body.Moon, observer, 0, new Date(naturalDate.nadir)).hor.altitude
209
+ }
210
+ } catch (error) {
211
+ console.error('Error in NaturalMoonPosition:', error);
212
+ return {
213
+ phase: 0,
214
+ altitude: 0,
215
+ highestAltitude: 0
216
+ }
124
217
  }
218
+ }
125
219
 
126
- let observer = new Observer(latitude, naturalDate.longitude, 0);
220
+ /**
221
+ * Calculates moon rise and set events for a given natural date and latitude
222
+ * @param {NaturalDate} naturalDate - Natural date instance
223
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
224
+ * @returns {{
225
+ * moonrise: number|null,
226
+ * moonset: number|null
227
+ * }} Moon events in natural degrees (0-360)
228
+ */
229
+ export function NaturalMoonEvents(naturalDate, latitude) {
230
+ const cacheKey = `MOON_${naturalDate.toDateString()}_${latitude}_${naturalDate.longitude}`;
231
+
232
+ try {
233
+ if (astroCache.has(cacheKey)) {
234
+ return astroCache.get(cacheKey);
235
+ }
127
236
 
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);
237
+ const observer = createObserver(latitude, naturalDate.longitude);
238
+ const nadir = new Date(naturalDate.nadir);
131
239
 
132
- return ASTRO_CACHE[cache_id] = {
133
- moonrise: naturalDate.getTimeOfEvent(moonrise),
134
- moonset: naturalDate.getTimeOfEvent(moonset),
240
+ const moonrise = SearchRiseSet(Body.Moon, observer, +1, nadir, 1);
241
+ const moonset = SearchRiseSet(Body.Moon, observer, -1, nadir, 1);
242
+
243
+ const events = {
244
+ moonrise: naturalDate.getTimeOfEvent(moonrise),
245
+ moonset: naturalDate.getTimeOfEvent(moonset)
246
+ };
247
+
248
+ astroCache.set(cacheKey, events);
249
+ return events;
250
+ } catch (error) {
251
+ console.error('Error calculating moon events:', error);
252
+ return {
253
+ moonrise: null,
254
+ moonset: null
255
+ }
135
256
  }
136
257
  }
137
258
 
138
259
  /**
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 }
260
+ * Calculates the range of sun positions between winter and summer solstices (average mustaches angle)
261
+ * @param {NaturalDate} naturalDate - Natural date instance
262
+ * @param {number} latitude - Latitude in degrees (-90 to 90)
263
+ * @returns {{
264
+ * winterSunrise: number,
265
+ * winterSunset: number,
266
+ * summerSunrise: number,
267
+ * summerSunset: number,
268
+ * averageMustacheAngle: number
269
+ * }} Solstice sun positions and mustache angle
144
270
  */
145
271
  export function MustachesRange(naturalDate, latitude) {
146
- let cache_id = `MUSTACHES_${naturalDate.year}_${latitude}`;
272
+ const cacheKey = `MUSTACHES_${naturalDate.year}_${latitude}`;
147
273
 
148
- if (ASTRO_CACHE[cache_id]) {
149
- return ASTRO_CACHE[cache_id];
150
- }
274
+ try {
275
+ if (astroCache.has(cacheKey)) {
276
+ return astroCache.get(cacheKey);
277
+ }
151
278
 
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);
279
+ const currentYear = new Date(naturalDate.unixTime).getFullYear();
280
+ const currentSeasons = Seasons(currentYear);
281
+
282
+ const winterSolsticeSunEvents = NaturalSunEvents(
283
+ new NaturalDate(currentSeasons.dec_solstice.date, 0),
284
+ latitude
285
+ );
286
+ const summerSolsticeSunEvents = NaturalSunEvents(
287
+ new NaturalDate(currentSeasons.jun_solstice.date, 0),
288
+ latitude
289
+ );
156
290
 
157
- // Calculate average mustache angle from 90° and 270° (usefull for farnorth and farsouth locations where mustaches become unsymetric)
158
- let averageMustacheAngle = latitude >= 0 ?
159
- (winterSolsticeSunEvents.sunrise - summerSolsticeSunEvents.sunrise + summerSolsticeSunEvents.sunset - winterSolsticeSunEvents.sunset) / 4 :
160
- (summerSolsticeSunEvents.sunrise - winterSolsticeSunEvents.sunrise + winterSolsticeSunEvents.sunset - summerSolsticeSunEvents.sunset) / 4;
161
-
162
- averageMustacheAngle = Math.min(Math.max(averageMustacheAngle, 0), 90);
163
-
164
- return ASTRO_CACHE[cache_id] = {
165
- winterSunrise: winterSolsticeSunEvents.sunrise,
166
- winterSunset: winterSolsticeSunEvents.sunset,
167
- summerSunrise: summerSolsticeSunEvents.sunrise,
168
- summerSunset: summerSolsticeSunEvents.sunset,
169
- averageMustacheAngle: averageMustacheAngle
170
- };
291
+ const averageMustacheAngle = Math.min(Math.max(
292
+ latitude >= 0
293
+ ? (winterSolsticeSunEvents.sunrise - summerSolsticeSunEvents.sunrise +
294
+ summerSolsticeSunEvents.sunset - winterSolsticeSunEvents.sunset) / 4
295
+ : (summerSolsticeSunEvents.sunrise - winterSolsticeSunEvents.sunrise +
296
+ winterSolsticeSunEvents.sunset - summerSolsticeSunEvents.sunset) / 4,
297
+ 0
298
+ ), 90);
299
+
300
+ const result = {
301
+ winterSunrise: winterSolsticeSunEvents.sunrise,
302
+ winterSunset: winterSolsticeSunEvents.sunset,
303
+ summerSunrise: summerSolsticeSunEvents.sunrise,
304
+ summerSunset: summerSolsticeSunEvents.sunset,
305
+ averageMustacheAngle
306
+ };
307
+
308
+ astroCache.set(cacheKey, result);
309
+ return result;
310
+ } catch (error) {
311
+ console.error('Error calculating mustaches range:', error);
312
+ return {
313
+ winterSunrise: 0,
314
+ winterSunset: 0,
315
+ summerSunrise: 0,
316
+ summerSunset: 0,
317
+ averageMustacheAngle: 0
318
+ }
319
+ }
171
320
  }
package/index.js CHANGED
@@ -1,28 +1,71 @@
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';
2
7
 
3
- const NT_CACHE = {};
8
+ const yearContextCache = new Map();
4
9
 
5
10
  /**
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
- *
11
+ * Calculates year context for natural date calculations
12
+ * @param {number} artificialYear - Gregorian calendar year
13
+ * @param {number} longitude - Longitude in degrees (-180 to 180)
14
+ * @returns {{
15
+ * start: number,
16
+ * duration: number
17
+ * }} Year context with start timestamp and duration
18
+ * @private
19
+ */
20
+ const calculateYearContext = (artificialYear, longitude) => {
21
+ const cacheKey = `${artificialYear}_${longitude}`;
22
+
23
+ if (yearContextCache.has(cacheKey)) {
24
+ return yearContextCache.get(cacheKey);
25
+ }
26
+
27
+ const startSolstice = Seasons(artificialYear).dec_solstice.date;
28
+ const endSolstice = Seasons(artificialYear + 1).dec_solstice.date;
29
+
30
+ const startNewYear = Date.UTC(
31
+ startSolstice.getUTCFullYear(),
32
+ startSolstice.getUTCMonth(),
33
+ startSolstice.getUTCDate() + (startSolstice.getUTCHours() >= 12 ? 1 : 0),
34
+ 12, 0, 0
35
+ );
36
+
37
+ const endNewYear = Date.UTC(
38
+ endSolstice.getUTCFullYear(),
39
+ endSolstice.getUTCMonth(),
40
+ endSolstice.getUTCDate() + (endSolstice.getUTCHours() >= 12 ? 1 : 0),
41
+ 12, 0, 0
42
+ );
43
+
44
+ const context = {
45
+ start: parseInt(startNewYear + (-longitude + 180) * NaturalDate.MILLISECONDS_PER_DAY/360),
46
+ duration: (endNewYear - startNewYear) / NaturalDate.MILLISECONDS_PER_DAY
47
+ };
48
+
49
+ yearContextCache.set(cacheKey, context);
50
+ return context;
51
+ }
52
+
53
+ /**
54
+ * Natural date class for converting artificial (Gregorian) dates to natural time
55
+ * @class
21
56
  */
22
57
  export class NaturalDate {
58
+ /**
59
+ * Number of milliseconds in a day
60
+ * @constant {number}
61
+ */
62
+ static MILLISECONDS_PER_DAY = 86400000; // 24 * 60 * 60 * 1000
23
63
 
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
64
+ /**
65
+ * End date of the artificial time era (December 21, 2012 12:00 UTC)
66
+ * @constant {number}
67
+ */
68
+ static END_OF_ARTIFICIAL_TIME = Date.UTC(2012, 11, 21, 12, 0, 0);
26
69
 
27
70
  unixTime; // Artificial gregorian date (UNIX timestamp)
28
71
  longitude; // Longitude (between -180° to +180°)
@@ -47,54 +90,32 @@ export class NaturalDate {
47
90
  nadir; // Beginning of the day at the current longitude (UNIX timestamp)
48
91
 
49
92
  /**
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
93
+ * Creates a new natural date instance
94
+ * @param {Date|number} date - JavaScript Date object or Unix timestamp
95
+ * @param {number} longitude - Longitude in degrees (-180 to 180)
96
+ * @throws {Error} If inputs are invalid
53
97
  */
54
98
  constructor(date, longitude) {
55
-
99
+ // Validate longitude
100
+ if (longitude !== undefined && (typeof longitude !== 'number' || longitude < -180 || longitude > 180)) {
101
+ throw new Error('Longitude must be between -180 and +180');
102
+ }
103
+
56
104
  date = new Date(date || Date.now());
105
+ if (isNaN(date.getTime())) {
106
+ throw new Error('Invalid date provided');
107
+ }
108
+
57
109
  this.unixTime = date.getTime();
58
110
  this.longitude = longitude || 0;
59
111
 
60
112
  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;
72
-
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);
85
-
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
-
92
113
  // YEAR START & DURATION
93
- let yearContext = getYearContext(date.getUTCFullYear()-1, longitude);
114
+ let yearContext = calculateYearContext(date.getUTCFullYear()-1, longitude);
94
115
 
95
116
  // Correction if between the beginning of natural year and the end of the artificial year
96
117
  if(this.unixTime - yearContext.start >= yearContext.duration * NaturalDate.MILLISECONDS_PER_DAY)
97
- yearContext = getYearContext(date.getUTCFullYear(), longitude);
118
+ yearContext = calculateYearContext(date.getUTCFullYear(), longitude);
98
119
 
99
120
  this.yearStart = yearContext.start;
100
121
  this.yearDuration = yearContext.duration;
@@ -133,9 +154,9 @@ export class NaturalDate {
133
154
  }
134
155
 
135
156
  /**
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
157
+ * Gets the time of an astronomical event in natural degrees
158
+ * @param {Date|number} event - Event timestamp
159
+ * @returns {number|false} Event time in natural degrees (0-360) or false if out of range
139
160
  */
140
161
  getTimeOfEvent(event) {
141
162
  // Make sure it's a unix timestamp
@@ -153,7 +174,7 @@ export class NaturalDate {
153
174
  * @returns ex: "004)04)01 113°00 NT+95"
154
175
  */
155
176
  toString() {
156
- return this.toDateString() + " " + this.toTimeString() + " " + this.toLongitudeString();
177
+ return `${this.toDateString()} ${this.toTimeString()} ${this.toLongitudeString()}`;
157
178
  }
158
179
 
159
180
  /**
@@ -162,10 +183,10 @@ export class NaturalDate {
162
183
  * @returns ex: "004)04)01"
163
184
  */
164
185
  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()}`;
186
+ if(this.isRainbowDay) {
187
+ return `${this.toYearString()}${separator}RAINBOW${this.dayOfYear === 366 ? "+" : ""}`;
188
+ }
189
+ return `${this.toYearString()}${separator}${this.toMoonString()}${separator}${this.toDayOfMoonString()}`;
169
190
  }
170
191
 
171
192
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natural-time-js",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
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
+ };