<|start_filename|>src/Qibla.js<|end_filename|>
import Coordinates from './Coordinates'
import { degreesToRadians, radiansToDegrees, unwindAngle } from './MathUtils'

export default function qibla(coordinates) {
    const makkah = new Coordinates(21.4225241, 39.8261818);

    // Equation from "Spherical Trigonometry For the use of colleges and schools" page 50
    const term1 = (
        Math.sin(degreesToRadians(makkah.longitude) -
        degreesToRadians(coordinates.longitude))
    );
    const term2 = (
        Math.cos(degreesToRadians(coordinates.latitude)) *
        Math.tan(degreesToRadians(makkah.latitude))
    );
    const term3 = (
        Math.sin(degreesToRadians(coordinates.latitude)) *
        Math.cos(degreesToRadians(makkah.longitude) -
        degreesToRadians(coordinates.longitude))
    );
    const angle = Math.atan2(term1, term2 - term3);

    return unwindAngle(radiansToDegrees(angle));
}


<|start_filename|>src/MathUtils.js<|end_filename|>
export function degreesToRadians(degrees) {
    return (degrees * Math.PI) / 180.0;
}

export function radiansToDegrees(radians) {
    return (radians * 180.0) / Math.PI;
}

export function normalizeToScale(number, max) {
    return number - (max * (Math.floor(number / max)))
}

export function unwindAngle(angle) {
    return normalizeToScale(angle, 360.0);
}

export function quadrantShiftAngle(angle) {
    if (angle >= -180 && angle <= 180) {
        return angle;
    }

    return angle - (360 * Math.round(angle/360));
}


<|start_filename|>jest.config.js<|end_filename|>
module.exports = {
  collectCoverage: true,

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // An array of file extensions your modules use
  moduleFileExtensions: [
    "js",
    "json",
    "jsx",
    "ts",
    "tsx",
    "node"
  ],

  collectCoverageFrom: [
    "src/**/*.js"
  ],

  // The test environment that will be used for testing
  testEnvironment: "node",
};


<|start_filename|>test/times.test.js<|end_filename|>
/* eslint-disable complexity */
import fs from 'fs';
import adhan from '../src/Adhan';
import moment from 'moment-timezone';

function parseParams(data) {
	let params;

	const method = data["method"];
	if (method === "MuslimWorldLeague") {
		params = adhan.CalculationMethod.MuslimWorldLeague();
	} else if (method === "Egyptian") {
		params = adhan.CalculationMethod.Egyptian();
	} else if (method === "Karachi") {
		params = adhan.CalculationMethod.Karachi();
	} else if (method === "UmmAlQura") {
		params = adhan.CalculationMethod.UmmAlQura();
	} else if (method === "Dubai") {
		params = adhan.CalculationMethod.Dubai();
	} else if (method === "MoonsightingCommittee") {
		params = adhan.CalculationMethod.MoonsightingCommittee();
	} else if (method === "NorthAmerica") {
		params = adhan.CalculationMethod.NorthAmerica();
	} else if (method === "Kuwait") {
		params = adhan.CalculationMethod.Kuwait();
	} else if (method === "Qatar") {
		params = adhan.CalculationMethod.Qatar();
	} else if (method === "Singapore") {
		params = adhan.CalculationMethod.Singapore();
	} else if (method === "Turkey") {
		params = adhan.CalculationMethod.Turkey();
	} else if (method === "Tehran") {
		params = adhan.CalculationMethod.Tehran();
	} else {
		params = adhan.CalculationMethod.Other();
	}

	const madhab = data["madhab"];
	if (madhab === "Shafi") {
		params.madhab = adhan.Madhab.Shafi;
	} else if (madhab === "Hanafi") {
		params.madhab = adhan.Madhab.Hanafi;
	}

	const highLatRule = data["highLatitudeRule"];
	if (highLatRule === "SeventhOfTheNight") {
		params.highLatitudeRule = adhan.HighLatitudeRule.SeventhOfTheNight;
	} else if (highLatRule === "TwilightAngle") {
		params.highLatitudeRule = adhan.HighLatitudeRule.TwilightAngle;
	} else {
		params.highLatitudeRule = adhan.HighLatitudeRule.MiddleOfTheNight;
	}

	return params;
}

expect.extend({
	toBeWithinRange(intialDate, comparisonDate, variance) {
		const initalValue = intialDate.getTime();
		const varianceValue = variance * 60 * 1000;
		const floor = comparisonDate.getTime() - varianceValue;
		const ceiling = comparisonDate.getTime() + varianceValue;
		const pass = (initalValue >= floor && initalValue <= ceiling);
		if (pass) {
			return {
				message: () =>
				`expected ${intialDate} not to be within range ${comparisonDate} and a variance of ${variance} minute`,
				pass: true,
			};
		} else {
			return {
				message: () =>
				`expected ${intialDate} to be within range ${comparisonDate} and a variance of ${variance} minute`,
				pass: false,
			};
		}
	},
  });

fs.readdirSync("Shared/Times").forEach( function (filename) {
	test(`compare calculated times against the prayer times in ${filename}`, () => {
		const file_contents = fs.readFileSync("Shared/Times/" + filename)
		const data = JSON.parse(file_contents);
		const coordinates = new adhan.Coordinates(data["params"]["latitude"], data["params"]["longitude"]);
		const params = parseParams(data["params"]);
		const variance = data["variance"] || 0;
		data["times"].forEach(function(time) {
			const date = moment(time["date"], "YYYY-MM-DD").toDate();
			const p = new adhan.PrayerTimes(coordinates, date, params);

			const testFajr = moment.tz(time["date"] + " " + time["fajr"], "YYYY-MM-DD h:mm A", data["params"]["timezone"]).toDate();
			const testSunrise = moment.tz(time["date"] + " " + time["sunrise"], "YYYY-MM-DD h:mm A", data["params"]["timezone"]).toDate();
			const testDhuhr = moment.tz(time["date"] + " " + time["dhuhr"], "YYYY-MM-DD h:mm A", data["params"]["timezone"]).toDate();
			const testAsr = moment.tz(time["date"] + " " + time["asr"], "YYYY-MM-DD h:mm A", data["params"]["timezone"]).toDate();
			const testMaghrib = moment.tz(time["date"] + " " + time["maghrib"], "YYYY-MM-DD h:mm A", data["params"]["timezone"]).toDate();
			const testIsha = moment.tz(time["date"] + " " + time["isha"], "YYYY-MM-DD h:mm A", data["params"]["timezone"]).toDate();

			expect(p.fajr).toBeWithinRange(testFajr, variance);
			expect(p.sunrise).toBeWithinRange(testSunrise, variance);
			expect(p.dhuhr).toBeWithinRange(testDhuhr, variance);
			expect(p.asr).toBeWithinRange(testAsr, variance);
			expect(p.maghrib).toBeWithinRange(testMaghrib, variance);
			expect(p.isha).toBeWithinRange(testIsha, variance);
		});
	});
});




<|start_filename|>test/qibla.test.js<|end_filename|>
import qibla from '../src/Qibla';

test("finding Qibla in North America", () => {
	const washingtonDC = { latitude: 38.9072, longitude: -77.0369 };
	expect(qibla(washingtonDC)).toBeCloseTo(56.560, 3);

	const nyc = { latitude: 40.7128, longitude: -74.0059 };
	expect(qibla(nyc)).toBeCloseTo(58.4817, 3);

	const sanFrancisco = { latitude: 37.7749, longitude: -122.4194 };
	expect(qibla(sanFrancisco)).toBeCloseTo(18.8438, 3);

	const anchorage = { latitude: 61.2181, longitude: -149.9003 };
	expect(qibla(anchorage)).toBeCloseTo(350.883, 3);
});

test("finding Qibla in the South Pacific", () => {
	const sydney = { latitude: -33.8688, longitude: 151.2093 };
	expect(qibla(sydney)).toBeCloseTo(277.4996, 3);
	
	const auckland = { latitude: -36.8485, longitude: 174.7633 };
	expect(qibla(auckland)).toBeCloseTo(261.197, 3);
});

test("finding Qibla in Europe", () => {
	const london = { latitude: 51.5074, longitude: -0.1278 };
	expect(qibla(london)).toBeCloseTo(118.987, 3);
	
	const paris = { latitude: 48.8566, longitude: 2.3522 };
	expect(qibla(paris)).toBeCloseTo(119.163, 3);
	
	const oslo = { latitude: 59.9139, longitude: 10.7522 };
	expect(qibla(oslo)).toBeCloseTo(139.0278, 3);
});

test("finding Qibla in Asia", () => {
	const islamabad = { latitude: 33.7294, longitude: 73.0931 };
	expect(qibla(islamabad)).toBeCloseTo(255.882, 3);
	
	const tokyo = { latitude: 35.6895, longitude: 139.6917 };
	expect(qibla(tokyo)).toBeCloseTo(293.021, 3);
});



<|start_filename|>src/SolarCoordinates.js<|end_filename|>
import Astronomical from './Astronomical';
import { degreesToRadians, radiansToDegrees, unwindAngle } from './MathUtils';

export default class SolarCoordinates {
    constructor(julianDay) {
        const T = Astronomical.julianCentury(julianDay);
        const L0 = Astronomical.meanSolarLongitude(T);
        const Lp = Astronomical.meanLunarLongitude(T);
        const Omega = Astronomical.ascendingLunarNodeLongitude(T);
        const Lambda = degreesToRadians(Astronomical.apparentSolarLongitude(T, L0));
        const Theta0 = Astronomical.meanSiderealTime(T);
        const dPsi = Astronomical.nutationInLongitude(T, L0, Lp, Omega);
        const dEpsilon = Astronomical.nutationInObliquity(T, L0, Lp, Omega);
        const Epsilon0 = Astronomical.meanObliquityOfTheEcliptic(T);
        const EpsilonApparent = degreesToRadians(Astronomical.apparentObliquityOfTheEcliptic(T, Epsilon0));

        /* declination: The declination of the sun, the angle between
            the rays of the Sun and the plane of the Earth's
            equator, in degrees.
            Equation from Astronomical Algorithms page 165 */
        this.declination = radiansToDegrees(Math.asin(Math.sin(EpsilonApparent) * Math.sin(Lambda)));

        /* rightAscension: Right ascension of the Sun, the angular distance on the
            celestial equator from the vernal equinox to the hour circle,
            in degrees.
            Equation from Astronomical Algorithms page 165 */
        this.rightAscension = unwindAngle(radiansToDegrees(Math.atan2(Math.cos(EpsilonApparent) * Math.sin(Lambda), Math.cos(Lambda))));

        /* apparentSiderealTime: Apparent sidereal time, the hour angle of the vernal
            equinox, in degrees.
            Equation from Astronomical Algorithms page 88 */
        this.apparentSiderealTime = Theta0 + (((dPsi * 3600) * Math.cos(degreesToRadians(Epsilon0 + dEpsilon))) / 3600);
    }
}



<|start_filename|>src/Madhab.js<|end_filename|>
export const Madhab = {
    Shafi: 'shafi',
    Hanafi: 'hanafi'
};

export function shadowLength(madhab) {
    switch (madhab) {
        case Madhab.Shafi:
            return 1;
        case Madhab.Hanafi:
            return 2
        default:
            throw "Invalid Madhab";
    }
}


<|start_filename|>src/Prayer.js<|end_filename|>
const Prayer = {
    Fajr: 'fajr',
    Sunrise: 'sunrise',
    Dhuhr: 'dhuhr',
    Asr: 'asr',
    Maghrib: 'maghrib',
    Isha: 'isha',
    None: 'none'
};

export default Prayer;

<|start_filename|>src/HighLatitudeRule.js<|end_filename|>
const HighLatitudeRule = {
    MiddleOfTheNight: 'middleofthenight',
    SeventhOfTheNight: 'seventhofthenight',
    TwilightAngle: 'twilightangle',

    recommended(coordinates) {
        if (coordinates.latitude > 48) {
            return HighLatitudeRule.SeventhOfTheNight;
        } else {
            return HighLatitudeRule.MiddleOfTheNight;
        }
    }
};

export default HighLatitudeRule;

<|start_filename|>src/SolarTime.js<|end_filename|>
import Astronomical from './Astronomical';
import { degreesToRadians, radiansToDegrees } from './MathUtils'
import SolarCoordinates from './SolarCoordinates';

export default class SolarTime {
    constructor(date, coordinates) {
        const julianDay = Astronomical.julianDay(date.getFullYear(), date.getMonth() + 1, date.getDate(), 0);

        this.observer = coordinates;
        this.solar = new SolarCoordinates(julianDay);

        this.prevSolar = new SolarCoordinates(julianDay - 1);
        this.nextSolar = new SolarCoordinates(julianDay + 1);

        const m0 = Astronomical.approximateTransit(coordinates.longitude, this.solar.apparentSiderealTime, this.solar.rightAscension);
        const solarAltitude = -50.0 / 60.0;

        this.approxTransit = m0;

        this.transit = Astronomical.correctedTransit(m0, coordinates.longitude, this.solar.apparentSiderealTime, 
            this.solar.rightAscension, this.prevSolar.rightAscension, this.nextSolar.rightAscension);

        this.sunrise = Astronomical.correctedHourAngle(m0, solarAltitude, coordinates, false, this.solar.apparentSiderealTime, 
            this.solar.rightAscension, this.prevSolar.rightAscension, this.nextSolar.rightAscension, 
            this.solar.declination, this.prevSolar.declination, this.nextSolar.declination);

        this.sunset = Astronomical.correctedHourAngle(m0, solarAltitude, coordinates, true, this.solar.apparentSiderealTime, 
            this.solar.rightAscension, this.prevSolar.rightAscension, this.nextSolar.rightAscension, 
            this.solar.declination, this.prevSolar.declination, this.nextSolar.declination);
    }

    hourAngle(angle, afterTransit) {
        return Astronomical.correctedHourAngle(this.approxTransit, angle, this.observer, afterTransit, this.solar.apparentSiderealTime, 
            this.solar.rightAscension, this.prevSolar.rightAscension, this.nextSolar.rightAscension, 
            this.solar.declination, this.prevSolar.declination, this.nextSolar.declination);
    }

    afternoon(shadowLength) {
        // TODO source shadow angle calculation
        const tangent = Math.abs(this.observer.latitude - this.solar.declination);
        const inverse = shadowLength + Math.tan(degreesToRadians(tangent));
        const angle = radiansToDegrees(Math.atan(1.0 / inverse));
        return this.hourAngle(angle, true);
    }
}




<|start_filename|>src/PolarCircleResolution.js<|end_filename|>
import SolarTime from './SolarTime';
import { dateByAddingDays } from './DateUtils';

export const PolarCircleResolution = {
  AqrabBalad: 'AqrabBalad',
  AqrabYaum: 'AqrabYaum',
  Unresolved: 'Unresolved'
};

const LATITUDE_VARIATION_STEP = 0.5; // Degrees to add/remove at each resolution step
const UNSAFE_LATITUDE = 65; // Based on https://en.wikipedia.org/wiki/Midnight_sun

const isValidSolarTime = (solarTime) => solarTime && !isNaN(solarTime.sunrise) && !isNaN(solarTime.sunset);

const aqrabYaumResolver = (coordinates, date, daysAdded = 1, direction = 1) => {
  if (daysAdded > Math.ceil(365 / 2)) {
    return null;
  }
  const testDate = new Date(date.getTime());
  testDate.setDate(testDate.getDate() + (direction * daysAdded));
  const tomorrow = dateByAddingDays(testDate, 1);
  const solarTime = new SolarTime(testDate, coordinates);
  const tomorrowSolarTime = new SolarTime(tomorrow, coordinates);

  if (!isValidSolarTime(solarTime) || !isValidSolarTime(tomorrowSolarTime)) {
    return aqrabYaumResolver(coordinates, date, daysAdded + (direction > 0 ? 0 : 1), -direction);
  }

  return {
    date,
    tomorrow,
    coordinates,
    solarTime,
    tomorrowSolarTime,
  };
}

const aqrabBaladResolver = (coordinates, date, latitude) => {
  const solarTime = new SolarTime(date, { ...coordinates, latitude });
  const tomorrow = dateByAddingDays(date, 1);
  const tomorrowSolarTime = new SolarTime(tomorrow, { ...coordinates, latitude });
  if (!isValidSolarTime(solarTime) || !isValidSolarTime(tomorrowSolarTime)) {
    return (Math.abs(latitude) >= UNSAFE_LATITUDE) ?
      aqrabBaladResolver(coordinates, date, latitude - Math.sign(latitude) * LATITUDE_VARIATION_STEP)
      : null;
  }

  return {
    date,
    tomorrow,
    coordinates: { latitude, longitude: coordinates.longitude },
    solarTime,
    tomorrowSolarTime,
  };
};

export const polarCircleResolvedValues = (resolver, date, coordinates) => {
  const defaultReturn = {
    date,
    tomorrow: dateByAddingDays(date, 1),
    coordinates,
    solarTime: new SolarTime(date, coordinates),
    tomorrowSolarTime: new SolarTime(dateByAddingDays(date, 1), coordinates),
  };

  switch (resolver) {
    case PolarCircleResolution.AqrabYaum: {
      return aqrabYaumResolver(coordinates, date) || defaultReturn;
    }
    case PolarCircleResolution.AqrabBalad: {
      const { latitude } = coordinates;
      return aqrabBaladResolver(coordinates, date, latitude - (Math.sign(latitude) * LATITUDE_VARIATION_STEP)) || defaultReturn;
    }
    default: {
      return defaultReturn;
    }
  }
};


<|start_filename|>src/TimeComponents.js<|end_filename|>
export default class TimeComponents {
    constructor(number) {
        this.hours = Math.floor(number);
        this.minutes = Math.floor((number - this.hours) * 60);
        this.seconds = Math.floor((number - (this.hours + this.minutes / 60)) * 60 * 60);
        return this;
    }
    
    utcDate(year, month, date) {
        return new Date(Date.UTC(year, month, date, this.hours, this.minutes, this.seconds));
    }
}


<|start_filename|>src/Astronomical.js<|end_filename|>
/* eslint-disable max-params, max-lines */
import { degreesToRadians, radiansToDegrees, unwindAngle, normalizeToScale, quadrantShiftAngle } from './MathUtils';
import { dateByAddingSeconds } from './DateUtils';

const Astronomical = {

    /* The geometric mean longitude of the sun in degrees. */
    meanSolarLongitude(julianCentury) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 163 */
        const term1 = 280.4664567;
        const term2 = 36000.76983 * T;
        const term3 = 0.0003032 * Math.pow(T, 2);
        const L0 = term1 + term2 + term3;
        return unwindAngle(L0);
    },

    /* The geometric mean longitude of the moon in degrees. */
    meanLunarLongitude(julianCentury) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 144 */
        const term1 = 218.3165;
        const term2 = 481267.8813 * T;
        const Lp = term1 + term2;
        return unwindAngle(Lp);
    },

    ascendingLunarNodeLongitude(julianCentury) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 144 */
        const term1 = 125.04452;
        const term2 = 1934.136261 * T;
        const term3 = 0.0020708 * Math.pow(T, 2);
        const term4 = Math.pow(T, 3) / 450000;
        const Omega = term1 - term2 + term3 + term4;
        return unwindAngle(Omega);
    },

    /* The mean anomaly of the sun. */
    meanSolarAnomaly(julianCentury) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 163 */
        const term1 = 357.52911;
        const term2 = 35999.05029 * T;
        const term3 = 0.0001537 * Math.pow(T, 2);
        const M = term1 + term2 - term3;
        return unwindAngle(M);
    },

    /* The Sun's equation of the center in degrees. */
    solarEquationOfTheCenter(julianCentury, meanAnomaly) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 164 */
        const Mrad = degreesToRadians(meanAnomaly);
        const term1 = (1.914602 - (0.004817 * T) - (0.000014 * Math.pow(T, 2))) * Math.sin(Mrad);
        const term2 = (0.019993 - (0.000101 * T)) * Math.sin(2 * Mrad);
        const term3 = 0.000289 * Math.sin(3 * Mrad);
        return term1 + term2 + term3;
    },

    /* The apparent longitude of the Sun, referred to the
        true equinox of the date. */
    apparentSolarLongitude(julianCentury, meanLongitude) {
        const T = julianCentury;
        const L0 = meanLongitude;
        /* Equation from Astronomical Algorithms page 164 */
        const longitude = L0 + Astronomical.solarEquationOfTheCenter(T, Astronomical.meanSolarAnomaly(T));
        const Omega = 125.04 - (1934.136 * T);
        const Lambda = longitude - 0.00569 - (0.00478 * Math.sin(degreesToRadians(Omega)));
        return unwindAngle(Lambda);
    },

    /* The mean obliquity of the ecliptic, formula
        adopted by the International Astronomical Union.
        Represented in degrees. */
    meanObliquityOfTheEcliptic(julianCentury) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 147 */
        const term1 = 23.439291;
        const term2 = 0.013004167 * T;
        const term3 = 0.0000001639 * Math.pow(T, 2);
        const term4 = 0.0000005036 * Math.pow(T, 3);
        return term1 - term2 - term3 + term4;
    },

    /* The mean obliquity of the ecliptic, corrected for
        calculating the apparent position of the sun, in degrees. */
    apparentObliquityOfTheEcliptic(julianCentury, meanObliquityOfTheEcliptic) {
        const T = julianCentury;
        const Epsilon0 = meanObliquityOfTheEcliptic;
        /* Equation from Astronomical Algorithms page 165 */
        const O = 125.04 - (1934.136 * T);
        return Epsilon0 + (0.00256 * Math.cos(degreesToRadians(O)));
    },

    /* Mean sidereal time, the hour angle of the vernal equinox, in degrees. */
    meanSiderealTime(julianCentury) {
        const T = julianCentury;
        /* Equation from Astronomical Algorithms page 165 */
        const JD = (T * 36525) + 2451545.0;
        const term1 = 280.46061837;
        const term2 = 360.98564736629 * (JD - 2451545);
        const term3 = 0.000387933 * Math.pow(T, 2);
        const term4 = Math.pow(T, 3) / 38710000;
        const Theta = term1 + term2 + term3 - term4;
        return unwindAngle(Theta)
    },

    nutationInLongitude(julianCentury, solarLongitude, lunarLongitude, ascendingNode) {
        const L0 = solarLongitude;
        const Lp = lunarLongitude;
        const Omega = ascendingNode;
        /* Equation from Astronomical Algorithms page 144 */
        const term1 = (-17.2/3600) * Math.sin(degreesToRadians(Omega));
        const term2 =  (1.32/3600) * Math.sin(2 * degreesToRadians(L0));
        const term3 =  (0.23/3600) * Math.sin(2 * degreesToRadians(Lp));
        const term4 =  (0.21/3600) * Math.sin(2 * degreesToRadians(Omega));
        return term1 - term2 - term3 + term4;
    },

    nutationInObliquity(julianCentury, solarLongitude, lunarLongitude, ascendingNode) {
        const L0 = solarLongitude;
        const Lp = lunarLongitude;
        const Omega = ascendingNode;
        /* Equation from Astronomical Algorithms page 144 */
        const term1 =  (9.2/3600) * Math.cos(degreesToRadians(Omega));
        const term2 = (0.57/3600) * Math.cos(2 * degreesToRadians(L0));
        const term3 = (0.10/3600) * Math.cos(2 * degreesToRadians(Lp));
        const term4 = (0.09/3600) * Math.cos(2 * degreesToRadians(Omega));
        return term1 + term2 + term3 - term4;
    },

    altitudeOfCelestialBody(observerLatitude, declination, localHourAngle) {
        const Phi = observerLatitude;
        const delta = declination;
        const H = localHourAngle;
        /* Equation from Astronomical Algorithms page 93 */
        const term1 = Math.sin(degreesToRadians(Phi)) * Math.sin(degreesToRadians(delta));
        const term2 = Math.cos(degreesToRadians(Phi)) * Math.cos(degreesToRadians(delta)) * Math.cos(degreesToRadians(H));
        return radiansToDegrees(Math.asin(term1 + term2));
    },

    approximateTransit(longitude, siderealTime, rightAscension) {
        const L = longitude;
        const Theta0 = siderealTime;
        const a2 = rightAscension;
        /* Equation from page Astronomical Algorithms 102 */
        const Lw = L * -1;
        return normalizeToScale((a2 + Lw - Theta0) / 360, 1);
    },

    /* The time at which the sun is at its highest point in the sky (in universal time) */
    correctedTransit(approximateTransit, longitude, siderealTime, rightAscension, previousRightAscension, nextRightAscension) {
        const m0 = approximateTransit;
        const L = longitude;
        const Theta0 = siderealTime;
        const a2 = rightAscension;
        const a1 = previousRightAscension;
        const a3 = nextRightAscension;
        /* Equation from page Astronomical Algorithms 102 */
        const Lw = L * -1;
        const Theta = unwindAngle((Theta0 + (360.985647 * m0)));
        const a = unwindAngle(Astronomical.interpolateAngles(a2, a1, a3, m0));
        const H = quadrantShiftAngle(Theta - Lw - a);
        const dm = H / -360;
        return (m0 + dm) * 24;
    },

    correctedHourAngle(approximateTransit, angle, coordinates, afterTransit, siderealTime,
                                    rightAscension, previousRightAscension, nextRightAscension, declination, previousDeclination, nextDeclination) {
        const m0 = approximateTransit;
        const h0 = angle;
        const Theta0 = siderealTime;
        const a2 = rightAscension;
        const a1 = previousRightAscension;
        const a3 = nextRightAscension;
        const d2 = declination;
        const d1 = previousDeclination;
        const d3 = nextDeclination;

        /* Equation from page Astronomical Algorithms 102 */
        const Lw = coordinates.longitude * -1;
        const term1 = Math.sin(degreesToRadians(h0)) - (Math.sin(degreesToRadians(coordinates.latitude)) * Math.sin(degreesToRadians(d2)));
        const term2 = Math.cos(degreesToRadians(coordinates.latitude)) * Math.cos(degreesToRadians(d2));
        const H0 = radiansToDegrees(Math.acos(term1 / term2));
        const m = afterTransit ? m0 + (H0 / 360) : m0 - (H0 / 360);
        const Theta = unwindAngle((Theta0 + (360.985647 * m)));
        const a = unwindAngle(Astronomical.interpolateAngles(a2, a1, a3, m));
        const delta = Astronomical.interpolate(d2, d1, d3, m);
        const H = (Theta - Lw - a);
        const h = Astronomical.altitudeOfCelestialBody(coordinates.latitude, delta, H);
        const term3 = h - h0;
        const term4 = 360 * Math.cos(degreesToRadians(delta)) * Math.cos(degreesToRadians(coordinates.latitude)) * Math.sin(degreesToRadians(H));
        const dm = term3 / term4;
        return (m + dm) * 24;
    },

    /* Interpolation of a value given equidistant
        previous and next values and a factor
        equal to the fraction of the interpolated
        point's time over the time between values. */
    interpolate(y2, y1, y3, n) {
        /* Equation from Astronomical Algorithms page 24 */
        const a = y2 - y1;
        const b = y3 - y2;
        const c = b - a;
        return y2 + ((n/2) * (a + b + (n * c)));
    },

    /* Interpolation of three angles, accounting for
        angle unwinding. */
    interpolateAngles(y2, y1, y3, n) {
        /* Equation from Astronomical Algorithms page 24 */
        const a = unwindAngle(y2 - y1);
        const b = unwindAngle(y3 - y2);
        const c = b - a;
        return y2 + ((n/2) * (a + b + (n * c)));
    },

    /* The Julian Day for the given Gregorian date components. */
    julianDay(year, month, day, hours) {
        /* Equation from Astronomical Algorithms page 60 */
        if (typeof hours === 'undefined') {
            hours = 0;
        }

        const trunc = Math.trunc || function (x) { return x < 0 ? Math.ceil(x) : Math.floor(x); };

        const Y = trunc(month > 2 ? year : year - 1);
        const M = trunc(month > 2 ? month : month + 12);
        const D = day + (hours / 24);

        const A = trunc(Y/100);
        const B = trunc(2 - A + trunc(A/4));

        const i0 = trunc(365.25 * (Y + 4716));
        const i1 = trunc(30.6001 * (M + 1));

        return i0 + i1 + D + B - 1524.5;
    },

    /* Julian century from the epoch. */
    julianCentury(julianDay) {
        /* Equation from Astronomical Algorithms page 163 */
        return (julianDay - 2451545.0) / 36525;
    },

    /* Whether or not a year is a leap year (has 366 days). */
    isLeapYear(year) {
        if (year % 4 !== 0) {
            return false;
        }

        if (year % 100 === 0 && year % 400 !== 0) {
            return false;
        }

        return true;
    },

    seasonAdjustedMorningTwilight(latitude, dayOfYear, year, sunrise) {
        const a = 75 + ((28.65 / 55.0) * Math.abs(latitude));
        const b = 75 + ((19.44 / 55.0) * Math.abs(latitude));
        const c = 75 + ((32.74 / 55.0) * Math.abs(latitude));
        const d = 75 + ((48.10 / 55.0) * Math.abs(latitude));

        const adjustment = (function() {
            const dyy = Astronomical.daysSinceSolstice(dayOfYear, year, latitude);
            if ( dyy < 91) {
                return a + ( b - a ) / 91.0 * dyy;
            } else if ( dyy < 137) {
                return b + ( c - b ) / 46.0 * ( dyy - 91 );
            } else if ( dyy < 183 ) {
                return c + ( d - c ) / 46.0 * ( dyy - 137 );
            } else if ( dyy < 229 ) {
                return d + ( c - d ) / 46.0 * ( dyy - 183 );
            } else if ( dyy < 275 ) {
                return c + ( b - c ) / 46.0 * ( dyy - 229 );
            } else {
                return b + ( a - b ) / 91.0 * ( dyy - 275 );
            }
        })();

        return dateByAddingSeconds(sunrise, Math.round(adjustment * -60.0));
    },

    seasonAdjustedEveningTwilight(latitude, dayOfYear, year, sunset) {
        const a = 75 + ((25.60 / 55.0) * Math.abs(latitude));
        const b = 75 + ((2.050 / 55.0) * Math.abs(latitude));
        const c = 75 - ((9.210 / 55.0) * Math.abs(latitude));
        const d = 75 + ((6.140 / 55.0) * Math.abs(latitude));

        const adjustment = (function() {
            const dyy = Astronomical.daysSinceSolstice(dayOfYear, year, latitude);
            if ( dyy < 91) {
                return a + ( b - a ) / 91.0 * dyy;
            } else if ( dyy < 137) {
                return b + ( c - b ) / 46.0 * ( dyy - 91 );
            } else if ( dyy < 183 ) {
                return c + ( d - c ) / 46.0 * ( dyy - 137 );
            } else if ( dyy < 229 ) {
                return d + ( c - d ) / 46.0 * ( dyy - 183 );
            } else if ( dyy < 275 ) {
                return c + ( b - c ) / 46.0 * ( dyy - 229 );
            } else {
                return b + ( a - b ) / 91.0 * ( dyy - 275 );
            }
        })();

        return dateByAddingSeconds(sunset, Math.round(adjustment * 60.0));
    },

    daysSinceSolstice(dayOfYear, year, latitude) {
        let daysSinceSolstice = 0;
        const northernOffset = 10;
        const southernOffset = Astronomical.isLeapYear(year) ? 173 : 172;
        const daysInYear = Astronomical.isLeapYear(year) ? 366 : 365;

        if (latitude >= 0) {
            daysSinceSolstice = dayOfYear + northernOffset;
            if (daysSinceSolstice >= daysInYear) {
                daysSinceSolstice = daysSinceSolstice - daysInYear;
            }
        } else {
            daysSinceSolstice = dayOfYear - southernOffset;
            if (daysSinceSolstice < 0) {
                daysSinceSolstice = daysSinceSolstice + daysInYear;
            }
        }

        return daysSinceSolstice;
    }
};

export default Astronomical;