Compare commits

..

No commits in common. "main" and "v2.0.1" have entirely different histories.
main ... v2.0.1

32 changed files with 125 additions and 1861 deletions

1
.claude/AGENTS.md Symbolic link
View file

@ -0,0 +1 @@
CLAUDE.md

View file

@ -2,33 +2,16 @@
**[Home](Home)**
**API**
- [API Reference](API-Reference)
- [spa()](api/spa)
- [spaFormatted()](api/spaFormatted)
- [formatTime()](api/formatTime)
- [init()](api/init)
- [SpaOptions](api/spa-options)
- [SpaResult](api/spa-result)
**Reference**
- [API Reference](API-Reference)
- [Architecture](Architecture)
- [NREL SPA Algorithm](NREL-SPA-Algorithm)
**Performance**
- [Bundle Size and Benchmarks](benchmarks/index)
**Performance & Compatibility**
- [Performance](Performance)
- [Validation and Benchmarks](Validation-and-Benchmarks)
- [Bundler Compatibility](Bundler-Compatibility)
- [WebAssembly in npm Packages](WebAssembly-in-npm-Packages)
**Guides**
- [Quick Start](guides/quickstart)
- [Advanced Usage](guides/advanced)
**Examples**
- [Annual Daylight Hours](examples/annual-daylight)
- [Solar Clock](examples/solar-clock)
- [Validation and Benchmarks](Validation-and-Benchmarks)
**Contributing**
- [Contributing](Contributing)

View file

@ -1,35 +0,0 @@
**solar-spa v2.0.1**
***
# solar-spa v2.0.1
## Interfaces
- [SpaFormattedResult](interfaces/SpaFormattedResult.md)
- [SpaOptions](interfaces/SpaOptions.md)
- [SpaResult](interfaces/SpaResult.md)
## Type Aliases
- [SpaFunctionCode](type-aliases/SpaFunctionCode.md)
## Variables
- [SPA\_ALL](variables/SPA_ALL.md)
- [SPA\_ZA](variables/SPA_ZA.md)
- [SPA\_ZA\_INC](variables/SPA_ZA_INC.md)
- [SPA\_ZA\_RTS](variables/SPA_ZA_RTS.md)
## Functions
- [formatTime](functions/formatTime.md)
- [init](functions/init.md)
- [spa](functions/spa.md)
- [spaFormatted](functions/spaFormatted.md)
## References
### default
Renames and re-exports [spa](functions/spa.md)

View file

@ -1,40 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / formatTime
# Function: formatTime()
> **formatTime**(`hours`): `string`
Defined in: [index.ts:113](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/index.ts#L113)
Purpose: Convert fractional hours to an HH:MM:SS string.
Inputs: hours, fractional hours (e.g. 6.5 for "06:30:00"); values ≥24 wrap
Outputs: "HH:MM:SS" string, or "N/A" for non-finite/negative inputs
Constraints: Non-finite and negative values occur during polar day/night;
returning "N/A" lets callers display a sensible label without special-casing.
SPORT: packages.md → solar-spa row
## Parameters
### hours
`number`
Fractional hours (e.g. 6.5 for 06:30:00). Values >= 24 wrap.
## Returns
`string`
Formatted time string in HH:MM:SS, or "N/A" for invalid input.
## Example
```ts
formatTime(6.5) // "06:30:00"
formatTime(12) // "12:00:00"
formatTime(Infinity) // "N/A"
```

View file

@ -1,32 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / init
# Function: init()
> **init**(): `Promise`\<`void`\>
Defined in: [index.ts:60](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/index.ts#L60)
Purpose: Pre-initialize the WASM module before the first spa() call.
Inputs: none
Outputs: Promise<void>; resolves when the module is ready
Constraints: Safe to call multiple times; subsequent calls return immediately.
If init() fails, the next call retries (failed promise is discarded).
SPORT: packages.md → solar-spa row
## Returns
`Promise`\<`void`\>
Promise that resolves when the WASM module is initialized.
## Example
```ts
import { init, spa } from 'solar-spa';
await init(); // pay WASM startup cost at app boot
const result = await spa(new Date(), 40.7128, -74.0060); // no init overhead
```

View file

@ -1,72 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / spa
# Function: spa()
> **spa**(`date`, `latitude`, `longitude`, `options?`): `Promise`\<[`SpaResult`](../interfaces/SpaResult.md)\>
Defined in: [index.ts:204](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/index.ts#L204)
Purpose: Compute solar position using the NREL SPA algorithm via WASM.
Inputs: date, latitude (-90..90), longitude (-180..180), optional SpaOptions
Outputs: Promise<SpaResult> with zenith, azimuth, incidence, sunrise/sunset/transit, eot
Constraints: WASM module is a singleton; first call incurs ~3-5 ms init cost.
Pass explicit timezone for server-side code (system may be UTC, not local).
Input validation runs before WASM call; invalid inputs throw before allocating.
SPORT: packages.md → solar-spa row
## Parameters
### date
`Date`
Date and time for the calculation
### latitude
`number`
Observer latitude in degrees (-90 to 90)
### longitude
`number`
Observer longitude in degrees (-180 to 180)
### options?
[`SpaOptions`](../interfaces/SpaOptions.md)
Optional observer and algorithm parameters (timezone, elevation, etc.)
## Returns
`Promise`\<[`SpaResult`](../interfaces/SpaResult.md)\>
Solar position result with all computed values
## Throws
If date is not a valid Date, or if latitude/longitude/option fields are not numbers
## Throws
If latitude/longitude are out of bounds, or if option fields are Infinity/NaN
## Throws
If WASM memory allocation fails or SPA returns a non-zero error code
## Example
```ts
import { spa } from 'solar-spa';
const result = await spa(new Date('2025-06-21T12:00:00Z'), 40.7128, -74.006, { timezone: -4 });
console.log(result.zenith); // ~27 degrees
console.log(result.sunrise); // ~5.4 fractional hours
```

View file

@ -1,72 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / spaFormatted
# Function: spaFormatted()
> **spaFormatted**(`date`, `latitude`, `longitude`, `options?`): `Promise`\<[`SpaFormattedResult`](../interfaces/SpaFormattedResult.md)\>
Defined in: [index.ts:290](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/index.ts#L290)
Purpose: Compute solar position with time fields formatted as HH:MM:SS strings.
Inputs: same as spa(): date, latitude, longitude, options
Outputs: Promise<SpaFormattedResult>; sunrise/sunset/suntransit are strings, all other fields numbers
Constraints: Delegates to spa() internally; throws under the same conditions.
"N/A" is returned for sunrise/sunset/suntransit during polar day or polar night.
SPORT: packages.md → solar-spa row
## Parameters
### date
`Date`
Date and time for the calculation
### latitude
`number`
Observer latitude in degrees (-90 to 90)
### longitude
`number`
Observer longitude in degrees (-180 to 180)
### options?
[`SpaOptions`](../interfaces/SpaOptions.md)
Optional observer and algorithm parameters
## Returns
`Promise`\<[`SpaFormattedResult`](../interfaces/SpaFormattedResult.md)\>
Solar position result with sunrise, sunset, suntransit as HH:MM:SS strings
## Throws
If date is not a valid Date, or if latitude/longitude/option fields are not numbers
## Throws
If latitude/longitude are out of bounds, or if option fields are Infinity/NaN
## Throws
If WASM memory allocation fails or SPA returns a non-zero error code
## Example
```ts
import { spaFormatted } from 'solar-spa';
const result = await spaFormatted(new Date('2025-06-21T12:00:00Z'), 40.7128, -74.006, { timezone: -4 });
console.log(result.sunrise); // "05:25:12"
console.log(result.suntransit); // "12:59:58"
console.log(result.sunset); // "20:34:47"
```

View file

@ -1,141 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SpaFormattedResult
# Interface: SpaFormattedResult
Defined in: [types.ts:65](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L65)
## Extends
- `Omit`\<[`SpaResult`](SpaResult.md), `"sunrise"` \| `"sunset"` \| `"suntransit"`\>
## Properties
### azimuth
> **azimuth**: `number`
Defined in: [types.ts:48](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L48)
Topocentric azimuth angle, eastward from north (navigational convention), in degrees.
#### Inherited from
[`SpaResult`](SpaResult.md).[`azimuth`](SpaResult.md#azimuth)
***
### azimuth\_astro
> **azimuth\_astro**: `number`
Defined in: [types.ts:46](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L46)
Topocentric azimuth angle, westward from south (astronomical convention), in degrees.
#### Inherited from
[`SpaResult`](SpaResult.md).[`azimuth_astro`](SpaResult.md#azimuth_astro)
***
### eot
> **eot**: `number`
Defined in: [types.ts:60](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L60)
Equation of time in minutes.
#### Inherited from
[`SpaResult`](SpaResult.md).[`eot`](SpaResult.md#eot)
***
### error\_code
> **error\_code**: `number`
Defined in: [types.ts:62](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L62)
SPA error code. Always 0 on a successful return (non-zero throws).
#### Inherited from
[`SpaResult`](SpaResult.md).[`error_code`](SpaResult.md#error_code)
***
### incidence
> **incidence**: `number`
Defined in: [types.ts:50](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L50)
Surface incidence angle in degrees.
#### Inherited from
[`SpaResult`](SpaResult.md).[`incidence`](SpaResult.md#incidence)
***
### sun\_transit\_alt
> **sun\_transit\_alt**: `number`
Defined in: [types.ts:58](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L58)
Sun transit altitude in degrees.
#### Inherited from
[`SpaResult`](SpaResult.md).[`sun_transit_alt`](SpaResult.md#sun_transit_alt)
***
### sunrise
> **sunrise**: `string`
Defined in: [types.ts:67](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L67)
Local sunrise time as HH:MM:SS string. "N/A" during polar day/night.
***
### sunset
> **sunset**: `string`
Defined in: [types.ts:69](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L69)
Local sunset time as HH:MM:SS string. "N/A" during polar day/night.
***
### suntransit
> **suntransit**: `string`
Defined in: [types.ts:71](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L71)
Local sun transit time as HH:MM:SS string. "N/A" during polar day/night.
***
### zenith
> **zenith**: `number`
Defined in: [types.ts:44](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L44)
Topocentric zenith angle in degrees.
#### Inherited from
[`SpaResult`](SpaResult.md).[`zenith`](SpaResult.md#zenith)

View file

@ -1,110 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SpaOptions
# Interface: SpaOptions
Defined in: [types.ts:16](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L16)
## Properties
### atmos\_refract?
> `optional` **atmos\_refract?**: `number`
Defined in: [types.ts:37](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L37)
Atmospheric refraction at sunrise/sunset in degrees. Default: 0.5667.
***
### azm\_rotation?
> `optional` **azm\_rotation?**: `number`
Defined in: [types.ts:35](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L35)
Surface azimuth rotation in degrees from south. Default: 0.
***
### delta\_t?
> `optional` **delta\_t?**: `number`
Defined in: [types.ts:31](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L31)
TT-UTC difference in seconds. Default: 67.
***
### delta\_ut1?
> `optional` **delta\_ut1?**: `number`
Defined in: [types.ts:29](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L29)
UT1-UTC correction in seconds. Default: 0.
***
### elevation?
> `optional` **elevation?**: `number`
Defined in: [types.ts:23](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L23)
Observer elevation in meters above sea level. Default: 0.
***
### function?
> `optional` **function?**: [`SpaFunctionCode`](../type-aliases/SpaFunctionCode.md)
Defined in: [types.ts:39](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L39)
SPA function code. Default: SPA_ALL (3).
***
### pressure?
> `optional` **pressure?**: `number`
Defined in: [types.ts:25](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L25)
Atmospheric pressure in millibars. Default: 1013.25.
***
### slope?
> `optional` **slope?**: `number`
Defined in: [types.ts:33](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L33)
Surface slope in degrees from horizontal. Default: 0.
***
### temperature?
> `optional` **temperature?**: `number`
Defined in: [types.ts:27](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L27)
Temperature in degrees Celsius. Default: 15.
***
### timezone?
> `optional` **timezone?**: `number`
Defined in: [types.ts:21](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L21)
Hours from UTC. If omitted, derived from the Date object's local offset.
For historical dates or DST transitions, pass an explicit value.

View file

@ -1,109 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SpaResult
# Interface: SpaResult
Defined in: [types.ts:42](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L42)
## Properties
### azimuth
> **azimuth**: `number`
Defined in: [types.ts:48](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L48)
Topocentric azimuth angle, eastward from north (navigational convention), in degrees.
***
### azimuth\_astro
> **azimuth\_astro**: `number`
Defined in: [types.ts:46](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L46)
Topocentric azimuth angle, westward from south (astronomical convention), in degrees.
***
### eot
> **eot**: `number`
Defined in: [types.ts:60](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L60)
Equation of time in minutes.
***
### error\_code
> **error\_code**: `number`
Defined in: [types.ts:62](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L62)
SPA error code. Always 0 on a successful return (non-zero throws).
***
### incidence
> **incidence**: `number`
Defined in: [types.ts:50](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L50)
Surface incidence angle in degrees.
***
### sun\_transit\_alt
> **sun\_transit\_alt**: `number`
Defined in: [types.ts:58](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L58)
Sun transit altitude in degrees.
***
### sunrise
> **sunrise**: `number`
Defined in: [types.ts:52](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L52)
Local sunrise time as fractional hours.
***
### sunset
> **sunset**: `number`
Defined in: [types.ts:54](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L54)
Local sunset time as fractional hours.
***
### suntransit
> **suntransit**: `number`
Defined in: [types.ts:56](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L56)
Local sun transit time (solar noon) as fractional hours.
***
### zenith
> **zenith**: `number`
Defined in: [types.ts:44](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L44)
Topocentric zenith angle in degrees.

View file

@ -1,11 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SpaFunctionCode
# Type Alias: SpaFunctionCode
> **SpaFunctionCode** = *typeof* [`SPA_ZA`](../variables/SPA_ZA.md) \| *typeof* [`SPA_ZA_INC`](../variables/SPA_ZA_INC.md) \| *typeof* [`SPA_ZA_RTS`](../variables/SPA_ZA_RTS.md) \| *typeof* [`SPA_ALL`](../variables/SPA_ALL.md)
Defined in: [types.ts:10](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L10)

View file

@ -1,13 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SPA\_ALL
# Variable: SPA\_ALL
> `const` **SPA\_ALL**: `3`
Defined in: [types.ts:8](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L8)
Compute all outputs: zenith, azimuth, incidence, and rise/transit/set.

View file

@ -1,13 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SPA\_ZA
# Variable: SPA\_ZA
> `const` **SPA\_ZA**: `0`
Defined in: [types.ts:2](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L2)
Compute zenith and azimuth only.

View file

@ -1,13 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SPA\_ZA\_INC
# Variable: SPA\_ZA\_INC
> `const` **SPA\_ZA\_INC**: `1`
Defined in: [types.ts:4](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L4)
Compute zenith, azimuth, and incidence angle.

View file

@ -1,13 +0,0 @@
[**solar-spa v2.0.1**](../README.md)
***
[solar-spa](../README.md) / SPA\_ZA\_RTS
# Variable: SPA\_ZA\_RTS
> `const` **SPA\_ZA\_RTS**: `2`
Defined in: [types.ts:6](https://github.com/acamarata/solar-spa/blob/3c848806ab852464d76baf7e3fc0ca58f5e42fd9/src/types.ts#L6)
Compute zenith, azimuth, and rise/transit/set times.

View file

@ -1,47 +0,0 @@
# Bundle Size and Performance
## Bundle size
Measured from the published npm package (`solar-spa@2.0.1`).
| File | Raw | Gzipped | Notes |
| --- | --- | --- | --- |
| `dist/index.mjs` | 5.3 KB | ~2.1 KB | JS wrapper, ESM |
| `dist/index.cjs` | 6.0 KB | ~2.3 KB | JS wrapper, CommonJS |
| `wasm/spa-module.cjs` | 58.6 KB | ~38 KB | Emscripten output with WASM binary inlined as base64 |
| Total installed | ~65 KB | ~40 KB | JS + WASM combined |
The WASM binary is inlined as base64 in `spa-module.cjs` (SINGLE_FILE mode). No external `.wasm` file is needed at runtime. The base64 encoding adds ~33% overhead over the raw binary size; the decoded WASM is approximately 44 KB.
For comparison, [nrel-spa](https://github.com/acamarata/nrel-spa) (the pure JavaScript port) is ~8 KB gzipped with no WASM dependency.
## Call latency
Single-call timings from the validation suite (`validate.mjs`), measured on Apple M2, Node.js 22:
| Scenario | Time |
| --- | --- |
| First call (includes WASM init) | ~3-5 ms |
| Subsequent calls (module cached) | 20-250 µs |
| Fastest observed (simple zenith/azimuth) | ~20 µs |
| Typical city scenario | 50-150 µs |
The first call initializes the WASM module, which adds 3-5 ms of one-time overhead. All subsequent calls reuse the cached module instance. Call [`init()`](../api/init) at startup to move this cost out of request paths.
## WASM vs pure JS
| Scenario | solar-spa (WASM) | nrel-spa (pure JS) | Ratio |
| --- | --- | --- | --- |
| Single call | ~100 µs | ~250 µs | ~2.5x faster |
| Batch 1000 calls | ~80 ms | ~200 ms | ~2.5x faster |
| First call with init | ~3-5 ms | ~0 ms | WASM has cold start |
For single-call use cases the cold start dominates. Use `init()` at app startup when single-call latency matters.
## Accuracy
The WASM module compiles the unmodified NREL SPA C source. All 100 validation scenarios in `validate.mjs` pass against physically-derived reference values. See [Validation and Benchmarks](../Validation-and-Benchmarks) for the full scenario table.
---
[Home](../Home) · [Performance](../Performance) · [Validation and Benchmarks](../Validation-and-Benchmarks)

View file

@ -1,42 +0,0 @@
# Example: Annual Daylight Hours
Compute total daylight hours for every day of a year and find the longest and shortest days.
```js
import { init, spa } from 'solar-spa';
const LAT = 51.5074; // London
const LON = -0.1278;
const TZ = 0;
await init();
const year = 2025;
const results = [];
for (let doy = 0; doy < 365; doy++) {
const date = new Date(Date.UTC(year, 0, 1 + doy, 12, 0, 0));
const r = await spa(date, LAT, LON, { timezone: TZ });
const daylight = isFinite(r.sunrise) ? r.sunset - r.sunrise : 0;
results.push({ date, daylight, sunrise: r.sunrise, sunset: r.sunset });
}
const longest = results.reduce((a, b) => a.daylight > b.daylight ? a : b);
const shortest = results.reduce((a, b) => a.daylight < b.daylight ? a : b);
console.log('London 2025');
console.log(`Longest day: ${longest.date.toISOString().slice(0, 10)} — ${longest.daylight.toFixed(2)} hours`);
console.log(`Shortest day: ${shortest.date.toISOString().slice(0, 10)} — ${shortest.daylight.toFixed(2)} hours`);
const total = results.reduce((sum, r) => sum + r.daylight, 0);
console.log(`Annual total: ${total.toFixed(0)} hours of daylight`);
```
Sample output:
```
London 2025
Longest day: 2025-06-21 — 16.44 hours
Shortest day: 2025-12-21 — 7.69 hours
Annual total: 4466 hours of daylight
```

View file

@ -1,41 +0,0 @@
# Example: Solar Clock
A simple Node.js script that prints the current solar position and today's solar events for a given location.
```js
import { spa, spaFormatted, init } from 'solar-spa';
const LAT = 40.7128; // New York
const LON = -74.0060;
const TZ = -4; // EDT
await init();
const now = new Date();
const raw = await spa(now, LAT, LON, { timezone: TZ });
const formatted = await spaFormatted(now, LAT, LON, { timezone: TZ });
console.log('Solar position right now');
console.log(` Zenith: ${raw.zenith.toFixed(2)}°`);
console.log(` Azimuth: ${raw.azimuth.toFixed(2)}°`);
console.log('');
console.log('Today\'s solar events');
console.log(` Sunrise: ${formatted.sunrise}`);
console.log(` Solar noon: ${formatted.suntransit}`);
console.log(` Sunset: ${formatted.sunset}`);
console.log(` Day length: ${(raw.sunset - raw.sunrise).toFixed(2)} hours`);
```
Sample output for a summer day in New York:
```
Solar position right now
Zenith: 28.14°
Azimuth: 214.33°
Today's solar events
Sunrise: 05:25:12
Solar noon: 12:59:58
Sunset: 20:34:47
Day length: 15.16 hours
```

View file

@ -1,108 +0,0 @@
# Advanced Usage
Edge cases, batch processing, and environment-specific notes.
## Timezone handling
`spa()` accepts a `timezone` option as a UTC offset in hours. If omitted, it reads from the `Date` object's built-in UTC offset.
```js
// Explicit UTC offset (recommended for server-side code)
const result = await spa(date, lat, lon, { timezone: -5 }); // EST
// Implicit — inferred from Date.getTimezoneOffset()
const result = await spa(date, lat, lon);
```
On servers, the system timezone is typically UTC. Pass an explicit `timezone` value when computing local solar events.
## Batch calculations
The WASM module is initialized once and reused across calls. Batch work is fast because there is no re-initialization overhead.
```js
import { init, spa } from 'solar-spa';
await init();
const dates = Array.from({ length: 365 }, (_, i) => {
const d = new Date('2025-01-01T12:00:00Z');
d.setUTCDate(d.getUTCDate() + i);
return d;
});
const results = await Promise.all(
dates.map(d => spa(d, 51.5074, -0.1278)), // London
);
const maxZenith = Math.min(...results.map(r => r.zenith));
console.log(`Lowest zenith (highest sun): ${maxZenith.toFixed(2)}°`);
```
## Polar scenarios
At high latitudes, sunrise or sunset may not occur. These fields return `NaN` in that case.
```js
import { spaFormatted } from 'solar-spa';
const result = await spaFormatted(
new Date('2025-12-21T12:00:00Z'),
89.0, // near North Pole
0,
);
console.log(result.sunrise); // "N/A" — polar night
console.log(result.sunset); // "N/A"
```
Use `isFinite(result.sunrise)` on the raw numeric result to detect polar conditions.
## Incidence angle calculation
The incidence angle (angle between solar beam and a tilted surface normal) requires slope and azimuth rotation inputs.
```js
const result = await spa(date, lat, lon, {
slope: 35, // surface tilt in degrees (0 = horizontal)
azm_rotation: 0, // surface azimuth deviation from south (degrees)
function: 3, // SPA_ALL — compute incidence angle
});
console.log(result.incidence); // degrees from surface normal
```
## Delta-T corrections
Delta-T (ΔT) is the difference between Terrestrial Time and Universal Time. The default (67 seconds) is accurate for dates near 2025. For historical or far-future dates, provide a more accurate value.
```js
// For 1900-01-01, ΔT ≈ -2.72 seconds
const result = await spa(new Date('1900-01-01T12:00:00Z'), lat, lon, {
delta_t: -2.72,
});
```
See the USNO delta-T tables for values outside the 2000-2050 range.
## Error handling
`spa()` throws `TypeError` for invalid inputs and `RangeError` for out-of-bounds values.
```js
try {
await spa(new Date('invalid'), 40, -74);
} catch (e) {
if (e instanceof TypeError) {
console.error('Invalid date');
}
}
```
A non-zero `error_code` in the result indicates an internal SPA calculation error, which should not occur with valid inputs.
## Related pages
- [Performance](../Performance) — benchmarks and optimization
- [Bundler Compatibility](../Bundler-Compatibility) — Webpack, Vite, Next.js notes
- [API Reference](../API-Reference) — full parameter documentation

View file

@ -1,73 +0,0 @@
# Quick Start
Five minutes from install to solar position.
## Install
```sh
npm install solar-spa
```
## Basic usage
```js
import { spa } from 'solar-spa';
const result = await spa(
new Date('2025-06-21T12:00:00Z'),
40.7128, // latitude
-74.0060, // longitude
);
console.log(result.zenith); // solar zenith angle in degrees
console.log(result.azimuth); // solar azimuth in degrees
console.log(result.sunrise); // fractional hours, e.g. 5.42
console.log(result.sunset); // fractional hours, e.g. 20.58
```
The module initializes the WASM binary on the first call. Subsequent calls reuse the same module instance.
## Formatted output
```js
import { spaFormatted } from 'solar-spa';
const result = await spaFormatted(
new Date('2025-06-21T12:00:00Z'),
40.7128,
-74.0060,
);
console.log(result.sunrise); // "05:25:12"
console.log(result.sunset); // "20:34:47"
console.log(result.suntransit); // "12:59:58"
```
## Options
```js
const result = await spa(date, lat, lon, {
elevation: 100, // metres above sea level
pressure: 1013.25, // millibars (default)
temperature: 15, // degrees Celsius (default)
delta_t: 67, // difference between UT1 and TT in seconds (default)
slope: 30, // surface slope in degrees (for incidence angle)
azm_rotation: 10, // surface azimuth rotation in degrees
});
```
## Eager initialization
Call `init()` at startup to avoid the first-call latency:
```js
import { init, spa } from 'solar-spa';
await init(); // WASM loads now, not on first spa() call
```
## Next steps
- [API Reference](../API-Reference) — full function signatures and return types
- [Architecture](../Architecture) — how the C/WASM/JS layers fit together
- [Advanced Guide](advanced) — batch calculations, custom options, timezone handling

View file

@ -15,14 +15,14 @@ jobs:
node-version: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
@ -40,14 +40,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
@ -58,14 +58,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
@ -75,14 +75,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run build:ts
@ -94,34 +94,3 @@ jobs:
grep -q "$f" pack-output.txt || { echo "MISSING: $f"; exit 1; }
done
echo "All expected files present in package"
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- name: Setup Node 24
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build:ts
- name: Coverage
run: pnpm run coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}

View file

@ -7,14 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [2.0.2] - 2026-05-30
### Changed
- Added `"type": "module"` to package.json per ADR-015 TypeScript source standard. The WASM compatibility issue from 2.0.1 is resolved: tsup now produces proper dual CJS/ESM dist files that Node resolves correctly via the exports map, so the package-level `type` field no longer conflicts with WASM module loading.
- Added `"./package.json"` exports entry for bundler compatibility.
- Added `"postbuild"` script to copy `dist/index.d.ts` to `dist/index.d.mts`.
- Adopted shared config packages (`@acamarata/tsconfig`, `@acamarata/eslint-config`, `@acamarata/prettier-config`).
## [2.0.1] - 2026-05-28
### Fixed

View file

@ -2,11 +2,9 @@
[![npm version](https://img.shields.io/npm/v/solar-spa.svg)](https://www.npmjs.com/package/solar-spa)
[![CI](https://github.com/acamarata/solar-spa/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/solar-spa/actions/workflows/ci.yml)
[![coverage](https://codecov.io/gh/acamarata/solar-spa/branch/main/graph/badge.svg)](https://codecov.io/gh/acamarata/solar-spa)
[![license](https://img.shields.io/npm/l/solar-spa.svg)](https://github.com/acamarata/solar-spa/blob/main/LICENSE)
[![wiki](https://img.shields.io/badge/docs-wiki-blue)](https://github.com/acamarata/solar-spa/wiki)
NREL Solar Position Algorithm compiled to WebAssembly. Calculates solar zenith, azimuth, incidence angle, sunrise, sunset, solar noon, and equation of time for any location and date. The WASM binary is inlined as base64, so there is no external `.wasm` file to locate. It works in Node.js, browsers, Webpack, Vite, Next.js, and web workers without configuration.
NREL Solar Position Algorithm compiled to WebAssembly. Calculates solar zenith, azimuth, incidence angle, sunrise, sunset, solar noon, and equation of time for any location and date. The WASM binary is inlined as base64, so there is no external `.wasm` file to locate — it works in Node.js, browsers, Webpack, Vite, Next.js, and web workers without configuration.
## Installation
@ -59,11 +57,6 @@ This package includes the Solar Position Algorithm (SPA) developed at the Nation
> Reda, I., Andreas, A. (2004). "Solar Position Algorithm for Solar Radiation Applications." *Solar Energy*, 76(5), 577-589.
## Telemetry
This package supports opt-in anonymous usage telemetry — off by default.
Enable: `ACAMARATA_TELEMETRY=1`. See [TELEMETRY.md](./TELEMETRY.md) for what is sent and how to disable.
## License
MIT (wrapper, TypeScript source, and build tooling). The NREL SPA C source (`src/spa.c`, `src/spa.h`) is subject to its own terms; see the notice in those files.

View file

@ -1,8 +0,0 @@
# Telemetry Disclosure
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
Full disclosure (what is sent, where it goes, how to disable):
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)

View file

@ -1,23 +1,12 @@
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
import { typescript } from '@acamarata/eslint-config';
export default [
{
files: ['src/**/*.ts'],
plugins: { '@typescript-eslint': tsPlugin },
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
},
...typescript.map((cfg) => ({ ...cfg, files: ['src/**/*.ts'] })),
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
{
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs', 'wasm/', 'src/spa.c', 'src/spa.h', 'validate.mjs'],
},
];
);

View file

@ -1,7 +1,6 @@
{
"type": "module",
"name": "solar-spa",
"version": "2.0.2",
"version": "2.0.1",
"description": "NREL Solar Position Algorithm (SPA) compiled to WebAssembly. High-performance solar position, sunrise, sunset, and solar noon calculations.",
"author": "Aric Camarata",
"license": "MIT",
@ -29,7 +28,7 @@
],
"scripts": {
"build:wasm": "emcc src/spa.c src/spa_wrapper.c -O3 -flto --no-entry -sMODULARIZE=1 -sEXPORT_NAME=createSpaModule -sSINGLE_FILE=1 -sEXPORTED_FUNCTIONS='[\"_spa_calculate_wrapper\",\"_spa_free_result\",\"_malloc\",\"_free\"]' -sEXPORTED_RUNTIME_METHODS='[\"cwrap\",\"getValue\"]' -sALLOW_MEMORY_GROWTH=0 -sINITIAL_MEMORY=1048576 -sSTACK_SIZE=65536 -sENVIRONMENT='node,web,worker' -sNO_FILESYSTEM=1 -sASSERTIONS=0 -sDISABLE_EXCEPTION_CATCHING=1 -sWASM_BIGINT=0 -o wasm/spa-module.js",
"build:ts": "tsup && cp dist/index.d.ts dist/index.d.mts",
"build:ts": "tsup",
"build": "pnpm run build:wasm && pnpm run build:ts",
"typecheck": "tsc --noEmit",
"pretest": "pnpm run build:ts",
@ -38,10 +37,8 @@
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"validate": "node validate.mjs",
"prepack": "pnpm run build:ts",
"coverage": "c8 --reporter=lcov --reporter=text node test.mjs",
"docs": "typedoc --out .github/wiki/api src/index.ts",
"postbuild": "cp dist/index.d.ts dist/index.d.mts"
"prepublishOnly": "pnpm run build:ts",
"coverage": "c8 --reporter=lcov --reporter=text node --test"
},
"keywords": [
"solar",
@ -73,24 +70,14 @@
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@acamarata/eslint-config": "^0.1.0",
"@acamarata/prettier-config": "^0.1.0",
"@acamarata/tsconfig": "^0.1.0",
"@eslint/js": "^10.0.1",
"@types/node": "^25.3.0",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"c8": "^10.1.3",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"tsup": "^8.5.1",
"typedoc": "^0.28.19",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"@acamarata/telemetry": "^0.1.0"
"typescript-eslint": "^8.56.1"
},
"packageManager": "pnpm@10.11.1",
"prettier": "@acamarata/prettier-config"
"packageManager": "pnpm@10.11.1"
}

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,17 @@
import type { SpaWasmModule, SpaResult, SpaFormattedResult, SpaOptions } from "./types.js";
import type { SpaWasmModule, SpaResult, SpaFormattedResult, SpaOptions } from './types.js';
export type { SpaOptions, SpaResult, SpaFormattedResult } from "./types.js";
export { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from "./types.js";
export type { SpaFunctionCode } from "./types.js";
export type { SpaOptions, SpaResult, SpaFormattedResult } from './types.js';
export { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from './types.js';
export type { SpaFunctionCode } from './types.js';
import { SPA_ALL } from "./types.js";
import { SPA_ALL } from './types.js';
// The WASM module is Emscripten CJS output. In ESM builds, tsup injects a
// createRequire-based __require shim via the banner option (see tsup.config.ts).
// In CJS builds, require() is natively available.
declare const __require: NodeRequire;
const _loadModule = typeof __require === "function" ? __require : require;
const createSpaModule: () => Promise<SpaWasmModule> = _loadModule("../wasm/spa-module.cjs");
const _loadModule = typeof __require === 'function' ? __require : require;
const createSpaModule: () => Promise<SpaWasmModule> = _loadModule('../wasm/spa-module.cjs');
// Singleton: the WASM module initializes once, all calls share it.
let _module: SpaWasmModule | null = null;
@ -44,18 +44,9 @@ const OFFSET = {
} as const;
/**
* Purpose: Pre-initialize the WASM module before the first spa() call.
* Inputs: none
* Outputs: Promise<void>; resolves when the module is ready
* Constraints: Safe to call multiple times; subsequent calls return immediately.
* If init() fails, the next call retries (failed promise is discarded).
* SPORT: packages.md solar-spa row
*
* @returns Promise that resolves when the WASM module is initialized.
* @example
* import { init, spa } from 'solar-spa';
* await init(); // pay WASM startup cost at app boot
* const result = await spa(new Date(), 40.7128, -74.0060); // no init overhead
* Initialize the WASM module. Returns a cached promise on repeat calls.
* Safe to call multiple times. If initialization fails, subsequent calls
* will retry rather than returning the failed promise.
*/
export function init(): Promise<void> {
if (_module) return Promise.resolve();
@ -64,27 +55,27 @@ export function init(): Promise<void> {
_pending = createSpaModule()
.then((mod: SpaWasmModule) => {
_module = mod;
_calculate = mod.cwrap("spa_calculate_wrapper", "number", [
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
"number",
_calculate = mod.cwrap('spa_calculate_wrapper', 'number', [
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
'number',
]) as (...args: number[]) => number;
_free = mod.cwrap("spa_free_result", null, ["number"]) as (ptr: number) => void;
_free = mod.cwrap('spa_free_result', null, ['number']) as (ptr: number) => void;
_pending = null;
})
.catch((err: unknown) => {
@ -96,22 +87,14 @@ export function init(): Promise<void> {
}
/**
* Purpose: Convert fractional hours to an HH:MM:SS string.
* Inputs: hours, fractional hours (e.g. 6.5 for "06:30:00"); values 24 wrap
* Outputs: "HH:MM:SS" string, or "N/A" for non-finite/negative inputs
* Constraints: Non-finite and negative values occur during polar day/night;
* returning "N/A" lets callers display a sensible label without special-casing.
* SPORT: packages.md solar-spa row
* Format fractional hours to HH:MM:SS string.
* Returns "N/A" for non-finite or negative values (polar night/day scenarios).
*
* @param hours - Fractional hours (e.g. 6.5 for 06:30:00). Values >= 24 wrap.
* @returns Formatted time string in HH:MM:SS, or "N/A" for invalid input.
* @example
* formatTime(6.5) // "06:30:00"
* formatTime(12) // "12:00:00"
* formatTime(Infinity) // "N/A"
* @returns Formatted time string in HH:MM:SS format, or "N/A" if input is invalid.
*/
export function formatTime(hours: number): string {
if (!isFinite(hours) || hours < 0) return "N/A";
if (!isFinite(hours) || hours < 0) return 'N/A';
const totalSec = Math.round(hours * 3600);
const h = Math.floor(totalSec / 3600) % 24;
@ -119,7 +102,7 @@ export function formatTime(hours: number): string {
const s = totalSec % 60;
return (
String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0")
String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0')
);
}
@ -127,16 +110,16 @@ export function formatTime(hours: number): string {
function readResult(ptr: number): SpaResult {
const m = _module!;
const result: SpaResult = {
zenith: m.getValue(ptr + OFFSET.zenith, "double"),
azimuth_astro: m.getValue(ptr + OFFSET.azimuth_astro, "double"),
azimuth: m.getValue(ptr + OFFSET.azimuth, "double"),
incidence: m.getValue(ptr + OFFSET.incidence, "double"),
sunrise: m.getValue(ptr + OFFSET.sunrise, "double"),
sunset: m.getValue(ptr + OFFSET.sunset, "double"),
suntransit: m.getValue(ptr + OFFSET.suntransit, "double"),
sun_transit_alt: m.getValue(ptr + OFFSET.sun_transit_alt, "double"),
eot: m.getValue(ptr + OFFSET.eot, "double"),
error_code: m.getValue(ptr + OFFSET.error_code, "i32"),
zenith: m.getValue(ptr + OFFSET.zenith, 'double'),
azimuth_astro: m.getValue(ptr + OFFSET.azimuth_astro, 'double'),
azimuth: m.getValue(ptr + OFFSET.azimuth, 'double'),
incidence: m.getValue(ptr + OFFSET.incidence, 'double'),
sunrise: m.getValue(ptr + OFFSET.sunrise, 'double'),
sunset: m.getValue(ptr + OFFSET.sunset, 'double'),
suntransit: m.getValue(ptr + OFFSET.suntransit, 'double'),
sun_transit_alt: m.getValue(ptr + OFFSET.sun_transit_alt, 'double'),
eot: m.getValue(ptr + OFFSET.eot, 'double'),
error_code: m.getValue(ptr + OFFSET.error_code, 'i32'),
};
_free!(ptr);
return result;
@ -147,7 +130,7 @@ function readResult(ptr: number): SpaResult {
* @internal
*/
function assertFiniteNumber(value: unknown, name: string): asserts value is number {
if (typeof value !== "number") {
if (typeof value !== 'number') {
throw new TypeError(`SPA: ${name} must be a finite number, got ${typeof value}`);
}
if (!isFinite(value)) {
@ -157,13 +140,13 @@ function assertFiniteNumber(value: unknown, name: string): asserts value is numb
/** Field names in SpaOptions that must be finite numbers when provided. */
const NUMERIC_OPTION_FIELDS = [
"elevation",
"pressure",
"temperature",
"delta_t",
"slope",
"azm_rotation",
"atmos_refract",
'elevation',
'pressure',
'temperature',
'delta_t',
'slope',
'azm_rotation',
'atmos_refract',
] as const;
/**
@ -179,27 +162,15 @@ function validateOptions(opts: SpaOptions): void {
}
/**
* Purpose: Compute solar position using the NREL SPA algorithm via WASM.
* Inputs: date, latitude (-90..90), longitude (-180..180), optional SpaOptions
* Outputs: Promise<SpaResult> with zenith, azimuth, incidence, sunrise/sunset/transit, eot
* Constraints: WASM module is a singleton; first call incurs ~3-5 ms init cost.
* Pass explicit timezone for server-side code (system may be UTC, not local).
* Input validation runs before WASM call; invalid inputs throw before allocating.
* SPORT: packages.md solar-spa row
* Compute solar position for the given parameters.
*
* @param date - Date and time for the calculation
* @param latitude - Observer latitude in degrees (-90 to 90)
* @param longitude - Observer longitude in degrees (-180 to 180)
* @param options - Optional observer and algorithm parameters (timezone, elevation, etc.)
* @param options - Optional parameters
* @returns Solar position result with all computed values
* @throws {TypeError} If date is not a valid Date, or if latitude/longitude/option fields are not numbers
* @throws {RangeError} If latitude/longitude are out of bounds, or if option fields are Infinity/NaN
* @throws {Error} If WASM memory allocation fails or SPA returns a non-zero error code
* @example
* import { spa } from 'solar-spa';
* const result = await spa(new Date('2025-06-21T12:00:00Z'), 40.7128, -74.006, { timezone: -4 });
* console.log(result.zenith); // ~27 degrees
* console.log(result.sunrise); // ~5.4 fractional hours
*/
export async function spa(
date: Date,
@ -209,10 +180,10 @@ export async function spa(
): Promise<SpaResult> {
// Input validation
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new TypeError("SPA: date must be a valid Date object");
throw new TypeError('SPA: date must be a valid Date object');
}
assertFiniteNumber(latitude, "latitude");
assertFiniteNumber(longitude, "longitude");
assertFiniteNumber(latitude, 'latitude');
assertFiniteNumber(longitude, 'longitude');
if (latitude < -90 || latitude > 90) {
throw new RangeError(`SPA: latitude must be between -90 and 90, got ${latitude}`);
@ -252,40 +223,26 @@ export async function spa(
);
if (!ptr) {
throw new Error("SPA: memory allocation failed");
throw new Error('SPA: memory allocation failed');
}
const result = readResult(ptr);
if (result.error_code !== 0) {
throw new Error("SPA: calculation failed (error code " + result.error_code + ")");
throw new Error('SPA: calculation failed (error code ' + result.error_code + ')');
}
return result;
}
/**
* Purpose: Compute solar position with time fields formatted as HH:MM:SS strings.
* Inputs: same as spa(): date, latitude, longitude, options
* Outputs: Promise<SpaFormattedResult>; sunrise/sunset/suntransit are strings, all other fields numbers
* Constraints: Delegates to spa() internally; throws under the same conditions.
* "N/A" is returned for sunrise/sunset/suntransit during polar day or polar night.
* SPORT: packages.md solar-spa row
* Compute solar position and return formatted time strings.
*
* Same parameters as spa(). Returns sunrise, sunset, and suntransit
* as HH:MM:SS strings instead of fractional hours.
*
* @param date - Date and time for the calculation
* @param latitude - Observer latitude in degrees (-90 to 90)
* @param longitude - Observer longitude in degrees (-180 to 180)
* @param options - Optional observer and algorithm parameters
* @returns Solar position result with sunrise, sunset, suntransit as HH:MM:SS strings
* @throws {TypeError} If date is not a valid Date, or if latitude/longitude/option fields are not numbers
* @throws {RangeError} If latitude/longitude are out of bounds, or if option fields are Infinity/NaN
* @throws {Error} If WASM memory allocation fails or SPA returns a non-zero error code
* @example
* import { spaFormatted } from 'solar-spa';
* const result = await spaFormatted(new Date('2025-06-21T12:00:00Z'), 40.7128, -74.006, { timezone: -4 });
* console.log(result.sunrise); // "05:25:12"
* console.log(result.suntransit); // "12:59:58"
* console.log(result.sunset); // "20:34:47"
*/
export async function spaFormatted(
date: Date,
@ -309,12 +266,3 @@ export async function spaFormatted(
}
export default spa;
// ── Opt-in anonymous telemetry ────────────────────────────────────────────────
// Off by default. Enable: ACAMARATA_TELEMETRY=1
// What is sent + how to disable: https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md
import("@acamarata/telemetry")
.then(({ track }) => track("load", { package: "solar-spa", version: "2.0.2" }))
.catch(() => {
// telemetry not installed or disabled — that is fine
});

View file

@ -62,7 +62,7 @@ export interface SpaResult {
error_code: number;
}
export interface SpaFormattedResult extends Omit<SpaResult, "sunrise" | "sunset" | "suntransit"> {
export interface SpaFormattedResult extends Omit<SpaResult, 'sunrise' | 'sunset' | 'suntransit'> {
/** Local sunrise time as HH:MM:SS string. "N/A" during polar day/night. */
sunrise: string;
/** Local sunset time as HH:MM:SS string. "N/A" during polar day/night. */

View file

@ -1,9 +1,19 @@
{
"extends": "@acamarata/tsconfig/tsconfig.library.json",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},

View file

@ -1,10 +0,0 @@
{
"entryPoints": ["src/index.ts"],
"out": ".github/wiki/api",
"plugin": ["typedoc-plugin-markdown"],
"readme": "none",
"skipErrorChecking": false,
"excludePrivate": true,
"excludeProtected": true,
"includeVersion": true
}