batoulapps / adhan-js

High precision Islamic prayer time library for JavaScript

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Feature Suggestion: Add Next Day Parameter

farisfaisalthena opened this issue · comments

It would be great if theres a next day parameter because if i use var nextPrayerTime = this.time.timeForPrayer(next); and the last prayers time is shown it will return as undefined. Would be great if it shows the next day time.

I recommend in general always calculating times for today and tomorrow, then when showing next prayer time you can check if you're getting an undefined from today's prayer times and choose to show the next prayer time for tomorrow's prayer times.

This approach has the advantage of signaling to the developer that there are no more prayer times for the current day. You may use this information to do other things in your application as well.

Both situations have their lots of advantages.

I think it's definitely a pretty common use case for the developer having to tackle this situation and for this reason I agree it would be extremly helpful to have it done natively.

I also agree that having an undefined result might be useful is some cases.

To extend the discussion, this does not only concern timeForNextPrayer but it concerns all these 3 calculations:

  timeForPrayer(prayer: Prayer): Date;
  currentPrayer(date?: Date): Prayer;
  nextPrayer(date?: Date): Prayer;

My suggestion is to add a parameter to CalculationParameters :

export class CalculationParameters {
  constructor(fajrAngle: number, ishaAngle: number, methodName?: string, ishaInterval?: number, maghribAngle?: number)

  readonly method: string;
  fajrAngle: number;
  ishaAngle: number;
  ...
  daysSlidingCalculation: boolean
}

The current API is geared towards single day calculations. The PrayerTimes object is for a single day and the timeForPrayer, currentPrayer, and nextPrayer functions are on that object.

If we wanted to accommodate automatically extending nextPrayer into tomorrow we would probably want to make a new object that contains a pair of PrayerTimes objects, one for today and one for tomorrow. We would then want new function signatures that can indicate if the next prayer time is for today or tomorrow and the timeForPrayer function would also need to know if you want it for today or tomorrow.

@z3bi What is your opinion? Should we work on it?

@korbav Yeah I think that would be a good addition.

Here's a quick idea of how you could organize it:

class PrayerSchedule
    .today: PrayerTimes
    .tomorrow: PrayerTimes

    function timeForPrayer(prayer: Prayer, today: Boolean)
    function nextPrayer() -> { prayer: Prayer, today: Boolean }
    function currentPrayer() -> prayer: Prayer

@korbav Yeah I think that would be a good addition.

Here's a quick idea of how you could organize it:

class PrayerSchedule
    .today: PrayerTimes
    .tomorrow: PrayerTimes

    function timeForPrayer(prayer: Prayer, today: Boolean)
    function nextPrayer() -> { prayer: Prayer, today: Boolean }
    function currentPrayer() -> prayer: Prayer

Great, what would you think of this implementation?

Note: As you will notice, I also tried to fix an unexpected behavior which will never happen in most of the cases, but, for some locations, in particular during summer in Western Europe, Isha will happen after midnight :

Let's say that we have the following time table :

Day Time of Isha prayer
June, 27th 00:15
June, 28th 00:20

Now, let's say we're computing the prayers times on the 27th of June, at 00:05.
Instead of getting June, 27th 00:15, we currently get June, 28th 00:20, which is wrong.

I had noticed this behavior some time ago and I think the PrayerSchedule is a great opportunity to handle this case.

import {
  dateByAddingDays,

} from './DateUtils';
import PrayerTimes from './PrayerTimes';
import Prayer from './Prayer';

const _prayersNames = [Prayer.Fajr, Prayer.Sunrise, Prayer.Dhuhr, Prayer.Asr, Prayer.Maghrib, Prayer.Isha];

const _getPrayers = (coordinates, calculationParameters, date = new Date()) => {
  const dayBefore = new PrayerTimes(coordinates, dateByAddingDays(date, -1), calculationParameters);
  const currentDay = new PrayerTimes(coordinates, date, calculationParameters);
  const dayAfter = new PrayerTimes(coordinates, dateByAddingDays(date, +1), calculationParameters);
  const prayers = _prayersNames.map((id) => {
    // If the prayer for the day before has not yet occurred, we still return it
    // Will fix an unexpected behavior happening, for example, when Isha time is after midnight day X
    // and we compute the prayer times the day X between midnight & Isha time
    // Without this fix, it would return Isha for day X+1 even if Isha of day X has not yet occurred
    const time = dayBefore[id] >= date ? dayBefore[id] : currentDay[id];
    return {
      id,
      time: time,
      timeDiff: time.getTime() - date.getTime(),
    };
  });
  return {
    prayers,
    futurePrayers: prayers.filter(p => p.timeDiff > 0).sort((p1, p2) => p1.timeDiff - p2.timeDiff),
    pastPrayers: prayers.filter(p => p.timeDiff <= 0).sort((p1, p2) => p1.timeDiff - p2.timeDiff),
    dayAfter
  };
}

export default class PrayerSchedule {
  constructor(coordinates, date, calculationParameters) {
    this.coordinates = coordinates;
    this.date = date;
    this.calculationParameters = calculationParameters;
    _getPrayers(coordinates, calculationParameters, date).prayers.forEach(({ id, time }) => {
      this[id] = time;
    });
  }

  /**
   *
   * @param prayer string, id of the prayer
   * @param upcoming boolean, if true will only return the time for the next occurrence of the current prayer, else will return the one for the current day (which may be already past or not)
   * @returns {Date}
   */
  timeForPrayer(prayer, upcoming = true) {
    const { prayers, dayAfter } = _getPrayers(this.coordinates, this.calculationParameters, this.date);
    const time = prayers.filter(p => p.id === prayer)[0].time;
    return (upcoming && time < this.date) ? dayAfter[prayer] : time;
  }

  /**
   * Returns the date of the last prayer, if the current date is inferior to fajr, will return Prayer.Isha
   * @param date Date?
   * @returns {string}
   */
  currentPrayer(date) {
    const { pastPrayers } = _getPrayers(this.coordinates, this.calculationParameters, date);
    return pastPrayers.length ? pastPrayers[pastPrayers.length - 1].id : Prayer.Isha;
  }

  /**
   * Returns the date of the upcoming prayer, if there's no more prayer for the current day, will return Prayer.Fajr
   * @param date Date?
   * @returns {string}
   */
  nextPrayer(date) {
    const { futurePrayers } = _getPrayers(this.coordinates, this.calculationParameters, date);
    return futurePrayers.length ? futurePrayers[0].id : Prayer.Fajr;
  }
}

About the types :

export class PrayerSchedule {
  constructor(coordinates: Coordinates, date: Date, params: CalculationParameters);

  fajr: Date;
  sunrise: Date;
  dhuhr: Date;
  asr: Date;
  maghrib: Date;
  isha: Date;

  timeForPrayer(prayer: Prayer, upcoming: boolean): Date;
  currentPrayer(date?: Date): Prayer;
  nextPrayer(date?: Date): Prayer;
}

@korbav while the library can technically produce an isha time after midnight, this is an indication that you are not using a high latitude rule which should prevent such an unreasonable prayer time.

@korbav calling _getPrayers inside each of the convenience functions seems unnecessary. We should probably store the results of this call that we already make inside the constructor.

@korbav while the library can technically produce an isha time after midnight, this is an indication that you are not using a high latitude rule which should prevent such an unreasonable prayer time.

Sorry to rectify you but we have been praying Isha after midnight during the end of June in Western Europe for decades, so it's a pretty common and normal thing, mosques or individuals are dealing with that every year.

@korbav calling _getPrayers inside each of the convenience functions seems unnecessary. We should probably store the results of this call that we already make inside the constructor.

The problem is that the constructor takes a date parameter, likewise these methods, so, whatever we would store would be correlated to a date and might be wrong for any further usage.
A slight optimization we can do is to use the stored value whenever we call currentPrayer or nextPrayer without a date.

we have been praying Isha after midnight during the end of June in Western Europe for decades, so it's a pretty common and normal thing, mosques or individuals are dealing with that every year.

Oh, very interesting. What location prays Isha after midnight? My understanding is that for these locations in the summer, twilight does not truly end which is what causes such extreme timings from the calculations. At that point the scholarly ruling of preventing hardship should allow people in that area to use the "high latitude rule" to approximate more normal times. But I'm very happy to be corrected :-)

The problem is that the constructor takes a date parameter, likewise these methods, so, whatever we would store would be correlated to a date and might be wrong for any further usage.
A slight optimization we can do is to use the stored value whenever we call currentPrayer or nextPrayer without a date.

That is currently how PrayerTimes itself also works. The date is passed into the constructor and the resulting prayer times are stored. It is not correct for the developer to modify the date property after initialization.

we have been praying Isha after midnight during the end of June in Western Europe for decades, so it's a pretty common and normal thing, mosques or individuals are dealing with that every year.

Oh, very interesting. What location prays Isha after midnight? My understanding is that for these locations in the summer, twilight does not truly end which is what causes such extreme timings from the calculations. At that point the scholarly ruling of preventing hardship should allow people in that area to use the "high latitude rule" to approximate more normal times. But I'm very happy to be corrected :-)

We actually do have some adjustments concerning hardships with a different choice of angles, for example most of people will use the angle 12° instead of the more common 18° one, especially to bring the Isha time sooner.

There are many locations where it happens, for my own location, it's around { latitude: 50.6, longitude: 3.05 }.
For example, the most officially used prayers calculations parameters in France output a Isha time at 00:04 today for my location. Some people criticize the angle adjustments and prefer to tackle the hardship, which will bring the Isha time even later, but this is another concern.

I got your point concerning the way the algorithm should work, I did the following adjustment :

import PrayerTimes from './PrayerTimes';
import Prayer from './Prayer';
import { dateByAddingDays } from './DateUtils';

const _prayersNames = [Prayer.Fajr, Prayer.Sunrise, Prayer.Dhuhr, Prayer.Asr, Prayer.Maghrib, Prayer.Isha];

const _getPrayers = (coordinates, calculationParameters, date = new Date()) => {
  const dayBefore = new PrayerTimes(coordinates, dateByAddingDays(date, -1), calculationParameters);
  const currentDay = new PrayerTimes(coordinates, date, calculationParameters);
  const dayAfter = new PrayerTimes(coordinates, dateByAddingDays(date, +1), calculationParameters);
  const prayers = _prayersNames.map((id) => {
    // If the prayer for the day before has not yet occurred, we still return it
    // Will fix an unexpected behavior happening, for example, when Isha time is after midnight day X
    // and we compute the prayer times the day X between midnight & Isha time
    // Without this fix, it would return Isha for day X+1 even if Isha of day X has not yet occurred
    const time = dayBefore[id] >= date ? dayBefore[id] : currentDay[id];
    return {
      id,
      time: time,
      timeDiff: time.getTime() - date.getTime(),
    };
  });
  return {
    prayers,
    futurePrayers: prayers.filter(p => p.timeDiff > 0).sort((p1, p2) => p1.timeDiff - p2.timeDiff),
    pastPrayers: prayers.filter(p => p.timeDiff <= 0).sort((p1, p2) => p1.timeDiff - p2.timeDiff),
    dayAfter
  };
}

export default class PrayerSchedule {
  constructor(coordinates, date, calculationParameters) {
    this.coordinates = coordinates;
    this.date = date;
    this.calculationParameters = calculationParameters;
    this._prayers = _getPrayers(coordinates, calculationParameters, date);
    this._prayers.prayers.forEach(({ id, time }) => {
      this[id] = time;
    });
  }

  /**
   *
   * @param prayer, string, id of the prayer
   * @param upcoming boolean, if true will only return the time for the next occurrence of the current prayer, else will return the one for the current day (which may be already past or not)
   * @returns {Date}
   */
  timeForPrayer(prayer, upcoming = true) {
    const { prayers, dayAfter } = this._prayers;
    const time = prayers.filter(p => p.id === prayer)[0].time;
    return (upcoming && time < this.date) ? dayAfter[prayer] : time;
  }

  /**
   * Returns the name of the last prayer, if the current date is inferior to Prayer.Fajr, will return Prayer.Isha
   * @returns {string}
   */
  get currentPrayer() {
    const { pastPrayers } = this._prayers;
    return pastPrayers.length ? pastPrayers[pastPrayers.length - 1].id : Prayer.Isha;
  }

  /**
   * Returns the name of the upcoming prayer, if there's no more prayer for the current day, will return Prayer.Fajr
   * @returns {string}
   */
  get nextPrayer() {
    const { futurePrayers } = this._prayers;
    return futurePrayers.length ? futurePrayers[0].id : Prayer.Fajr;
  }
}

Types:

export class PrayerSchedule {
  constructor(coordinates: Coordinates, date: Date, params: CalculationParameters);

  fajr: Date;
  sunrise: Date;
  dhuhr: Date;
  asr: Date;
  maghrib: Date;
  isha: Date;

  timeForPrayer(prayer: Prayer, upcoming: boolean): Date;
  currentPrayer: Prayer;
  nextPrayer: Prayer;
}

@korbav I do like that we are now storing the computed prayer times, however I think futurePrayers and pastPrayers still need to be dynamically computed.

I also wanted to discuss the list of prayer properties on PrayerSchedule itself. You are intentionally allowing for a previous day's isha to be on the list with the rest of the times being for the current day. Perhaps we should consider how we expect this to be used. If a developer would use this to display the current day's prayer times this could be confusing.

timeForPrayer and currentPrayer already have awareness of the previous day, I think this is sufficient for the use case of the post-midnight isha. For the list of prayer times on PrayerSchedule itself I suggest those be only of the current day.

@z3bi I got your point, you're totally right, it would be a big mistake to do const time = dayBefore[id] >= date ? dayBefore[id] : currentDay[id]; in a permanent way.

However, since the behavior of having a "live schedule" based on the current date is highly likable to be used by many developers, what would you think if we create 2 different behaviors and leave the choice to the developer.

something like :

const PrayerScheduleBehavior = {
    DefaultBehavior: 'DefaultBehavior',
    LiveSchedule: 'LiveSchedule',
}
export default class PrayerSchedule {
  constructor(coordinates, date, calculationParameters, behavior = PrayerScheduleBehavior.DefaultBehavior) {
...
}

It would then be pretty easy to tweak the returned times based on the chosen behavior.

To follow up on our discussions, I created a sandbox to clarify the idea of behaviors and how will they differ.
If you could take a look at it @z3bi https://codesandbox.io/s/patient-sunset-frozn

Apart from the default behavior, I identified 3 basic & pertinent behaviors :

image

To offer a deeper look & testing interface, I created a time control on top of the sandbox, so that we can adjust the time and see how each behavior will exactly handle the case.

Below is another example of the implementation :

image

By default, the developer will deal with the currently implemented behavior.

Quick description of the behaviors :

  • Default: Current implementation, returns the times as they come by the library

All the cases below are "adjusted", meaning that they will return the closest future prayer if not happened yet. As we had discussed before, computing Isha at 00:03 will likely be expected to return 00:06 of the same day instead of 00:09 of the Day+1 since 00:06 has not happened yet.

  • Default Adjusted : Nothing more than the adjustment described above
  • Closest Upcoming Always : Will always show prayers that has not happened yet
  • Closest To Current Time : Will show prayers from both path & future, based on the shortest "delta-to-current-time"

Stale issue message