mirror of
https://github.com/acamarata/pray-calc-dart.git
synced 2026-07-01 19:34:26 +00:00
Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de3f9c8c67 | ||
|
|
b8923f49a6 | ||
|
|
178e990cbc | ||
|
|
fc01b129f4 | ||
|
|
882c586e2f |
12 changed files with 324 additions and 54 deletions
34
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
34
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as contributors and maintainers pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment:
|
||||||
|
|
||||||
|
- Using welcoming and inclusive language
|
||||||
|
- Being respectful of differing viewpoints and experiences
|
||||||
|
- Accepting constructive criticism gracefully
|
||||||
|
- Focusing on what is best for the community
|
||||||
|
- Showing empathy toward other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior:
|
||||||
|
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information without explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable behavior. They will take appropriate and fair corrective action in response to any behavior they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at alisalaah@gmail.com. All complaints will be reviewed and investigated. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
|
||||||
28
.github/wiki/SECURITY.md
vendored
Normal file
28
.github/wiki/SECURITY.md
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Security
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
`pray_calc_dart` is a pure-math library with no network access, no file I/O, and no external dependencies beyond `nrel_spa`. The attack surface is limited to the mathematical functions themselves.
|
||||||
|
|
||||||
|
The main concern is input validation: functions accept latitude (-90 to 90), longitude (-180 to 180), and UTC offset. Out-of-range values produce undefined behavior — clamp inputs to valid ranges before passing untrusted data.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security issue (for example, a case where malformed input causes unexpected behavior or crashes), please report it privately before filing a public issue.
|
||||||
|
|
||||||
|
**Contact:** alisalaah@gmail.com
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
1. A description of the vulnerability
|
||||||
|
2. Steps to reproduce it
|
||||||
|
3. The version of `pray_calc_dart` where you observed the issue
|
||||||
|
4. Any suggested fix if you have one
|
||||||
|
|
||||||
|
You can expect an acknowledgment within 48 hours and a resolution or status update within 7 days.
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- Prayer times are computed using the NREL Solar Position Algorithm. Accuracy is approximately one second relative to the reference implementation. Results near polar latitudes (above 65 degrees N/S) should be treated as estimates.
|
||||||
|
- The MCW seasonal model uses empirical piecewise-linear functions. Accuracy at extreme latitudes degrades gracefully rather than producing errors, but times may differ from local observation.
|
||||||
|
- Time zone handling is the caller's responsibility. The library accepts a UTC offset in hours and returns fractional hours in that offset. DST adjustments must be applied by the caller.
|
||||||
3
.github/wiki/_Footer.md
vendored
Normal file
3
.github/wiki/_Footer.md
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[](https://pub.dev/packages/pray_calc_dart) [](https://github.com/acamarata/pray-calc-dart/blob/main/LICENSE)
|
||||||
|
|
||||||
|
[pub.dev](https://pub.dev/packages/pray_calc_dart) | [GitHub](https://github.com/acamarata/pray-calc-dart) | [Issues](https://github.com/acamarata/pray-calc-dart/issues)
|
||||||
20
.github/wiki/_Sidebar.md
vendored
Normal file
20
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
## pray_calc_dart
|
||||||
|
|
||||||
|
- [Home](Home)
|
||||||
|
- [API Reference](API-Reference)
|
||||||
|
|
||||||
|
**Guides**
|
||||||
|
- [Quickstart](guides/quickstart)
|
||||||
|
- [Advanced Usage](guides/advanced)
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
- [Basic Usage](examples/basic-usage)
|
||||||
|
- [Flutter Integration](examples/flutter-integration)
|
||||||
|
|
||||||
|
**Project**
|
||||||
|
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||||
|
- [Security](SECURITY)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[pub.dev](https://pub.dev/packages/pray_calc_dart) | [GitHub](https://github.com/acamarata/pray-calc-dart)
|
||||||
122
.github/wiki/examples/flutter-integration.md
vendored
Normal file
122
.github/wiki/examples/flutter-integration.md
vendored
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Flutter Integration
|
||||||
|
|
||||||
|
This example shows how to use `pray_calc_dart` in a Flutter widget. It reads the device's current location using `geolocator` and displays the current prayer times for that location.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Add to `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
pray_calc_dart: ^1.0.0
|
||||||
|
geolocator: ^14.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## PrayerTimesCard Widget
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:pray_calc_dart/pray_calc_dart.dart';
|
||||||
|
|
||||||
|
class PrayerTimesCard extends StatefulWidget {
|
||||||
|
const PrayerTimesCard({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PrayerTimesCard> createState() => _PrayerTimesCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrayerTimesCardState extends State<PrayerTimesCard> {
|
||||||
|
PrayerTimes? _times;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadTimes();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadTimes() async {
|
||||||
|
try {
|
||||||
|
final permission = await Geolocator.requestPermission();
|
||||||
|
if (permission == LocationPermission.denied ||
|
||||||
|
permission == LocationPermission.deniedForever) {
|
||||||
|
setState(() => _error = 'Location permission denied.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pos = await Geolocator.getCurrentPosition();
|
||||||
|
|
||||||
|
// UTC offset in hours — derive from the device's local time.
|
||||||
|
final utcOffset =
|
||||||
|
DateTime.now().timeZoneOffset.inMinutes / 60.0;
|
||||||
|
|
||||||
|
final times = getTimes(
|
||||||
|
DateTime.now(),
|
||||||
|
pos.latitude,
|
||||||
|
pos.longitude,
|
||||||
|
utcOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() => _times = times);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _error = e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_error != null) {
|
||||||
|
return Text('Error: $_error');
|
||||||
|
}
|
||||||
|
if (_times == null) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
final rows = [
|
||||||
|
('Fajr', _times!.fajr),
|
||||||
|
('Sunrise', _times!.sunrise),
|
||||||
|
('Dhuhr', _times!.dhuhr),
|
||||||
|
('Asr', _times!.asr),
|
||||||
|
('Maghrib', _times!.maghrib),
|
||||||
|
('Isha', _times!.isha),
|
||||||
|
('Qiyam', _times!.qiyam),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: rows.map((r) {
|
||||||
|
final (name, hours) = r;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(formatTime(hours)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `getTimes` is a pure synchronous function. Only the location request is async.
|
||||||
|
- Pass `hanafi: true` to `getTimes` for the Hanafi Asr convention (shadow factor 2 instead of 1).
|
||||||
|
- `formatTime` returns `'N/A'` for unreachable events (polar nights). Handle this in the UI if targeting high-latitude users.
|
||||||
|
- For production use, cache the result and re-compute once per day or on location change. Prayer times change by only a few minutes per day at most latitudes.
|
||||||
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
||||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## 1.0.1 - 2026-06-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Prayer times are now host-timezone-independent. `getTimes` normalizes the
|
||||||
|
caller's `date` to a stable UTC-noon reference (`DateTime.utc(y, m, d, 12)`)
|
||||||
|
before passing it to `getSpa` and all astronomical calculations. Previously,
|
||||||
|
a local `DateTime(2024, 3, 15)` in a UTC+12 zone would reach `getSpa` as
|
||||||
|
UTC March 14, shifting all times by one civil day.
|
||||||
|
|
||||||
## [1.0.0] - 2026-05-25
|
## [1.0.0] - 2026-05-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
57
README.md
57
README.md
|
|
@ -3,6 +3,7 @@
|
||||||
[](https://pub.dev/packages/pray_calc_dart)
|
[](https://pub.dev/packages/pray_calc_dart)
|
||||||
[](https://github.com/acamarata/pray-calc-dart/actions/workflows/ci.yml)
|
[](https://github.com/acamarata/pray-calc-dart/actions/workflows/ci.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
[](https://github.com/acamarata/pray-calc-dart/wiki)
|
||||||
|
|
||||||
Islamic prayer times for Dart and Flutter. Pure Dart port of [pray-calc](https://github.com/acamarata/pray-calc), implementing the MCW seasonal model and dynamic twilight angles. Uses [nrel_spa](https://github.com/acamarata/nrel-spa-dart) for the NREL Solar Position Algorithm.
|
Islamic prayer times for Dart and Flutter. Pure Dart port of [pray-calc](https://github.com/acamarata/pray-calc), implementing the MCW seasonal model and dynamic twilight angles. Uses [nrel_spa](https://github.com/acamarata/nrel-spa-dart) for the NREL Solar Position Algorithm.
|
||||||
|
|
||||||
|
|
@ -34,61 +35,23 @@ void main() {
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `getTimes(date, lat, lng, tz, {elevation, temperature, pressure, hanafi})`
|
Full API documentation, guides, and examples are in the [wiki](https://github.com/acamarata/pray-calc-dart/wiki).
|
||||||
|
|
||||||
Computes all prayer times for a given date and location.
|
### Core functions
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Function | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- |
|
||||||
| `date` | `DateTime` | required | Local date (time-of-day ignored) |
|
| `getTimes(date, lat, lng, tz, {...})` | All prayer times for a date and location |
|
||||||
| `lat` | `double` | required | Latitude (-90 to 90, south negative) |
|
| `getAngles(date, lat, lng, {...})` | Dynamic Fajr/Isha depression angles |
|
||||||
| `lng` | `double` | required | Longitude (-180 to 180, west negative) |
|
| `getSpa(date, lat, lng, tz, {...})` | NREL Solar Position Algorithm (re-export) |
|
||||||
| `tz` | `double` | required | UTC offset in hours (e.g., -5 for EST) |
|
| `formatTime(hours)` | Fractional hours to `HH:MM:SS` string |
|
||||||
| `elevation` | `double` | 0 | Observer elevation in meters |
|
|
||||||
| `temperature` | `double` | 15 | Ambient temperature in Celsius |
|
|
||||||
| `pressure` | `double` | 1013.25 | Atmospheric pressure in mbar |
|
|
||||||
| `hanafi` | `bool` | false | Hanafi Asr (2x shadow) vs Shafi'i (1x) |
|
|
||||||
|
|
||||||
Returns a `PrayerTimes` object with fractional-hour values for: `qiyam`, `fajr`, `sunrise`, `noon`, `dhuhr`, `asr`, `maghrib`, `isha`, and the computed `angles`.
|
|
||||||
|
|
||||||
### `getAngles(date, lat, lng, {elevation, temperature, pressure})`
|
|
||||||
|
|
||||||
Computes dynamic twilight depression angles using the three-layer model:
|
|
||||||
|
|
||||||
1. MCW seasonal base (piecewise-linear, latitude-dependent)
|
|
||||||
2. Ephemeris corrections (Earth-Sun distance, Fourier season smoothing)
|
|
||||||
3. Environmental corrections (elevation dip, atmospheric refraction)
|
|
||||||
|
|
||||||
Returns `TwilightAngles` with `fajrAngle` and `ishaAngle` in degrees, clipped to [10, 22].
|
|
||||||
|
|
||||||
### `getSpa(date, lat, lng, tz, {...})`
|
|
||||||
|
|
||||||
Full NREL Solar Position Algorithm. Accurate to +/-0.0003 degrees for zenith angle. Supports custom zenith angles for twilight calculations.
|
|
||||||
|
|
||||||
### `formatTime(hours)`
|
|
||||||
|
|
||||||
Converts fractional hours to an `HH:MM:SS` string. Returns `"N/A"` for non-finite values.
|
|
||||||
|
|
||||||
### Additional Functions
|
|
||||||
|
|
||||||
- `solarEphemeris(jd)` -- Jean Meeus Ch. 25 low-precision ephemeris
|
|
||||||
- `toJulianDate(date)` -- DateTime to Julian Date
|
|
||||||
- `getAsr(solarNoon, latitude, declination, {hanafi})` -- Asr computation
|
|
||||||
- `getQiyam(fajrTime, ishaTime)` -- Last third of the night
|
|
||||||
- `getMscFajr(date, latitude)` -- MCW Fajr offset in minutes
|
|
||||||
- `getMscIsha(date, latitude, [shafaq])` -- MCW Isha offset in minutes
|
|
||||||
- `minutesToDepression(minutes, latDeg, declDeg)` -- Time to angle conversion
|
|
||||||
|
|
||||||
## Dynamic Angle Algorithm
|
## Dynamic Angle Algorithm
|
||||||
|
|
||||||
Fixed-angle methods (ISNA 15 degrees, MWL 18 degrees, etc.) produce inaccurate Fajr times at latitudes above 45 degrees N/S. The dynamic method adapts the depression angle based on season, latitude, Earth-Sun distance, and local atmospheric conditions.
|
Fixed-angle methods (ISNA 15 degrees, MWL 18 degrees) produce inaccurate Fajr times at latitudes above 45 degrees N/S. The dynamic method adapts the depression angle based on season, latitude, Earth-Sun distance, and local atmospheric conditions.
|
||||||
|
|
||||||
Result: approximately 18 degrees at the equator, approximately 12-14 degrees at 50-55 degrees N in summer. Matches observational data from the Moonsighting Committee Worldwide.
|
Result: approximately 18 degrees at the equator, approximately 12-14 degrees at 50-55 degrees N in summer. Matches observational data from the Moonsighting Committee Worldwide.
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Built in three layers: (1) [nrel_spa](https://pub.dev/packages/nrel_spa) provides the solar ephemeris foundation; (2) the MSC piecewise model computes seasonal minute offsets which are converted to depression angles via spherical trigonometry; (3) physics corrections (Earth-Sun distance, refraction, elevation dip) adjust the angle within [10°, 22°] bounds.
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
Dart SDK 3.7.0+. Works in Flutter (iOS, Android, Web, Desktop), Dart CLI, and server-side Dart. Single dependency: [nrel_spa](https://pub.dev/packages/nrel_spa).
|
Dart SDK 3.7.0+. Works in Flutter (iOS, Android, Web, Desktop), Dart CLI, and server-side Dart. Single dependency: [nrel_spa](https://pub.dev/packages/nrel_spa).
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ TwilightAngles getAngles(
|
||||||
double pressure = 1013.25,
|
double pressure = 1013.25,
|
||||||
}) {
|
}) {
|
||||||
// 1. Solar ephemeris at UTC noon of the given date.
|
// 1. Solar ephemeris at UTC noon of the given date.
|
||||||
|
// date.year/month/day are read without TZ conversion so they reflect the
|
||||||
|
// caller's expressed civil date for both local and UTC DateTime inputs.
|
||||||
final noonDate = DateTime.utc(date.year, date.month, date.day, 12, 0, 0);
|
final noonDate = DateTime.utc(date.year, date.month, date.day, 12, 0, 0);
|
||||||
final jd = toJulianDate(noonDate);
|
final jd = toJulianDate(noonDate);
|
||||||
final eph = solarEphemeris(jd);
|
final eph = solarEphemeris(jd);
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,17 @@ PrayerTimes getTimes(
|
||||||
double pressure = 1013.25,
|
double pressure = 1013.25,
|
||||||
bool hanafi = false,
|
bool hanafi = false,
|
||||||
}) {
|
}) {
|
||||||
|
// Normalize to a stable UTC-noon DateTime for this civil calendar date.
|
||||||
|
// Reading date.year/month/day directly (without TZ conversion) preserves
|
||||||
|
// the caller's expressed date regardless of whether they passed a local or
|
||||||
|
// UTC DateTime. Constructing UTC noon removes host-timezone influence from
|
||||||
|
// every downstream Julian-Day computation and aligns getSpa with the
|
||||||
|
// Meeus/MSC calculations (which all need the same civil date).
|
||||||
|
final civDate = DateTime.utc(date.year, date.month, date.day, 12, 0, 0);
|
||||||
|
|
||||||
// 1. Compute dynamic twilight angles.
|
// 1. Compute dynamic twilight angles.
|
||||||
final tw = getAngles(
|
final tw = getAngles(
|
||||||
date,
|
civDate,
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
elevation: elevation,
|
elevation: elevation,
|
||||||
|
|
@ -49,8 +57,11 @@ PrayerTimes getTimes(
|
||||||
final ishaZenith = 90 + tw.ishaAngle;
|
final ishaZenith = 90 + tw.ishaAngle;
|
||||||
|
|
||||||
// 3. Run SPA for solar position + custom twilight times.
|
// 3. Run SPA for solar position + custom twilight times.
|
||||||
|
// Pass civDate (UTC noon) so SPA receives a deterministic UTC instant
|
||||||
|
// and the date component it extracts (via date.toUtc()) is always the
|
||||||
|
// intended civil day, independent of host timezone.
|
||||||
final spaData = getSpa(
|
final spaData = getSpa(
|
||||||
date,
|
civDate,
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
tz,
|
tz,
|
||||||
|
|
@ -70,9 +81,8 @@ PrayerTimes getTimes(
|
||||||
final dhuhrTime = noonTime + 2.5 / 60;
|
final dhuhrTime = noonTime + 2.5 / 60;
|
||||||
|
|
||||||
// 4. Solar declination for Asr (Meeus formula, accurate to ~0.01°).
|
// 4. Solar declination for Asr (Meeus formula, accurate to ~0.01°).
|
||||||
final jd = toJulianDate(
|
// civDate already is UTC noon of the civil date.
|
||||||
DateTime.utc(date.year, date.month, date.day, 12, 0, 0),
|
final jd = toJulianDate(civDate);
|
||||||
);
|
|
||||||
final eph = solarEphemeris(jd);
|
final eph = solarEphemeris(jd);
|
||||||
|
|
||||||
// 5. Asr time.
|
// 5. Asr time.
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,31 @@ class PrayerTimes {
|
||||||
|
|
||||||
/// Prayer times formatted as HH:MM:SS strings.
|
/// Prayer times formatted as HH:MM:SS strings.
|
||||||
class FormattedPrayerTimes {
|
class FormattedPrayerTimes {
|
||||||
|
/// Start of the last third of the night (Qiyam al-Layl), as HH:MM:SS.
|
||||||
final String qiyam;
|
final String qiyam;
|
||||||
|
|
||||||
|
/// True dawn (Subh Sadiq), as HH:MM:SS.
|
||||||
final String fajr;
|
final String fajr;
|
||||||
|
|
||||||
|
/// Astronomical sunrise, as HH:MM:SS.
|
||||||
final String sunrise;
|
final String sunrise;
|
||||||
|
|
||||||
|
/// Solar noon (exact geometric transit), as HH:MM:SS.
|
||||||
final String noon;
|
final String noon;
|
||||||
|
|
||||||
|
/// Dhuhr (2.5 minutes after solar noon), as HH:MM:SS.
|
||||||
final String dhuhr;
|
final String dhuhr;
|
||||||
|
|
||||||
|
/// Asr (Shafi'i or Hanafi shadow convention), as HH:MM:SS.
|
||||||
final String asr;
|
final String asr;
|
||||||
|
|
||||||
|
/// Maghrib (sunset), as HH:MM:SS.
|
||||||
final String maghrib;
|
final String maghrib;
|
||||||
|
|
||||||
|
/// Isha (nightfall, end of shafaq), as HH:MM:SS.
|
||||||
final String isha;
|
final String isha;
|
||||||
|
|
||||||
|
/// Dynamic twilight angles used for this calculation.
|
||||||
final TwilightAngles angles;
|
final TwilightAngles angles;
|
||||||
|
|
||||||
const FormattedPrayerTimes({
|
const FormattedPrayerTimes({
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ description: >
|
||||||
Islamic prayer times for Dart and Flutter. Pure Dart port of the pray-calc
|
Islamic prayer times for Dart and Flutter. Pure Dart port of the pray-calc
|
||||||
library implementing the MCW seasonal model and dynamic twilight angles.
|
library implementing the MCW seasonal model and dynamic twilight angles.
|
||||||
Uses nrel_spa for the NREL Solar Position Algorithm.
|
Uses nrel_spa for the NREL Solar Position Algorithm.
|
||||||
version: 1.0.0
|
version: 1.0.1
|
||||||
homepage: https://github.com/acamarata/pray-calc-dart
|
homepage: https://github.com/acamarata/pray-calc-dart
|
||||||
repository: https://github.com/acamarata/pray-calc-dart
|
repository: https://github.com/acamarata/pray-calc-dart
|
||||||
issue_tracker: https://github.com/acamarata/pray-calc-dart/issues
|
issue_tracker: https://github.com/acamarata/pray-calc-dart/issues
|
||||||
publisher: ariccamarata.com
|
|
||||||
topics:
|
topics:
|
||||||
- prayer-times
|
- prayer-times
|
||||||
- islamic
|
- islamic
|
||||||
|
|
|
||||||
|
|
@ -190,4 +190,66 @@ void main() {
|
||||||
expect(result.angles[0].sunset, lessThan(result.angles[1].sunset));
|
expect(result.angles[0].sunset, lessThan(result.angles[1].sunset));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('UTC day-boundary regression — civil-date consistency', () {
|
||||||
|
// getTimes normalizes the input date to UTC noon of the expressed civil
|
||||||
|
// date before passing it to getSpa and all astronomical calculations.
|
||||||
|
// This ensures that a local DateTime and a UTC DateTime that both express
|
||||||
|
// "March 15" (via their .year/.month/.day fields) produce identical prayer
|
||||||
|
// times, regardless of what the host machine timezone is.
|
||||||
|
|
||||||
|
test(
|
||||||
|
'local DateTime and UTC noon for same civil date produce identical times',
|
||||||
|
() {
|
||||||
|
// Both express 2024-03-15: local midnight and UTC noon share year/month/day = 2024/3/15
|
||||||
|
final local = DateTime(2024, 3, 15);
|
||||||
|
final utcNoon = DateTime.utc(2024, 3, 15, 12, 0, 0);
|
||||||
|
|
||||||
|
final timesLocal = getTimes(local, 40.7128, -74.0060, -5.0);
|
||||||
|
final timesNoon = getTimes(utcNoon, 40.7128, -74.0060, -5.0);
|
||||||
|
|
||||||
|
// After normalization both resolve to the same UTC noon reference point
|
||||||
|
expect(timesNoon.fajr, closeTo(timesLocal.fajr, 0.0001));
|
||||||
|
expect(timesNoon.sunrise, closeTo(timesLocal.sunrise, 0.0001));
|
||||||
|
expect(timesNoon.dhuhr, closeTo(timesLocal.dhuhr, 0.0001));
|
||||||
|
expect(timesNoon.asr, closeTo(timesLocal.asr, 0.0001));
|
||||||
|
expect(timesNoon.maghrib, closeTo(timesLocal.maghrib, 0.0001));
|
||||||
|
expect(timesNoon.isha, closeTo(timesLocal.isha, 0.0001));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'UTC midnight and UTC noon for same civil date produce identical times',
|
||||||
|
() {
|
||||||
|
final utcMidnight = DateTime.utc(2024, 3, 15, 0, 0, 0);
|
||||||
|
final utcNoon = DateTime.utc(2024, 3, 15, 12, 0, 0);
|
||||||
|
|
||||||
|
final timesMid = getTimes(utcMidnight, 40.7128, -74.0060, -5.0);
|
||||||
|
final timesNoon = getTimes(utcNoon, 40.7128, -74.0060, -5.0);
|
||||||
|
|
||||||
|
expect(timesNoon.fajr, closeTo(timesMid.fajr, 0.0001));
|
||||||
|
expect(timesNoon.sunrise, closeTo(timesMid.sunrise, 0.0001));
|
||||||
|
expect(timesNoon.dhuhr, closeTo(timesMid.dhuhr, 0.0001));
|
||||||
|
expect(timesNoon.asr, closeTo(timesMid.asr, 0.0001));
|
||||||
|
expect(timesNoon.maghrib, closeTo(timesMid.maghrib, 0.0001));
|
||||||
|
expect(timesNoon.isha, closeTo(timesMid.isha, 0.0001));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'angles are identical for local vs UTC noon for the same civil date',
|
||||||
|
() {
|
||||||
|
final local = DateTime(2024, 6, 15);
|
||||||
|
final utcNoon = DateTime.utc(2024, 6, 15, 12, 0, 0);
|
||||||
|
|
||||||
|
// getAngles receives civDate (already UTC noon from getTimes) but can
|
||||||
|
// also be called directly — test that same civil date gives same angles.
|
||||||
|
final anglesLocal = getAngles(local, 40.7128, -74.0060);
|
||||||
|
final anglesUtcNoon = getAngles(utcNoon, 40.7128, -74.0060);
|
||||||
|
|
||||||
|
expect(anglesUtcNoon.fajrAngle, closeTo(anglesLocal.fajrAngle, 0.001));
|
||||||
|
expect(anglesUtcNoon.ishaAngle, closeTo(anglesLocal.ishaAngle, 0.001));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue