├── tests ├── utils.ts ├── .eslintrc.js ├── tsconfig.json ├── YomiCalculator.test.ts ├── YerushalmiYomiCalculator.test.ts ├── kosher-zmanim.test.ts └── Utils.test.ts ├── .gitignore ├── src ├── tsconfig.json ├── tsconfig-es6.json ├── tsconfig-types.json ├── polyfills │ ├── errors.ts │ └── Utils.ts ├── kosher-zmanim.ts ├── hebrewcalendar │ ├── YerushalmiYomiCalculator.ts │ ├── YomiCalculator.ts │ └── Daf.ts └── util │ ├── Time.ts │ ├── Zman.ts │ ├── GeoLocationUtils.ts │ ├── SunTimesCalculator.ts │ ├── AstronomicalCalculator.ts │ ├── ZmanimFormatter.ts │ ├── NOAACalculator.ts │ └── GeoLocation.ts ├── .github └── workflows │ └── node.js.yml ├── webpack.config.js ├── .eslintrc.js ├── package.json ├── CHANGELOG.md ├── docs └── index.html └── README.md /tests/utils.ts: -------------------------------------------------------------------------------- 1 | export function omit(obj: Record, props: string[]): object { 2 | const obj2: Record = {}; 3 | Object.keys(obj).forEach(key => !props.includes(key) && (obj2[key] = obj[key])); 4 | return obj2; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | lib-cjs/ 4 | tsserver.cmd 5 | tsserver 6 | tsc.cmd 7 | tsc 8 | /test/ 9 | KosherJava.jar 10 | lib-es6/ 11 | work/ 12 | /src/**/*.js 13 | /src/**/*.js.map 14 | webpack 15 | webpack.cmd 16 | *.tgz 17 | dist/ 18 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | extends: path.resolve(__dirname, '../.eslintrc.js'), 5 | parserOptions: { 6 | project: path.resolve(__dirname, './tsconfig.json'), 7 | }, 8 | rules: { 9 | 'prefer-arrow-callback': 'off', 10 | 'func-names': 'off', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "allowSyntheticDefaultImports": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "noEmit": true, 9 | "resolveJsonModule": true 10 | }, 11 | "include": [ 12 | "**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "target": "es6", 6 | "sourceMap": true, 7 | "outDir": "../dist/lib-cjs/", 8 | "lib": ["ESNext"], 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "node", 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig-es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es6", 4 | "declaration": false, 5 | "target": "es6", 6 | "sourceMap": true, 7 | "outDir": "../dist/lib-es6/", 8 | "lib": ["ESNext"], 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "node", 11 | "strict": true 12 | }, 13 | "exclude": [ 14 | "cli.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/tsconfig-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "target": "es6", 6 | "sourceMap": true, 7 | "outDir": "../dist/types/", 8 | "lib": ["ESNext"], 9 | "allowSyntheticDefaultImports": true, 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "emitDeclarationOnly": true, 13 | }, 14 | "exclude": [ 15 | "cli.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/polyfills/errors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | class BaseCustomError extends Error { 3 | constructor(message?: string) { 4 | super(message); 5 | this.name = this.constructor.name; 6 | } 7 | } 8 | 9 | export class NullPointerException extends BaseCustomError { 10 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 11 | constructor() { 12 | super(); 13 | } 14 | } 15 | 16 | export class IllegalArgumentException extends BaseCustomError {} 17 | 18 | export class UnsupportedError extends BaseCustomError {} 19 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [ 20.x, 22.x ] 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Cache Node.js modules 21 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 22 | with: 23 | # npm cache files are stored in `~/.npm` on Linux/macOS 24 | path: ~/.npm 25 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.OS }}-node- 28 | ${{ runner.OS }}- 29 | - run: npm ci 30 | # TODO: removed until all the lint errors are fixed 31 | # - run: npm run lint 32 | - run: npm test 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: "production", 6 | devtool: 'source-map', 7 | context: __dirname, 8 | entry: { 9 | "kosher-zmanim": "./src/kosher-zmanim.ts", 10 | "kosher-zmanim.min": "./src/kosher-zmanim.ts", 11 | }, 12 | output: { 13 | filename: "[name].js", 14 | path: path.resolve(__dirname, "./dist"), 15 | libraryTarget: "umd", 16 | library: "KosherZmanim", 17 | umdNamedDefine: true, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.ts$/, 23 | use: [ 24 | { 25 | loader: "ts-loader", 26 | options: { 27 | transpileOnly: true, 28 | logInfoToStdOut: true, 29 | compilerOptions: { 30 | target: "es5", 31 | module: "es6", 32 | }, 33 | configFile: "src/tsconfig.json", 34 | }, 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | extensions: [".ts", ".js"], 42 | }, 43 | optimization: { 44 | minimizer: [new UglifyJsPlugin({ 45 | sourceMap: true, 46 | include: /\.min\.js$/, 47 | uglifyOptions: { 48 | mangle: { 49 | keep_fnames: true, 50 | }, 51 | }, 52 | })], 53 | }, 54 | }; 55 | 56 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'airbnb-typescript/base', 5 | ], 6 | parser: '@typescript-eslint/parser', 7 | plugins: [ 8 | '@typescript-eslint', 9 | ], 10 | parserOptions: { 11 | project: './src/tsconfig.json', 12 | }, 13 | rules: { 14 | '@typescript-eslint/indent': ['error', 2, { 15 | FunctionExpression: { 16 | parameters: 2, 17 | }, 18 | SwitchCase: 1, 19 | }], 20 | 'import/prefer-default-export': 'off', 21 | 'lines-between-class-members': ['error', 'always', { 22 | exceptAfterSingleLine: true, 23 | }], 24 | // TODO: Change to 100 25 | 'max-len': ['off', 120], 26 | 'no-plusplus': 'off', 27 | '@typescript-eslint/naming-convention': [ 28 | 'error', 29 | { 30 | 'selector': 'variable', 31 | 'format': [ 32 | 'camelCase', 33 | 'PascalCase', 34 | 'UPPER_CASE', 35 | ], 36 | filter: { 37 | regex: '^Long_MIN_VALUE$', 38 | match: false, 39 | }, 40 | }, 41 | ], 42 | 'arrow-parens': ['error', 'as-needed'], 43 | 'object-curly-newline': ['error', { 44 | multiline: true, 45 | consistent: true, 46 | }], 47 | 'prefer-destructuring': ['error', { 48 | AssignmentExpression: { 49 | array: false, 50 | }, 51 | }], 52 | 'no-else-return': ['error', { 53 | allowElseIf: true, 54 | }], 55 | 'no-underscore-dangle': 'off', 56 | 'operator-assignment': 'off', 57 | '@typescript-eslint/no-use-before-define': ['error', { 58 | functions: false, 59 | }], 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /tests/YomiCalculator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { assert } from 'chai'; 3 | 4 | import { Daf } from '../src/hebrewcalendar/Daf'; 5 | import { YomiCalculator } from '../src/hebrewcalendar/YomiCalculator'; 6 | import { JewishDate } from '../src/hebrewcalendar/JewishDate'; 7 | import { JewishCalendar } from '../src/hebrewcalendar/JewishCalendar'; 8 | 9 | describe('Test YomiCalculator', function () { 10 | it('Gets Bavli daf for 12 Kislev 5685 (Yoma 2)', () => { 11 | const jewishCalendar: JewishCalendar = new JewishCalendar(5685, JewishDate.KISLEV, 12); 12 | const daf: Daf = YomiCalculator.getDafYomiBavli(jewishCalendar); 13 | 14 | assert.strictEqual(daf.getMasechtaNumber(), 5); 15 | assert.strictEqual(daf.getDaf(), 2); 16 | }); 17 | 18 | it('Gets Bavli daf for 26 Elul 5736 (Shekalim 14)', () => { 19 | const jewishCalendar: JewishCalendar = new JewishCalendar(5736, JewishDate.ELUL, 26); 20 | const daf: Daf = YomiCalculator.getDafYomiBavli(jewishCalendar); 21 | 22 | assert.strictEqual(daf.getMasechtaNumber(), 4); 23 | assert.strictEqual(daf.getDaf(), 14); 24 | }); 25 | 26 | it('Gets Bavli daf for 10 Elul 5777 (Sanhedrin 47)', () => { 27 | const jewishCalendar: JewishCalendar = new JewishCalendar(5777, JewishDate.ELUL, 10); 28 | const daf: Daf = YomiCalculator.getDafYomiBavli(jewishCalendar); 29 | 30 | assert.strictEqual(daf.getMasechtaNumber(), 23); 31 | assert.strictEqual(daf.getDaf(), 47); 32 | }); 33 | 34 | it('Gets Bavli daf for 29 Elul 5683 (Error)', () => { 35 | const jewishCalendar: JewishCalendar = new JewishCalendar(5683, JewishDate.ELUL, 29); 36 | 37 | assert.throws(() => YomiCalculator.getDafYomiBavli(jewishCalendar), Error); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kosher-zmanim", 3 | "version": "0.9.0", 4 | "description": "A library for calculating zmanim, based on KosherJava.", 5 | "author": "BehindTheMath", 6 | "license": "LGPL-3.0", 7 | "bugs": "BehindTheMath/KosherZmanim/issues", 8 | "homepage": "https://github.com/BehindTheMath/KosherZmanim", 9 | "repository": "BehindTheMath/KosherZmanim", 10 | "main": "dist/lib-cjs/kosher-zmanim.js", 11 | "types": "dist/types/kosher-zmanim.d.ts", 12 | "module": "dist/lib-es6/kosher-zmanim.js", 13 | "scripts": { 14 | "clean": "rimraf ./dist/*", 15 | "build": "npm run clean && npm run build:all && npm run webpack", 16 | "build:all": "npm run build:cjs && npm run build:es6 && npm run build:types", 17 | "build:cjs": "tsc -p ./src/", 18 | "build:es6": "tsc -p ./src/tsconfig-es6.json", 19 | "build:types": "tsc -p ./src/tsconfig-types.json", 20 | "lint": "eslint --ext .ts src tests", 21 | "test": "TS_NODE_PROJECT=tests/tsconfig.json mocha -r ts-node/register/transpile-only tests/**/*.test.ts", 22 | "prepublishOnly": "npm run build", 23 | "webpack": "webpack" 24 | }, 25 | "keywords": [ 26 | "zmanim", 27 | "zman", 28 | "kosherzmanim", 29 | "kosher-zmanim", 30 | "kosherjava" 31 | ], 32 | "unpkg": "dist/kosher-zmanim.min.js", 33 | "files": [ 34 | "dist/*" 35 | ], 36 | "dependencies": { 37 | "big.js": "^6.1.1", 38 | "luxon": "^1.26.0" 39 | }, 40 | "devDependencies": { 41 | "@types/big.js": "^6.0.2", 42 | "@types/chai": "^4.2.18", 43 | "@types/luxon": "^1.26.5", 44 | "@types/mocha": "^8.2.2", 45 | "@types/node": "^14.14.44", 46 | "@typescript-eslint/eslint-plugin": "^4.23.0", 47 | "@typescript-eslint/parser": "^4.23.0", 48 | "chai": "^4.3.4", 49 | "eslint": "^7.26.0", 50 | "eslint-config-airbnb-typescript": "^12.3.1", 51 | "eslint-plugin-import": "^2.22.1", 52 | "mocha": "^8.4.0", 53 | "rimraf": "^3.0.2", 54 | "ts-loader": "^8.2.0", 55 | "ts-node": "^9.1.1", 56 | "typescript": "^4.2.4", 57 | "uglifyjs-webpack-plugin": "^2.2.0", 58 | "webpack": "^4.46.0", 59 | "webpack-cli": "^3.3.12" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/kosher-zmanim.ts: -------------------------------------------------------------------------------- 1 | import * as _Luxon from 'luxon'; 2 | 3 | import { GeoLocation } from './util/GeoLocation'; 4 | import { ZmanimCalendar } from './ZmanimCalendar'; 5 | import { ComplexZmanimCalendar } from './ComplexZmanimCalendar'; 6 | import { JsonOutput, ZmanimFormatter } from './util/ZmanimFormatter'; 7 | 8 | export function getZmanimJson(options: Options): JsonOutput { 9 | const geoLocation: GeoLocation = new GeoLocation(options.locationName || null, options.latitude, options.longitude, 10 | options.elevation || 0, options.timeZoneId); 11 | 12 | const zmanimCalendar: ZmanimCalendar = options.complexZmanim 13 | ? new ComplexZmanimCalendar(geoLocation) 14 | : new ZmanimCalendar(geoLocation); 15 | zmanimCalendar.setDate(options.date || _Luxon.DateTime.local()); 16 | return ZmanimFormatter.toJSON(zmanimCalendar); 17 | } 18 | 19 | export interface Options { 20 | /** 21 | * @default The current local date. The time is ignored. 22 | */ 23 | date?: Date | string | number | _Luxon.DateTime; 24 | /** 25 | * IANA timezone ID 26 | */ 27 | timeZoneId: string; 28 | locationName?: string; 29 | latitude: number; 30 | longitude: number; 31 | /** 32 | * @default 0 33 | */ 34 | elevation?: number; 35 | /** 36 | * Whether to use `ComplexZmanimCalendar` instead of `ZmanimCalendar` 37 | * @default false 38 | */ 39 | complexZmanim?: boolean; 40 | } 41 | 42 | export * from './util/Time'; 43 | export * from './util/GeoLocation'; 44 | export * from './util/GeoLocationUtils'; 45 | export * from './util/Zman'; 46 | export * from './polyfills/Utils'; 47 | 48 | export * from './util/NOAACalculator'; 49 | export * from './util/SunTimesCalculator'; 50 | 51 | export * from './AstronomicalCalendar'; 52 | export * from './ZmanimCalendar'; 53 | export * from './ComplexZmanimCalendar'; 54 | 55 | export * from './hebrewcalendar/JewishDate'; 56 | export * from './hebrewcalendar/JewishCalendar'; 57 | export * from './hebrewcalendar/Daf'; 58 | export * from './hebrewcalendar/YomiCalculator'; 59 | export * from './hebrewcalendar/YerushalmiYomiCalculator'; 60 | 61 | export * from './hebrewcalendar/HebrewDateFormatter'; 62 | export * from './util/ZmanimFormatter'; 63 | 64 | export const Luxon = _Luxon; 65 | 66 | // Exported explicitly as a convenience. 67 | export const DateTime = _Luxon.DateTime; 68 | -------------------------------------------------------------------------------- /tests/YerushalmiYomiCalculator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { assert } from 'chai'; 3 | 4 | import { Daf } from '../src/hebrewcalendar/Daf'; 5 | import { YerushalmiYomiCalculator } from '../src/hebrewcalendar/YerushalmiYomiCalculator'; 6 | import { JewishDate } from '../src/hebrewcalendar/JewishDate'; 7 | import { JewishCalendar } from '../src/hebrewcalendar/JewishCalendar'; 8 | 9 | describe('Test YerushalmiYomiCalculator', function () { 10 | it('Gets Yerushalmi daf for 10 Elul 5777 (Kiddushin 8)', () => { 11 | const jewishCalendar: JewishCalendar = new JewishCalendar(5777, JewishDate.ELUL, 10); 12 | const daf: Daf | null = YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar); 13 | 14 | assert.strictEqual(daf?.getMasechtaNumber(), 29); 15 | assert.strictEqual(daf?.getDaf(), 8); 16 | }); 17 | 18 | it('Gets Yerushalmi daf for 1 Kislev 5744 (Pesachim 26)', () => { 19 | const jewishCalendar: JewishCalendar = new JewishCalendar(5744, JewishDate.KISLEV, 1); 20 | const daf: Daf | null = YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar); 21 | 22 | assert.strictEqual(daf?.getMasechtaNumber(), 32); 23 | assert.strictEqual(daf?.getDaf(), 26); 24 | }); 25 | 26 | it('Gets Yerushalmi daf for 1 Sivan 5782 (Shevuos 15)', () => { 27 | const jewishCalendar: JewishCalendar = new JewishCalendar(5782, JewishDate.SIVAN, 1); 28 | const daf: Daf | null = YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar); 29 | 30 | assert.strictEqual(daf?.getMasechtaNumber(), 33); 31 | assert.strictEqual(daf?.getDaf(), 15); 32 | }); 33 | 34 | it('Returns null for Yerushalmi daf for 10 Tishrei 5775 (No daf)', () => { 35 | const jewishCalendar: JewishCalendar = new JewishCalendar(5775, JewishDate.TISHREI, 10); 36 | assert.strictEqual(YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar), null); 37 | }); 38 | 39 | it('Returns null for Yerushalmi daf for 9 Av 5783 (No daf)', () => { 40 | const jewishCalendar: JewishCalendar = new JewishCalendar(5783, JewishDate.AV, 9); 41 | assert.strictEqual(YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar), null); 42 | }); 43 | 44 | it('Returns null for Yerushalmi daf for 10 Av 5775 (No daf)', () => { 45 | // 9 Av delayed to Sunday 10 Av 46 | const jewishCalendar: JewishCalendar = new JewishCalendar(5775, JewishDate.AV, 10); 47 | assert.strictEqual(YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar), null); 48 | }); 49 | 50 | it('Gets Yerushalmi daf for 14 Shevat 5740 (Error)', () => { 51 | const jewishCalendar: JewishCalendar = new JewishCalendar(5740, JewishDate.SHEVAT, 14); 52 | 53 | assert.throws(() => YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar), Error); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/kosher-zmanim.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { assert } from 'chai'; 3 | 4 | import { DateTime } from 'luxon'; 5 | 6 | import * as KosherZmanim from '../src/kosher-zmanim'; 7 | 8 | import { omit } from './utils'; 9 | 10 | describe('Test kosher-zmanim', function () { 11 | it('It returns the correct metadata for Basic Zmanim', function () { 12 | const date = new Date(); 13 | const locationName: string = 'Lakewood'; 14 | const latitude: number = 40.0821; 15 | const longitude: number = -74.2097; 16 | const timeZoneId: string = 'America/New_York'; 17 | 18 | const options: KosherZmanim.Options = { 19 | date, 20 | timeZoneId, 21 | locationName, 22 | latitude, 23 | longitude, 24 | elevation: 10, 25 | }; 26 | const zmanimJson = KosherZmanim.getZmanimJson(options); 27 | 28 | const expected = { 29 | algorithm: 'US National Oceanic and Atmospheric Administration Algorithm', 30 | date: DateTime.fromJSDate(date).toFormat('yyyy-MM-dd'), 31 | elevation: '10.0', 32 | latitude: latitude.toString(), 33 | location: locationName, 34 | longitude: longitude.toString(), 35 | timeZoneID: timeZoneId, 36 | type: 'com.kosherjava.zmanim.ZmanimCalendar', 37 | }; 38 | 39 | assert.deepStrictEqual(omit(zmanimJson.metadata, ['timeZoneName', 'timeZoneOffset']), expected); 40 | assert.oneOf(zmanimJson.metadata.timeZoneName, ['Eastern Daylight Time', 'Eastern Standard Time']); 41 | assert.oneOf(zmanimJson.metadata.timeZoneOffset, ['-4.0', '-5.0']); 42 | }); 43 | 44 | it('It returns the correct metadata for Complex Zmanim', function () { 45 | const date = new Date(); 46 | const latitude: number = 40.0821; 47 | const longitude: number = -74.2097; 48 | const timeZoneId: string = 'America/New_York'; 49 | 50 | const options: KosherZmanim.Options = { 51 | date, 52 | timeZoneId, 53 | latitude, 54 | longitude, 55 | elevation: 10, 56 | complexZmanim: true, 57 | }; 58 | const zmanimJson = KosherZmanim.getZmanimJson(options); 59 | 60 | const expected = { 61 | algorithm: 'US National Oceanic and Atmospheric Administration Algorithm', 62 | date: DateTime.fromJSDate(date).toFormat('yyyy-MM-dd'), 63 | elevation: '10.0', 64 | latitude: latitude.toString(), 65 | location: null, 66 | longitude: longitude.toString(), 67 | timeZoneID: timeZoneId, 68 | type: 'com.kosherjava.zmanim.ComplexZmanimCalendar', 69 | }; 70 | 71 | assert.deepStrictEqual(omit(zmanimJson.metadata, ['timeZoneName', 'timeZoneOffset']), expected); 72 | assert.oneOf(zmanimJson.metadata.timeZoneName, ['Eastern Daylight Time', 'Eastern Standard Time']); 73 | assert.oneOf(zmanimJson.metadata.timeZoneOffset, ['-4.0', '-5.0']); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.9.0 2 | ### Breaking changes: 3 | * `Daf.masechtosBavliTransliterated`: The order of `Makkos` and `Sanhedrin` have been switched. 4 | * `Daf`: All methods with `Yerushlmi` have been renamed to `Yerushalmi`. 5 | * `ComplexZmanimCalendar`: All methods with `BainHasmashos` has been renamed to `BainHashmashos`. 6 | * `ComplexZmanimCalendar.getSofZmanAchilasChametz*()` methods return `null` if it's not Erev Pesach. 7 | ### Deprecated 8 | * `ComplexZmanimCalendar.getSofZmanTfilahAteretTorah()`: A new method spelled `ComplexZmanimCalendar.getSofZmanTfilaAteretTorah()` has been added. 9 | * A number of methods have been deprecated for various reasons. See the corresponding JSDocs. 10 | 11 | # v0.8.0 12 | KosherZmanim should now be up to date with KosherJava v2.1.0. 13 | ### Breaking Changes: 14 | * The class name strings have been changed to match the new package path of KosherJava (`com.kosherjava.zmanim.*`). 15 | * The `GeoLocationUtils` class has been marked as deprecated, and will be removed in a future version. 16 | ### Features: 17 | * Add the Yereim's bain hashmashos. 18 | * Allow changing transliterated masechtos' names. 19 | 20 | # v0.7.0 21 | ### Breaking Changes: 22 | * Some of the names and types of members of the `Zman` class have been changed. 23 | 24 | # v0.6.0 25 | ### Breaking Changes: 26 | * Moment and Moment Timezone have been replaced with [Luxon](https://moment.github.io/luxon/index.html). 27 | Any methods that previously took `Moment` instances as arguments now take Luxon class instances. 28 | * `AstronomicalCalendar.getTimeOffset()` has been changed to a static method. 29 | 30 | # v0.5.0 31 | ### Breaking Changes: 32 | * The spelling has changed from `Parshah` to `Parsha`. 33 | * `JewishCalendar.getParshahIndex()` has been renamed to `JewishCalendar.getParsha()`. 34 | * The signatures for `HebrewDateFormatter.getTransliteratedParshiosList()` and `HebrewDateFormatter.getTransliteratedParshiosList()` have changed. 35 | 36 | # v0.4.0 37 | ### Breaking Changes: 38 | * The default calculator is now NOAA (af66119d). 39 | * Options for formatting years ending in מנצפ״ך (af66119d). 40 | The default is now using non-final form for years ending in מנצפ״ך, 41 | so that 5780 will be default format as תש״פ, while it can optionally 42 | be set to format it as תש״ף. 43 | * Removed `KosherZmanim.getZmanimXML()`. 44 | 45 | # v0.3.1 46 | ### Bug fixes 47 | * Fixed the UMD bundle and example for the new API. 48 | 49 | # v0.3.0 50 | ### Breaking Changes: 51 | * `KosherZmanim` is no longer an instantiable class, and the classes are no longer exported under `KosherZmanim.lib.*`. 52 | Instead, everything is exported as named exports. See the Usage section in the [README](./README.md) for more details. 53 | * Default exports have been converted to named exports. 54 | * `KosherZmanim.getZmanimXML()` and `ZmanimFormatter.toXML()` have been deprecated. 55 | * Fixes based on KosherJava updates may have breaking changes. For example, `ZmanimCalculator` was removed. See KosherJava for more details. 56 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | KosherZmanim demo 6 | 10 | 11 | 12 | 13 |

KosherZmanim demo

14 |
You can also open devtools and access KosherZmanim there.
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 |

52 |

53 | 
54 |     
89 |   
90 | 
91 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | ## This project is at an alpha stage.
  2 | 
  3 | Things will break, and APIs might change. Be cautious using this in production.
  4 | Additionally, not all methods have been tested for accuracy.
  5 | 
  6 | # Introduction
  7 | Kosher Zmanim is a TS/JS port of the [KosherJava](https://github.com/KosherJava/zmanim) library.
  8 | 
  9 | # Installation
 10 | **NPM:**
 11 | ```
 12 | npm install kosher-zmanim
 13 | ```
 14 | 
 15 | **UMD (browser):**
 16 | ```html
 17 | 
 18 | ```
 19 | 
 20 | It is highly recommended that you pin the version (e.g. `https://unpkg.com/kosher-zmanim@0.6.0/dist/kosher-zmanim.min.js`),
 21 | so updates don't break your app.
 22 | 
 23 | # Usage and Documentation
 24 | #### Importing
 25 | **ES6 modules / Typescript:**
 26 | ```javascript
 27 | import * as KosherZmanim from "kosher-zmanim";
 28 | ```
 29 | Or:
 30 | ```javascript
 31 | import { ComplexZmanimCalendar, getZmanimJson } from "kosher-zmanim";
 32 | ```
 33 | 
 34 | **CommonJS modules:**
 35 | ```javascript
 36 | const KosherZmanim = require("kosher-zmanim");
 37 | ```
 38 | Or:
 39 | ```javascript
 40 | const { ComplexZmanimCalendar, getZmanimJson } = require("kosher-zmanim");
 41 | ```
 42 | 
 43 | **UMD (browser):**
 44 | 
 45 | For UMD, a global `KosherZmanim` object is exposed.
 46 | 
 47 | #### Library Usage:
 48 | The KosherJava library has been ported to JS, following the original API as close as possible.
 49 | The classes are exposed as named exports. You can instantiate or extend those classes as necessary, the same way you would in Java.
 50 | 
 51 | ```javascript
 52 | const zmanimCalendar = new KosherZmanim.ZmanimCalendar();
 53 | ```
 54 | 
 55 | See the [KosherJava API documentation](https://kosherjava.com/zmanim/docs/api/index.html?overview-summary.html) for more details.
 56 | 
 57 | #### Simple usage
 58 | Alternatively, there is a `getZmanimJson()` utility method.
 59 | 
 60 | ```javascript
 61 | const zmanim = KosherZmanim.getZmanimJson(options);
 62 | ```
 63 | 
 64 | Check out the [demo](https://behindthemath.github.io/KosherZmanim/), and look at the [code](./docs/index.html) to see an example of how it's used.
 65 | 
 66 | ##### `options` object
 67 | The `options` object has the following structure and defaults:
 68 | ```
 69 | {
 70 |     date: Date | string | number = new Date(),
 71 |     timeZoneId: string
 72 |     locationName?: string,
 73 |     latitude: number,
 74 |     longitude: number,
 75 |     elevation?: number = 0,
 76 |     complexZmanim?: boolean = false,
 77 | }
 78 | ```
 79 | 
 80 | ## Note about how zmanim are calculated
 81 | This library uses [Luxon](https://moment.github.io/luxon) as a date/time library, since
 82 | Javascript's `Date` object does not support setting timezones other than the system timezone.
 83 | All class methods that return a `DateTime` object will be in UTC.
 84 | 
 85 | # Breaking changes from KosherJava
 86 | * `AstronomicalCalendar.getTemporalHour()` returns `null` instead of `Long.MIN_VALUE` if the calculations cannot be completed.
 87 | * JS/TS does not have a parallel to Java's `Long.MIN_VALUE`, so `Long_MIN_VALUE` is set to `NaN`.
 88 | * The following methods are not supported:
 89 |   * `AstronomicalCalendar.toString()`
 90 |   * `AstronomicalCalendar.toJSON()`
 91 |   (Use `ZmanimFormatter.toJSON(astronomicalCalendar)` instead).
 92 |   * `AstronomicalCalculator.getDefault()`
 93 |   (Use `new NOAACalculator()` instead).
 94 |   * `JewishCalendar.getDafYomiBavli()`
 95 |   (Use `YomiCalculator.getDafYomiBavli(jewishCalendar)` instead).
 96 |   * `JewishCalendar.getDafYomiYerushalmi()`
 97 |   (Use `YerushalmiYomiCalculator.getDafYomiYerushalmi(jewishCalendar)` instead).
 98 |   * `Time.toString()`
 99 |   (Use `new ZmanimFormatter(TimeZone.getTimeZone("UTC")).format(time)` instead).
100 |   * `ZmanimFormatter.toXML()`
101 | * Some method signatures are different, due to the differences between Java and JS.
102 | * The `Zman` class uses public members instead of getters and setters.
103 | 


--------------------------------------------------------------------------------
/tests/Utils.test.ts:
--------------------------------------------------------------------------------
  1 | // eslint-disable-next-line max-classes-per-file
  2 | import { describe, it } from 'mocha';
  3 | import { assert } from 'chai';
  4 | 
  5 | import { DateTime } from 'luxon';
  6 | 
  7 | import { MathUtils, StringUtils, TimeZone, Utils, padZeros } from '../src/polyfills/Utils';
  8 | 
  9 | const janDateTime = DateTime.fromMillis(1483228800000, { zone: 'UTC' });
 10 | const julyDateTime = DateTime.fromMillis(1498867200000, { zone: 'UTC' });
 11 | 
 12 | describe('Test Utils', function () {
 13 |   it('Tests Utils.getAllMethodNames()', function () {
 14 |     class BaseClass {
 15 |       // eslint-disable-next-line @typescript-eslint/no-useless-constructor,@typescript-eslint/no-empty-function
 16 |       constructor() {
 17 |       }
 18 | 
 19 |       // eslint-disable-next-line class-methods-use-this
 20 |       private privateMethod() {
 21 |       }
 22 | 
 23 |       // eslint-disable-next-line class-methods-use-this
 24 |       protected protectedMethod() {
 25 |       }
 26 | 
 27 |       // eslint-disable-next-line class-methods-use-this
 28 |       public publicMethod() {
 29 |       }
 30 |     }
 31 | 
 32 |     class SubClass extends BaseClass {
 33 |       // eslint-disable-next-line @typescript-eslint/no-useless-constructor,@typescript-eslint/no-empty-function
 34 |       constructor() {
 35 |         super();
 36 |       }
 37 | 
 38 |       // eslint-disable-next-line class-methods-use-this
 39 |       private subClassPrivateMethod() {
 40 |       }
 41 | 
 42 |       // eslint-disable-next-line class-methods-use-this
 43 |       protected subClassProtectedMethod() {
 44 |       }
 45 | 
 46 |       // eslint-disable-next-line class-methods-use-this
 47 |       public subClassPublicMethod() {
 48 |       }
 49 |     }
 50 | 
 51 |     const expectedMethods: Array = [
 52 |       'constructor',
 53 |       'privateMethod',
 54 |       'protectedMethod',
 55 |       'publicMethod',
 56 |       'subClassPrivateMethod',
 57 |       'subClassProtectedMethod',
 58 |       'subClassPublicMethod',
 59 |     ];
 60 |     assert.deepStrictEqual(Utils.getAllMethodNames(new SubClass()), expectedMethods);
 61 | 
 62 |     const expectedMethodsWithoutConstructor: Array = [
 63 |       'privateMethod',
 64 |       'protectedMethod',
 65 |       'publicMethod',
 66 |       'subClassPrivateMethod',
 67 |       'subClassProtectedMethod',
 68 |       'subClassPublicMethod',
 69 |     ];
 70 |     assert.deepStrictEqual(Utils.getAllMethodNames(new SubClass(), true), expectedMethodsWithoutConstructor);
 71 |   });
 72 | });
 73 | 
 74 | describe('Test TimeZone', function () {
 75 |   it('Gets the raw offset for Australia/Eucla', function () {
 76 |     const result = TimeZone.getRawOffset('Australia/Eucla');
 77 |     const expected = 8.75 * 60 * 60 * 1000;
 78 |     assert.strictEqual(result, expected);
 79 |   });
 80 | 
 81 |   it('Gets the raw offset for Australia/Eucla on 2019/01/01 00:00:00Z', function () {
 82 |     const result = TimeZone.getOffset('Australia/Eucla', janDateTime.valueOf());
 83 |     const expected = 8.75 * 60 * 60 * 1000;
 84 |     assert.strictEqual(result, expected);
 85 |   });
 86 | 
 87 |   it('Gets the raw offset for Australia/Eucla on 2019/07/01 00:00:00Z', function () {
 88 |     const result = TimeZone.getOffset('Australia/Eucla', julyDateTime.valueOf());
 89 |     const expected = 8.75 * 60 * 60 * 1000;
 90 |     assert.strictEqual(result, expected);
 91 |   });
 92 | });
 93 | 
 94 | // TODO: TimeZone.getDisplayName()
 95 | // TODO: Zman
 96 | 
 97 | describe('Test MathUtils', function () {
 98 |   it('MathUtils.degreesToRadians()', function () {
 99 |     const result = MathUtils.degreesToRadians(90);
100 |     assert.strictEqual(result, Math.PI / 2);
101 |   });
102 | 
103 |   it('MathUtils.radiansToDegrees()', function () {
104 |     const result = MathUtils.radiansToDegrees(Math.PI / 2);
105 |     assert.strictEqual(result, 90);
106 |   });
107 | });
108 | 
109 | describe('Test StringUtils', function () {
110 |   it('test StringUtils.compareTo()', function () {
111 |     assert.strictEqual(StringUtils.compareTo('test3.ts', 'test3.ts'), 0);
112 | 
113 |     assert.strictEqual(StringUtils.compareTo('test1234', 'test1.ts'), 4);
114 | 
115 |     assert.strictEqual(StringUtils.compareTo('test1.ts', 'test1234'), -4);
116 | 
117 |     assert.strictEqual(StringUtils.compareTo('test12', 'test234'), -1);
118 |   });
119 | });
120 | 
121 | describe('Test `padZeros()`', function () {
122 |   it('test padding `1.234` to 3 places', function () {
123 |     assert.strictEqual(padZeros(1.234, 3), '001');
124 |   });
125 | 
126 |   it('test padding `1234.56` to 3 places', function () {
127 |     assert.strictEqual(padZeros(1234.56, 3), '1234');
128 |   });
129 | });
130 | 


--------------------------------------------------------------------------------
/src/hebrewcalendar/YerushalmiYomiCalculator.ts:
--------------------------------------------------------------------------------
  1 | import { DateTime, Interval } from 'luxon';
  2 | 
  3 | import { Calendar } from '../polyfills/Utils';
  4 | import { Daf } from './Daf';
  5 | import { JewishCalendar } from './JewishCalendar';
  6 | import { IllegalArgumentException } from '../polyfills/errors';
  7 | 
  8 | /**
  9 |  * This class calculates the Talmud Yerusalmi Daf Yomi page ({@link Daf}) for the given date.
 11 |  *
 12 |  * @author © elihaidv
 13 |  * @author © Eliyahu Hershfeld 2017 - 2019
 14 |  */
 15 | export class YerushalmiYomiCalculator {
 16 |   /**
 17 |    * The start date of the first Daf Yomi Yerushalmi cycle of February 2, 1980 / 15 Shevat, 5740.
 18 |    */
 19 |   private static readonly DAF_YOMI_START_DAY: DateTime = DateTime.fromObject({
 20 |     year: 1980,
 21 |     month: Calendar.FEBRUARY + 1,
 22 |     day: 2,
 23 |   });
 24 | 
 25 |   /** The number of pages in the Talmud Yerushalmi. */
 26 |   private static readonly WHOLE_SHAS_DAFS: number = 1554;
 27 | 
 28 |   /** The number of pages per masechta (tractate). */
 29 |   private static readonly BLATT_PER_MASECHTA: number[] = [68, 37, 34, 44, 31, 59, 26, 33, 28, 20, 13, 92, 65, 71, 22,
 30 |     22, 42, 26, 26, 33, 34, 22, 19, 85, 72, 47, 40, 47, 54, 48, 44, 37, 34, 44, 9, 57, 37, 19, 13];
 31 | 
 32 |   /**
 33 |    * Default constructor.
 34 |    */
 35 | /*
 36 |   public YerushalmiYomiCalculator() {
 37 |     // nothing here
 38 |   }
 39 | */
 40 | 
 41 |   /**
 42 |    * Returns the Daf Yomi
 43 |    * Yerusalmi page ({@link Daf}) for the given date.
 44 |    * The first Daf Yomi cycle started on 15 Shevat (Tu Bishvat), 5740 (February, 2, 1980) and calculations
 45 |    * prior to this date will result in an IllegalArgumentException thrown. A null will be returned on Tisha B'Av or
 46 |    * Yom Kippur.
 47 |    *
 48 |    * @param jewishCalendar
 49 |    *            the calendar date for calculation
 50 |    * @return the {@link Daf} or null if the date is on Tisha B'Av or Yom Kippur.
 51 |    *
 52 |    * @throws IllegalArgumentException
 53 |    *             if the date is prior to the February 2, 1980, the start of the first Daf Yomi Yerushalmi cycle
 54 |    */
 55 |   public static getDafYomiYerushalmi(jewishCalendar: JewishCalendar): Daf | null {
 56 |     let nextCycle: DateTime = YerushalmiYomiCalculator.DAF_YOMI_START_DAY;
 57 |     let prevCycle: DateTime = YerushalmiYomiCalculator.DAF_YOMI_START_DAY;
 58 |     const requested: DateTime = jewishCalendar.getDate();
 59 |     let masechta: number = 0;
 60 |     let dafYomi: Daf;
 61 | 
 62 |     // There is no Daf Yomi on Yom Kippur or Tisha B'Av.
 63 |     if (jewishCalendar.getYomTovIndex() === JewishCalendar.YOM_KIPPUR || jewishCalendar.getYomTovIndex() === JewishCalendar.TISHA_BEAV) {
 64 |       return null;
 65 |     }
 66 | 
 67 |     if (requested < YerushalmiYomiCalculator.DAF_YOMI_START_DAY) {
 68 |       throw new IllegalArgumentException(`${requested} is prior to organized Daf Yomi Yerushalmi cycles that started on ${YerushalmiYomiCalculator.DAF_YOMI_START_DAY}`);
 69 |     }
 70 | 
 71 |     // Start to calculate current cycle. Initialize the start day
 72 |     // nextCycle = YerushalmiYomiCalculator.DAF_YOMI_START_DAY;
 73 | 
 74 |     // Go cycle by cycle, until we get the next cycle
 75 |     while (requested > nextCycle) {
 76 |       prevCycle = nextCycle;
 77 | 
 78 |       // Adds the number of whole shas dafs, and then the number of days that not have daf.
 79 |       nextCycle = nextCycle.plus({ days: YerushalmiYomiCalculator.WHOLE_SHAS_DAFS });
 80 |       // This needs to be a separate step
 81 |       nextCycle = nextCycle.plus({ days: YerushalmiYomiCalculator.getNumOfSpecialDays(prevCycle, nextCycle) });
 82 |     }
 83 | 
 84 |     // Get the number of days from cycle start until request.
 85 |     const dafNo: number = requested.diff(prevCycle, ['days']).days;
 86 | 
 87 |     // Get the number of special days to subtract
 88 |     const specialDays: number = YerushalmiYomiCalculator.getNumOfSpecialDays(prevCycle, requested);
 89 |     let total: number = dafNo - specialDays;
 90 | 
 91 |     // Finally find the daf.
 92 |     // TODO:
 93 |     // eslint-disable-next-line no-restricted-syntax
 94 |     for (const blattInMasechta of YerushalmiYomiCalculator.BLATT_PER_MASECHTA) {
 95 |       if (total <= blattInMasechta) {
 96 |         dafYomi = new Daf(masechta, total + 1);
 97 |         break;
 98 |       }
 99 |       total -= blattInMasechta;
100 |       masechta++;
101 |     }
102 | 
103 |     return dafYomi!;
104 |   }
105 | 
106 |   /**
107 |    * Return the number of special days (Yom Kippur and Tisha Beav, where there are no dafim), between the start date
108 |    * (as a Calendar) and end date (also as a Calendar).
109 |    * @param start - start date to calculate from
110 |    * @param end - end date to calculate at
111 |    * @return the number of special days between the start and end dates
112 |    */
113 |   private static getNumOfSpecialDays(start: DateTime, end: DateTime): number {
114 |     // Find the start and end Jewish years
115 |     const jewishStartYear: number = new JewishCalendar(start).getJewishYear();
116 |     const jewishEndYear: number = new JewishCalendar(end).getJewishYear();
117 | 
118 |     // Value to return
119 |     let specialDays: number = 0;
120 | 
121 |     // Instant of special dates
122 |     const yomKippur: JewishCalendar = new JewishCalendar(jewishStartYear, 7, 10);
123 |     const tishaBeav: JewishCalendar = new JewishCalendar(jewishStartYear, 5, 9);
124 | 
125 |     // Go over the years and find special dates
126 |     for (let i: number = jewishStartYear; i <= jewishEndYear; i++) {
127 |       yomKippur.setJewishYear(i);
128 |       tishaBeav.setJewishYear(i);
129 | 
130 |       const interval = Interval.fromDateTimes(start, end);
131 |       if (interval.contains(yomKippur.getDate())) specialDays++;
132 |       if (interval.contains(tishaBeav.getDate())) specialDays++;
133 |     }
134 | 
135 |     return specialDays;
136 |   }
137 | }
138 | 


--------------------------------------------------------------------------------
/src/hebrewcalendar/YomiCalculator.ts:
--------------------------------------------------------------------------------
  1 | import { DateTime } from 'luxon';
  2 | 
  3 | import { Calendar } from '../polyfills/Utils';
  4 | import { Daf } from './Daf';
  5 | import { JewishCalendar } from './JewishCalendar';
  6 | import { IllegalArgumentException } from '../polyfills/errors';
  7 | 
  8 | /**
  9 |  * This class calculates the Daf Yomi Bavli page (daf) for a given date. To calculate Daf Yomi Yerushalmi
 10 |  * use the {@link YerushalmiYomiCalculator}. The library may cover Mishna Yomi etc. at some point in the future.
 11 |  *
 12 |  * @author © Bob Newell (original C code)
 13 |  * @author © Eliyahu Hershfeld 2011 - 2019
 14 |  * @version 0.0.1
 15 |  */
 16 | export class YomiCalculator {
 17 |   /**
 18 |    * The start date of the first Daf Yomi Bavli cycle of September 11, 1923 / Rosh Hashana 5684.
 19 |    */
 20 |   private static readonly dafYomiStartDate: DateTime = DateTime.fromObject({
 21 |     year: 1923,
 22 |     month: Calendar.SEPTEMBER + 1,
 23 |     day: 11,
 24 |   });
 25 | 
 26 |   /** The start date of the first Daf Yomi Bavli cycle in the Julian calendar. Used internally for calculations. */
 27 |   private static readonly dafYomiJulianStartDay: number = YomiCalculator.getJulianDay(YomiCalculator.dafYomiStartDate);
 28 | 
 29 |   /**
 30 |    * The date that the pagination for the Daf Yomi Maseches Shekalim changed to use the commonly used Vilna
 31 |    * Shas pagination from the no longer commonly available Zhitomir / Slavuta Shas used by Rabbi Meir Shapiro.
 32 |    */
 33 |   private static readonly shekalimChangeDate: DateTime = DateTime.fromObject({ year: 1975, month: Calendar.JUNE + 1, day: 24 });
 34 | 
 35 |   /** The Julian date that the cycle for Shekalim changed.
 36 |    * @see #getDafYomiBavli(JewishCalendar) for details.
 37 |    */
 38 |   private static readonly shekalimJulianChangeDay: number = YomiCalculator.getJulianDay(YomiCalculator.shekalimChangeDate);
 39 | 
 40 |   /**
 41 |    * Default constructor.
 42 |    */
 43 | /*
 44 |   public YomiCalculator() {
 45 |     // nothing here
 46 |   }
 47 | */
 48 | 
 49 |   /**
 50 |    * Returns the Daf Yomi Bavli {@link Daf} for a given date. The first Daf Yomi cycle
 52 |    * started on Rosh Hashana 5684 (September 11, 1923) and calculations prior to this date will result in an
 53 |    * IllegalArgumentException thrown. For historical calculations (supported by this method), it is important to note
 54 |    * that a change in length of the cycle was instituted starting in the eighth Daf Yomi cycle beginning on June 24,
 55 |    * 1975. The Daf Yomi Bavli cycle has a single masechta of the Talmud Yerushalmi - Shekalim as part of the cycle.
 56 |    * Unlike the Bavli where the number of daf per masechta was standardized since the original Bomberg Edition published from 1520 - 1523, there is no
 58 |    * uniform page length in the Yerushalmi. The early cycles had the Yerushalmi Shekalim length of 13 days following the
 59 |    * Slavuta/Zhytomyr
 61 |    * Shas used by Rabbi Meir Shapiro. With the start of the eighth Daf Yomi
 62 |    * cycle beginning on June 24, 1975, the length of the Yerushalmi Shekalim was changed from 13 to 22 daf to follow
 63 |    * the Vilna Shas that is in common use today.
 64 |    *
 65 |    * @param calendar
 66 |    *            the calendar date for calculation
 67 |    * @return the {@link Daf}.
 68 |    *
 69 |    * @throws IllegalArgumentException
 70 |    *             if the date is prior to the September 11, 1923, the start date of the first Daf Yomi cycle
 71 |    */
 72 |   public static getDafYomiBavli(calendar: JewishCalendar): Daf {
 73 |     /*
 74 |      * The number of daf per masechta. Since the number of blatt in Shekalim changed on the 8th Daf Yomi cycle
 75 |      * beginning on June 24, 1975, from 13 to 22, the actual calculation for blattPerMasechta[4] will later be
 76 |      * adjusted based on the cycle.
 77 |      */
 78 |     const blattPerMasechta: number[] = [64, 157, 105, 121, 22, 88, 56, 40, 35, 31, 32, 29, 27, 122, 112, 91, 66, 49, 90, 82,
 79 |       119, 119, 176, 113, 24, 49, 76, 14, 120, 110, 142, 61, 34, 34, 28, 22, 4, 9, 5, 73];
 80 | 
 81 |     const date: DateTime = calendar.getDate();
 82 | 
 83 |     let dafYomi: Daf;
 84 |     const julianDay: number = this.getJulianDay(date);
 85 |     let cycleNo: number;
 86 |     let dafNo: number;
 87 |     if (date < YomiCalculator.dafYomiStartDate) {
 88 |       // TODO: should we return a null or throw an IllegalArgumentException?
 89 |       throw new IllegalArgumentException(`${calendar} is prior to organized Daf Yomi Bavli cycles that started on ${YomiCalculator.dafYomiStartDate}`);
 90 |     }
 91 |     if ((date > YomiCalculator.shekalimChangeDate) || date.equals(YomiCalculator.shekalimChangeDate)) {
 92 |       cycleNo = 8 + ((julianDay - YomiCalculator.shekalimJulianChangeDay) / 2711);
 93 |       dafNo = ((julianDay - YomiCalculator.shekalimJulianChangeDay) % 2711);
 94 |     } else {
 95 |       cycleNo = 1 + ((julianDay - YomiCalculator.dafYomiJulianStartDay) / 2702);
 96 |       dafNo = ((julianDay - YomiCalculator.dafYomiJulianStartDay) % 2702);
 97 |     }
 98 | 
 99 |     let total: number = 0;
100 |     let masechta: number = -1;
101 |     let blatt: number;
102 | 
103 |     /* Fix Shekalim for old cycles. */
104 |     if (cycleNo <= 7) {
105 |       blattPerMasechta[4] = 13;
106 |     }
107 |     /* Finally find the daf. */
108 |     // eslint-disable-next-line no-restricted-syntax
109 |     for (const blattInMasechta of blattPerMasechta) {
110 |       masechta++;
111 |       total = total + blattInMasechta - 1;
112 |       if (dafNo < total) {
113 |         blatt = 1 + blattInMasechta - (total - dafNo);
114 |         /* Fiddle with the weird ones near the end. */
115 |         if (masechta === 36) {
116 |           blatt += 21;
117 |         } else if (masechta === 37) {
118 |           blatt += 24;
119 |         } else if (masechta === 38) {
120 |           blatt += 32;
121 |         }
122 |         dafYomi = new Daf(masechta, blatt);
123 |         break;
124 |       }
125 |     }
126 | 
127 |     return dafYomi!;
128 |   }
129 | 
130 |   /**
131 |    * Return the Julian day from a Java Date.
132 |    *
133 |    * @param date
134 |    *            The Java Date
135 |    * @return the Julian day number corresponding to the date
136 |    */
137 |   private static getJulianDay(date: DateTime): number {
138 |     let { year, month } = date;
139 |     const { day } = date;
140 | 
141 |     if (month <= 2) {
142 |       year -= 1;
143 |       month += 12;
144 |     }
145 | 
146 |     const a: number = Math.trunc(year / 100);
147 |     const b: number = 2 - a + Math.trunc(a / 4);
148 |     return Math.trunc(Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + b - 1524.5);
149 |   }
150 | }
151 | 


--------------------------------------------------------------------------------
/src/util/Time.ts:
--------------------------------------------------------------------------------
  1 | import { UnsupportedError } from '../polyfills/errors';
  2 | 
  3 | /**
  4 |  * A class that represents a numeric time. Times that represent a time of day are stored as {@link java.util.Date}s in
  5 |  * this API. The time class is used to represent numeric time such as the time in hours, minutes, seconds and
  6 |  * milliseconds of a {@link AstronomicalCalendar#getTemporalHour() temporal hour}.
  7 |  *
  8 |  * @author © Eliyahu Hershfeld 2004 - 2011
  9 |  * @version 0.9.0
 10 |  */
 11 | 
 12 | export class Time {
 13 |   /** milliseconds in a second. */
 14 |   private static readonly SECOND_MILLIS: number = 1000;
 15 | 
 16 |   /** milliseconds in a minute. */
 17 |   private static readonly MINUTE_MILLIS: number = Time.SECOND_MILLIS * 60;
 18 | 
 19 |   /** milliseconds in an hour. */
 20 |   private static readonly HOUR_MILLIS: number = Time.MINUTE_MILLIS * 60;
 21 | 
 22 |   /**
 23 |    * The hour.
 24 |    * @see #getHours()
 25 |    */
 26 |   private hours: number;
 27 | 
 28 |   /**
 29 |    * The minutes.
 30 |    * @see #getMinutes()
 31 |    */
 32 |   private minutes: number;
 33 | 
 34 |   /**
 35 |    * The seconds.
 36 |    * @see #getSeconds()
 37 |    */
 38 |   private seconds: number;
 39 | 
 40 |   /**
 41 |    * The milliseconds.
 42 |    * @see #getMilliseconds()
 43 |    */
 44 |   private milliseconds: number;
 45 | 
 46 |   /**
 47 |    * Is the time negative
 48 |    * @see #isNegative()
 49 |    * @see #setIsNegative(boolean)
 50 |    */
 51 |   private negative: boolean = false;
 52 | 
 53 |   /**
 54 |    * Constructor with parameters for the hours, minutes, seconds and milliseconds.
 55 |    *
 56 |    * @param hours the hours to set
 57 |    * @param minutes the minutes to set
 58 |    * @param seconds the seconds to set
 59 |    * @param milliseconds the milliseconds to set
 60 |    */
 61 |   constructor(hours: number, minutes: number, seconds: number, milliseconds: number)
 62 |   /**
 63 |    * A constructor that sets the time from milliseconds. The milliseconds are converted to hours, minutes,
 64 |    * seconds and milliseconds. If the milliseconds are negative it will call {@link #setIsNegative(boolean)}.
 65 |    * @param millis the milliseconds to set.
 66 |    */
 67 |   constructor(millis: number)
 68 |   /**
 69 |    * A constructor with 2 overloads:
 70 |    * - A constructor that sets the time by milliseconds.
 71 |    *   The milliseconds are converted to hours, minutes, seconds and milliseconds. If the
 72 |    *   milliseconds are negative it will call {@link #setIsNegative(boolean)}.
 73 |    * - A constructor with parameters for the hours, minutes, seconds and milliseconds.
 74 |    * @param hoursOrMillis
 75 |    * @param minutes
 76 |    * @param seconds
 77 |    * @param milliseconds
 78 |    */
 79 |   constructor(hoursOrMillis: number, minutes?: number, seconds: number = 0, milliseconds: number = 0) {
 80 |     if (minutes) {
 81 |       this.hours = hoursOrMillis;
 82 |       this.minutes = minutes;
 83 |       this.seconds = seconds;
 84 |       this.milliseconds = milliseconds;
 85 |     } else {
 86 |       let adjustedMillis: number = hoursOrMillis;
 87 |       if (adjustedMillis < 0) {
 88 |         this.negative = true;
 89 |         adjustedMillis = Math.abs(adjustedMillis);
 90 |       }
 91 |       this.hours = Math.trunc(adjustedMillis / Time.HOUR_MILLIS);
 92 |       adjustedMillis = adjustedMillis - this.hours * Time.HOUR_MILLIS;
 93 | 
 94 |       this.minutes = Math.trunc(adjustedMillis / Time.MINUTE_MILLIS);
 95 |       adjustedMillis = adjustedMillis - this.minutes * Time.MINUTE_MILLIS;
 96 | 
 97 |       this.seconds = Math.trunc(adjustedMillis / Time.SECOND_MILLIS);
 98 |       adjustedMillis = adjustedMillis - this.seconds * Time.SECOND_MILLIS;
 99 | 
100 |       this.milliseconds = adjustedMillis;
101 |     }
102 |   }
103 | 
104 |   /*
105 |       /!**
106 |        * A constructor that sets the time from milliseconds. The milliseconds are converted to hours, minutes,
107 |        * seconds and milliseconds. If the milliseconds are negative it will call {@link#setIsNegative(boolean)}.
108 |        * @param millis the milliseconds to set.
109 |        *!/
110 |       public Time(millis: number) {
111 |           this((int) millis);
112 |       }
113 | 
114 |       public Time(millis: number) {
115 |           adjustedMillis: number = millis;
116 |           if (adjustedMillis < 0) {
117 |               this.isNegative = true;
118 |               adjustedMillis = Math.abs(adjustedMillis);
119 |           }
120 |           this.hours = adjustedMillis / HOUR_MILLIS;
121 |           adjustedMillis = adjustedMillis - this.hours * HOUR_MILLIS;
122 | 
123 |           this.minutes = adjustedMillis / MINUTE_MILLIS;
124 |           adjustedMillis = adjustedMillis - this.minutes * MINUTE_MILLIS;
125 | 
126 |           this.seconds = adjustedMillis / SECOND_MILLIS;
127 |           adjustedMillis = adjustedMillis - this.seconds * SECOND_MILLIS;
128 | 
129 |           this.milliseconds = adjustedMillis;
130 |       }
131 |   */
132 | 
133 |   /**
134 |    * Does the time represent a negative time 9such as using this to subtract time from another Time.
135 |    * @return if the time is negative.
136 |    */
137 |   public isNegative(): boolean {
138 |     return this.negative;
139 |   }
140 | 
141 |   /**
142 |    * Does the time represent a negative time, such as using this to subtract time from another Time.
143 |    * @param isNegative that the Time represents negative time
144 |    */
145 |   public setIsNegative(isNegative: boolean): void {
146 |     this.negative = isNegative;
147 |   }
148 | 
149 |   /**
150 |    * Get the hour.
151 |    * @return Returns the hour.
152 |    */
153 |   public getHours(): number {
154 |     return this.hours;
155 |   }
156 | 
157 |   /**
158 |    * Set the hour.
159 |    * @param hours
160 |    *            The hours to set.
161 |    */
162 |   public setHours(hours: number): void {
163 |     this.hours = hours;
164 |   }
165 | 
166 |   /**
167 |    * Get the minutes.
168 |    * @return Returns the minutes.
169 |    */
170 |   public getMinutes(): number {
171 |     return this.minutes;
172 |   }
173 | 
174 |   /**
175 |    * Set the minutes.
176 |    * @param minutes
177 |    *            The minutes to set.
178 |    */
179 |   public setMinutes(minutes: number): void {
180 |     this.minutes = minutes;
181 |   }
182 | 
183 |   /**
184 |    * Get the seconds.
185 |    * @return Returns the seconds.
186 |    */
187 |   public getSeconds(): number {
188 |     return this.seconds;
189 |   }
190 | 
191 |   /**
192 |    * Set the seconds.
193 |    * @param seconds
194 |    *            The seconds to set.
195 |    */
196 |   public setSeconds(seconds: number): void {
197 |     this.seconds = seconds;
198 |   }
199 | 
200 |   /**
201 |    * Get the milliseconds.
202 |    * @return Returns the milliseconds.
203 |    */
204 |   public getMilliseconds(): number {
205 |     return this.milliseconds;
206 |   }
207 | 
208 |   /**
209 |    * Set the milliseconds.
210 |    * @param milliseconds
211 |    *            The milliseconds to set.
212 |    */
213 |   public setMilliseconds(milliseconds: number): void {
214 |     this.milliseconds = milliseconds;
215 |   }
216 | 
217 |   /**
218 |    * Returns the time in milliseconds by converting hours, minutes and seconds into milliseconds.
219 |    * @return the time in milliseconds
220 |    */
221 |   public getTime(): number {
222 |     return this.hours * Time.HOUR_MILLIS + this.minutes * Time.MINUTE_MILLIS + this.seconds * Time.SECOND_MILLIS
223 |       + this.milliseconds;
224 |   }
225 | 
226 |   /**
227 |    * @deprecated This depends on a circular dependency. Use 
new ZmanimFormatter(TimeZone.getTimeZone("UTC")).format(time)
instead. 228 | */ 229 | // eslint-disable-next-line class-methods-use-this 230 | public toString(): string { 231 | throw new UnsupportedError('This method is deprecated, due to the fact that it depends on a circular dependency. ' 232 | + 'Use `new ZmanimFormatter(TimeZone.getTimeZone(\'UTC\')).format(time)` instead'); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/polyfills/Utils.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Info } from 'luxon'; 2 | 3 | export namespace Utils { 4 | // https://stackoverflow.com/a/40577337/8037425 5 | export function getAllMethodNames(obj: object, excludeContructors: boolean = false): Array { 6 | let _obj: object | null = obj; 7 | const methods: Set = new Set(); 8 | 9 | // eslint-disable-next-line no-cond-assign 10 | while ((_obj = Reflect.getPrototypeOf(_obj)) && Reflect.getPrototypeOf(_obj)) { 11 | const keys: Array = Reflect.ownKeys(_obj) as Array; 12 | keys.filter((key: string) => !excludeContructors || key !== 'constructor') 13 | .forEach((key: string) => methods.add(key)); 14 | } 15 | 16 | // Convert Symbols to strings, if there are any 17 | return Array.from(methods, value => value.toString()) 18 | .sort(); 19 | } 20 | } 21 | 22 | export namespace TimeZone { 23 | /** 24 | * Returns the amount of time in milliseconds to add to UTC to get 25 | * standard time in this time zone. Because this value is not 26 | * affected by daylight saving time, it is called raw 27 | * offset. 28 | * 29 | * Since JS doesn't have a native function for this, use the lesser offset of January and July. 30 | * 31 | * @return the amount of raw offset time in milliseconds to add to UTC. 32 | */ 33 | export function getRawOffset(timeZoneId: string): number { 34 | const janDateTime = DateTime.fromObject({ 35 | month: 1, 36 | day: 1, 37 | zone: timeZoneId, 38 | }); 39 | const julyDateTime = janDateTime.set({ month: 7 }); 40 | 41 | let rawOffsetMinutes; 42 | if (janDateTime.offset === julyDateTime.offset) { 43 | rawOffsetMinutes = janDateTime.offset; 44 | } else { 45 | const max = Math.max(janDateTime.offset, julyDateTime.offset); 46 | 47 | rawOffsetMinutes = max < 0 48 | ? 0 - max 49 | : 0 - Math.min(janDateTime.offset, julyDateTime.offset); 50 | } 51 | 52 | return rawOffsetMinutes * 60 * 1000; 53 | } 54 | 55 | /** 56 | * Returns a name in the specified style of this TimeZone suitable for presentation to the user in the default locale. 57 | * @param {string} timeZoneId 58 | * @param {DateTime} [date] 59 | * @param {boolean} [short] 60 | */ 61 | export function getDisplayName(timeZoneId: string, date: DateTime = DateTime.local(), short: boolean = false): string { 62 | return Info.normalizeZone(timeZoneId).offsetName(date.toMillis(), { format: short ? 'short' : 'long' }); 63 | } 64 | 65 | /** 66 | * Returns the amount of time to be added to local standard time to get local wall clock time. 67 | * The default implementation returns 3600000 milliseconds (i.e., one hour) if a call to useDaylightTime() returns true. 68 | * Otherwise, 0 (zero) is returned. 69 | * @param {string} timeZoneId 70 | * @return {number} 71 | */ 72 | export function getDSTSavings(timeZoneId: string): number { 73 | return Info.hasDST(timeZoneId) ? 3600000 : 0; 74 | } 75 | 76 | /** 77 | * Returns the offset of this time zone from UTC at the specified date. If Daylight Saving Time is in effect at the 78 | * specified date, the offset value is adjusted with the amount of daylight saving. 79 | * 80 | * This method returns a historically correct offset value if an underlying TimeZone implementation subclass 81 | * supports historical Daylight Saving Time schedule and GMT offset changes. 82 | * @param {string} timeZoneId 83 | * @param {number} millisSinceEpoch 84 | */ 85 | export function getOffset(timeZoneId: string, millisSinceEpoch: number): number { 86 | return Info.normalizeZone(timeZoneId).offset(millisSinceEpoch) * 60 * 1000; 87 | } 88 | } 89 | 90 | /** 91 | * java.util.Calendar 92 | */ 93 | export namespace Calendar { 94 | export const JANUARY: number = 0; 95 | export const FEBRUARY: number = 1; 96 | export const MARCH: number = 2; 97 | export const APRIL: number = 3; 98 | export const MAY: number = 4; 99 | export const JUNE: number = 5; 100 | export const JULY: number = 6; 101 | export const AUGUST: number = 7; 102 | export const SEPTEMBER: number = 8; 103 | export const OCTOBER: number = 9; 104 | export const NOVEMBER: number = 10; 105 | export const DECEMBER: number = 11; 106 | 107 | export const SUNDAY: number = 1; 108 | export const MONDAY: number = 2; 109 | export const TUESDAY: number = 3; 110 | export const WEDNESDAY: number = 4; 111 | export const THURSDAY: number = 5; 112 | export const FRIDAY: number = 6; 113 | export const SATURDAY: number = 7; 114 | 115 | export const DATE = 5; 116 | export const MONTH = 2; 117 | export const YEAR = 1; 118 | } 119 | 120 | /** 121 | * java.lang.Math 122 | */ 123 | export namespace MathUtils { 124 | /** 125 | * java.lang.Math.toRadians 126 | * @param degrees 127 | */ 128 | export function degreesToRadians(degrees: number): number { 129 | return degrees * Math.PI / 180; 130 | } 131 | 132 | /** 133 | * java.lang.Math.toDegrees 134 | * @param radians 135 | */ 136 | export function radiansToDegrees(radians: number): number { 137 | return radians * 180 / Math.PI; 138 | } 139 | } 140 | 141 | /** 142 | * java.lang.String 143 | */ 144 | export namespace StringUtils { 145 | /** 146 | * Compares two strings lexicographically. 147 | * The comparison is based on the Unicode value of each character in 148 | * the strings. The character sequence represented by this 149 | * {@code String} object is compared lexicographically to the 150 | * character sequence represented by the argument string. The result is 151 | * a negative integer if this {@code String} object 152 | * lexicographically precedes the argument string. The result is a 153 | * positive integer if this {@code String} object lexicographically 154 | * follows the argument string. The result is zero if the strings 155 | * are equal; {@code compareTo} returns {@code 0} exactly when 156 | * the {@link #equals(Object)} method would return {@code true}. 157 | *

158 | * This is the definition of lexicographic ordering. If two strings are 159 | * different, then either they have different characters at some index 160 | * that is a valid index for both strings, or their lengths are different, 161 | * or both. If they have different characters at one or more index 162 | * positions, let k be the smallest such index; then the string 163 | * whose character at position k has the smaller value, as 164 | * determined by using the < operator, lexicographically precedes the 165 | * other string. In this case, {@code compareTo} returns the 166 | * difference of the two character values at position {@code k} in 167 | * the two string -- that is, the value: 168 | *

169 |    * this.charAt(k)-anotherString.charAt(k)
170 |    * 
171 | * If there is no index position at which they differ, then the shorter 172 | * string lexicographically precedes the longer string. In this case, 173 | * {@code compareTo} returns the difference of the lengths of the 174 | * strings -- that is, the value: 175 | *
176 |    * this.length()-anotherString.length()
177 |    * 
178 | * 179 | * @param string1 180 | * @param string2 the {@code String} to be compared. 181 | * @return the value {@code 0} if the argument string is equal to 182 | * this string; a value less than {@code 0} if this string 183 | * is lexicographically less than the string argument; and a 184 | * value greater than {@code 0} if this string is 185 | * lexicographically greater than the string argument. 186 | */ 187 | export function compareTo(string1: string, string2: string): number { 188 | let k: number = 0; 189 | while (k < Math.min(string1.length, string2.length)) { 190 | if (string1.substr(k, 1) !== string2.substr(k, 1)) { 191 | return string1.charCodeAt(k) - string2.charCodeAt(k); 192 | } 193 | k++; 194 | } 195 | return string1.length - string2.length; 196 | } 197 | } 198 | 199 | export namespace IntegerUtils { 200 | /** 201 | * Compares 2 numbers 202 | * @param x 203 | * @param y 204 | */ 205 | export function compare(x: number, y: number): number { 206 | if (x === y) return 0; 207 | return x > y ? 1 : -1; 208 | } 209 | 210 | } 211 | 212 | // export const Long_MIN_VALUE = 0; 213 | export const Long_MIN_VALUE = NaN; 214 | export const Double_MIN_VALUE = NaN; 215 | 216 | /** 217 | * @param {number} num 218 | * @param {number} places - The number of places to pad with zeros 219 | * @returns {string} - The formatted integer 220 | */ 221 | export function padZeros(num: number, places: number): string { 222 | const int = Math.trunc(num); 223 | if (int >= Math.pow(10, places)) return int.toString(); 224 | return '0'.repeat(places).concat(int.toString()).slice(-places); 225 | } 226 | 227 | export type ValueOf = T[keyof T]; 228 | -------------------------------------------------------------------------------- /src/util/Zman.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { IntegerUtils, StringUtils } from '../polyfills/Utils'; 4 | import { UnsupportedError } from '../polyfills/errors'; 5 | import { GeoLocation } from './GeoLocation'; 6 | 7 | /** 8 | * A wrapper class for astronomical times / zmanim that is mostly intended to allow sorting collections of astronomical times. 9 | * It has fields for both date/time and duration based zmanim, name / labels as well as a longer description or explanation of a 10 | * zman. 11 | *

12 | * Here is an example of various ways of sorting zmanim. 13 | *

First create the Calendar for the location you would like to calculate: 14 | * 15 | *

 16 |  * String locationName = "Lakewood, NJ";
 17 |  * double latitude = 40.0828; // Lakewood, NJ
 18 |  * double longitude = -74.2094; // Lakewood, NJ
 19 |  * double elevation = 20; // optional elevation correction in Meters
 20 |  * // the String parameter in getTimeZone() has to be a valid timezone listed in {@link java.util.TimeZone#getAvailableIDs()}
 21 |  * TimeZone timeZone = TimeZone.getTimeZone("America/New_York");
 22 |  * GeoLocation location = new GeoLocation(locationName, latitude, longitude, elevation, timeZone);
 23 |  * ComplexZmanimCalendar czc = new ComplexZmanimCalendar(location);
 24 |  * Zman sunset = new Zman(czc.getSunset(), "Sunset");
 25 |  * Zman shaah16 = new Zman(czc.getShaahZmanis16Point1Degrees(), "Shaah zmanis 16.1");
 26 |  * Zman sunrise = new Zman(czc.getSunrise(), "Sunrise");
 27 |  * Zman shaah = new Zman(czc.getShaahZmanisGra(), "Shaah zmanis GRA");
 28 |  * ArrayList<Zman> zl = new ArrayList<Zman>();
 29 |  * zl.add(sunset);
 30 |  * zl.add(shaah16);
 31 |  * zl.add(sunrise);
 32 |  * zl.add(shaah);
 33 |  * //will sort sunset, shaah 1.6, sunrise, shaah GRA
 34 |  * System.out.println(zl);
 35 |  * Collections.sort(zl, Zman.DATE_ORDER);
 36 |  * // will sort sunrise, sunset, shaah, shaah 1.6 (the last 2 are not in any specific order)
 37 |  * Collections.sort(zl, Zman.DURATION_ORDER);
 38 |  * // will sort sunrise, sunset (the first 2 are not in any specific order), shaah GRA, shaah 1.6
 39 |  * Collections.sort(zl, Zman.NAME_ORDER);
 40 |  * // will sort shaah 1.6, shaah GRA, sunrise, sunset
 41 |  * 
42 | * 43 | * @author © Eliyahu Hershfeld 2007-2020 44 | * @todo Add secondary sorting. As of now the {@code Comparator}s in this class do not sort by secondary order. This means that when sorting a 45 | * {@link java.util.Collection} of zmanim and using the {@link #DATE_ORDER} {@code Comparator} will have the duration based zmanim 46 | * at the end, but they will not be sorted by duration. This should be N/A for label based sorting. 47 | */ 48 | export class Zman { 49 | /** 50 | * The name / label of the zman. 51 | */ 52 | label: string | null; 53 | 54 | /** 55 | * The {@link Date} of the zman 56 | */ 57 | zman?: DateTime; 58 | 59 | /** 60 | * The duration if the zman is a {@link AstronomicalCalendar#getTemporalHour() temporal hour} (or the various 61 | * shaah zmanis base times such as {@link ZmanimCalendar#getShaahZmanisGra() shaah Zmanis GRA} or 62 | * {@link ComplexZmanimCalendar#getShaahZmanis16Point1Degrees() shaah Zmanis 16.1°}). 63 | */ 64 | duration?: number; 65 | 66 | /** 67 | * A longer description or explanation of a zman. 68 | */ 69 | description?: string; 70 | 71 | /** 72 | * The location information of the zman. 73 | */ 74 | geoLocation?: GeoLocation; 75 | 76 | /** 77 | * The constructor setting a {@link Date} based zman and a label. 78 | * @param date the Date of the zman. 79 | * @param label the label of the zman such as "Sof Zman Krias Shema GRA". 80 | * @see #Zman(Date, GeoLocation, String) 81 | */ 82 | constructor(date: DateTime, label: string | null) 83 | /** 84 | * The constructor setting a duration based zman such as 85 | * {@link AstronomicalCalendar#getTemporalHour() temporal hour} (or the various shaah zmanis times such as 86 | * {@link ZmanimCalendar#getShaahZmanisGra() shaah zmanis GRA} or 87 | * {@link ComplexZmanimCalendar#getShaahZmanis16Point1Degrees() shaah Zmanis 16.1°}) and label. 88 | * @param duration a duration based zman such as ({@link AstronomicalCalendar#getTemporalHour()} 89 | * @param label the label of the zman such as "Shaah Zmanis GRA". 90 | * @see #Zman(Date, String) 91 | */ 92 | constructor(duration: number, label: string | null) 93 | constructor(date: DateTime, geoLocation: GeoLocation, label: string | null) 94 | constructor(dateOrDuration: number | DateTime, labelOrGeoLocation: string | GeoLocation | null, label?: string | null) { 95 | this.label = label || null; 96 | 97 | if (labelOrGeoLocation instanceof GeoLocation) { 98 | this.geoLocation = labelOrGeoLocation; 99 | } else { 100 | this.label = labelOrGeoLocation; 101 | } 102 | 103 | if (DateTime.isDateTime(dateOrDuration)) { 104 | this.zman = dateOrDuration; 105 | } else { 106 | this.duration = dateOrDuration; 107 | } 108 | } 109 | 110 | /** 111 | * A {@link Comparator} that will compare and sort zmanim by date/time order. Compares its two arguments by the zman's date/time 112 | * order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater 113 | * than the second. 114 | * Please note that this class will handle cases where either the {@code Zman} is a null or {@link #getZman()} returns a null. 115 | */ 116 | static compareDateOrder(zman1: Zman, zman2: Zman): number { 117 | const firstMillis = zman1.zman?.valueOf() || 0; 118 | const secondMillis = zman2.zman?.valueOf() || 0; 119 | 120 | return IntegerUtils.compare(firstMillis, secondMillis); 121 | } 122 | 123 | /** 124 | * A {@link Comparator} that will compare and sort zmanim by zmanim label order. Compares its two arguments by the zmanim label 125 | * name order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater 126 | * than the second. 127 | * Please note that this class will sort cases where either the {@code Zman} is a null or {@link #label} returns a null 128 | * as empty {@code String}s. 129 | */ 130 | static compareNameOrder(zman1: Zman, zman2: Zman): number { 131 | return StringUtils.compareTo(zman1.label || '', zman2.label || ''); 132 | } 133 | 134 | /** 135 | * A {@link Comparator} that will compare and sort duration based zmanim such as 136 | * {@link AstronomicalCalendar#getTemporalHour() temporal hour} (or the various shaah zmanis times 137 | * such as {@link ZmanimCalendar#getShaahZmanisGra() shaah zmanis GRA} or 138 | * {@link ComplexZmanimCalendar#getShaahZmanis16Point1Degrees() shaah zmanis 16.1°}). Returns a negative 139 | * integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. 140 | * Please note that this class will sort cases where {@code Zman} is a null. 141 | */ 142 | static compareDurationOrder(zman1: Zman, zman2: Zman): number { 143 | return IntegerUtils.compare(zman1.duration || 0, zman2.duration || 0); 144 | } 145 | 146 | /** 147 | * A method that returns an XML formatted String representing the serialized Object. Very 148 | * similar to the toString method but the return value is in an xml format. The format currently used (subject to 149 | * change) is: 150 | * 151 | *
152 |    * <Zman>
153 |    *  <Label>Sof Zman Krias Shema GRA</Label>
154 |    *  <Zman>1969-02-08T09:37:56.820</Zman>
155 |    *  <TimeZone>
156 |    *    <TimezoneName>America/Montreal</TimezoneName>
157 |    *    <TimeZoneDisplayName>Eastern Standard Time</TimeZoneDisplayName>
158 |    *    <TimezoneGMTOffset>-5</TimezoneGMTOffset>
159 |    *    <TimezoneDSTOffset>1</TimezoneDSTOffset>
160 |    *  </TimeZone>
161 |    *  <Duration>0</Duration>
162 |    *  <Description>Sof Zman Krias Shema GRA is 3 sha'os zmaniyos calculated from sunrise to sunset.</Description>
163 |    * </Zman>
164 |    * 
165 | * @return The XML formatted String. 166 | * @deprecated 167 | */ 168 | // eslint-disable-next-line class-methods-use-this 169 | public toXML(): void { 170 | throw new UnsupportedError('This method is deprecated'); 171 | } 172 | 173 | toString(): string { 174 | return (`\nLabel:\t${this.label}`) 175 | .concat(`\nZman:\t${this.zman}`) 176 | .concat(`\nnGeoLocation:\t${this.geoLocation?.toString().replace(/\n/g, '\n\t')}`) 177 | .concat(`\nDuration:\t${this.duration}`) 178 | .concat(`\nDescription:\t${this.description}`); 179 | } 180 | } 181 | 182 | export type ZmanWithZmanDate = Zman & { zman: DateTime }; 183 | export type ZmanWithDuration = Zman & { duration: number }; 184 | -------------------------------------------------------------------------------- /src/util/GeoLocationUtils.ts: -------------------------------------------------------------------------------- 1 | import { GeoLocation } from './GeoLocation'; 2 | import { MathUtils } from '../polyfills/Utils'; 3 | 4 | /** 5 | * A class for various location calculations 6 | * Most of the code in this class is ported from Chris Veness' 7 | * LGPL Javascript Implementation 8 | * 9 | * @author © Eliyahu Hershfeld 2009 - 2020 10 | * @deprecated All methods in this class are available in the {@link GeoLocation} class, and this class that duplicates that 11 | * code will be removed in a future release. 12 | */ 13 | export class GeoLocationUtils { 14 | /** 15 | * Constant for a distance type calculation. 16 | * @see #getGeodesicDistance(GeoLocation, GeoLocation) 17 | */ 18 | private static readonly DISTANCE: number = 0; 19 | 20 | /** 21 | * Constant for an initial bearing type calculation. 22 | * @see #getGeodesicInitialBearing(GeoLocation, GeoLocation) 23 | */ 24 | private static readonly INITIAL_BEARING: number = 1; 25 | 26 | /** 27 | * Constant for a final bearing type calculation. 28 | * @see #getGeodesicFinalBearing(GeoLocation, GeoLocation) 29 | */ 30 | private static readonly FINAL_BEARING: number = 2; 31 | 32 | /** 33 | * Calculate the geodesic initial bearing between this Object and 34 | * a second Object passed to this method using Thaddeus 35 | * Vincenty's inverse formula See T Vincenty, "Direct and 36 | * Inverse Solutions of Geodesics on the Ellipsoid with application of nested equations", Survey Review, vol XXII 37 | * no 176, 1975. 38 | * 39 | * @param location 40 | * the initial location 41 | * @param destination 42 | * the destination location 43 | * @return the geodesic bearing 44 | */ 45 | public static getGeodesicInitialBearing(location: GeoLocation, destination: GeoLocation): number { 46 | return GeoLocationUtils.vincentyFormula(location, destination, GeoLocationUtils.INITIAL_BEARING); 47 | } 48 | 49 | /** 50 | * Calculate the geodesic final bearing between this Object 51 | * and a second Object passed to this method using Thaddeus Vincenty's 52 | * inverse formula See T Vincenty, "Direct and Inverse Solutions of Geodesics 53 | * on the Ellipsoid with application of nested equations", Survey Review, vol XXII no 176, 1975. 54 | * 55 | * @param location 56 | * the initial location 57 | * @param destination 58 | * the destination location 59 | * @return the geodesic bearing 60 | */ 61 | public static getGeodesicFinalBearing(location: GeoLocation, destination: GeoLocation): number { 62 | return GeoLocationUtils.vincentyFormula(location, destination, GeoLocationUtils.FINAL_BEARING); 63 | } 64 | 65 | /** 66 | * Calculate geodesic distance in Meters 67 | * between this Object and a second Object passed to this method using Thaddeus Vincenty's inverse formula See T Vincenty, 69 | * "Direct and Inverse Solutions of Geodesics on the 70 | * Ellipsoid with application of nested equations", Survey Review, vol XXII no 176, 1975. This uses the 71 | * WGS-84 geodetic model. 72 | * @param location 73 | * the initial location 74 | * @param destination 75 | * the destination location 76 | * @return the geodesic distance in Meters 77 | */ 78 | public static getGeodesicDistance(location: GeoLocation, destination: GeoLocation): number { 79 | return GeoLocationUtils.vincentyFormula(location, destination, GeoLocationUtils.DISTANCE); 80 | } 81 | 82 | /** 83 | * Calculates the initial geodesic bearing, final bearing or 84 | * geodesic distance using Thaddeus Vincenty's inverse formula See T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid 87 | * with application of nested equations", Survey Review, vol XXII no 176, 1975. 88 | * 89 | * @param location 90 | * the initial location 91 | * @param destination 92 | * the destination location 93 | * @param formula 94 | * This formula calculates initial bearing ({@link #INITIAL_BEARING}), 95 | * final bearing ({@link #FINAL_BEARING}) and distance ({@link #DISTANCE}). 96 | * @return 97 | * the geodesic distance, initial or final bearing (based on the formula passed in) between the location 98 | * and destination in Meters 99 | * @see #getGeodesicDistance(GeoLocation, GeoLocation) 100 | * @see #getGeodesicInitialBearing(GeoLocation, GeoLocation) 101 | * @see #getGeodesicFinalBearing(GeoLocation, GeoLocation) 102 | */ 103 | private static vincentyFormula(location: GeoLocation, destination: GeoLocation, formula: number): number { 104 | const a: number = 6378137; // length of semi-major axis of the ellipsoid (radius at equator) in metres based on WGS-84 105 | const b: number = 6356752.3142; // length of semi-minor axis of the ellipsoid (radius at the poles) in meters based on WGS-84 106 | const f: number = 1 / 298.257223563; // flattening of the ellipsoid based on WGS-84 107 | const L: number = MathUtils.degreesToRadians(destination.getLongitude() - location.getLongitude()); // difference in longitude of two points; 108 | const U1: number = Math.atan((1 - f) * Math.tan(MathUtils.degreesToRadians(location.getLatitude()))); // reduced latitude (latitude on the auxiliary sphere) 109 | const U2: number = Math.atan((1 - f) * Math.tan(MathUtils.degreesToRadians(destination.getLatitude()))); // reduced latitude (latitude on the auxiliary sphere) 110 | 111 | const sinU1: number = Math.sin(U1); 112 | const cosU1: number = Math.cos(U1); 113 | const sinU2: number = Math.sin(U2); 114 | const cosU2: number = Math.cos(U2); 115 | 116 | let lambda: number = L; 117 | let lambdaP: number = 2 * Math.PI; 118 | let iterLimit: number = 20; 119 | let sinLambda: number = 0; 120 | let cosLambda: number = 0; 121 | let sinSigma: number = 0; 122 | let cosSigma: number = 0; 123 | let sigma: number = 0; 124 | let sinAlpha: number = 0; 125 | let cosSqAlpha: number = 0; 126 | let cos2SigmaM: number = 0; 127 | let C: number; 128 | 129 | while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0) { 130 | sinLambda = Math.sin(lambda); 131 | cosLambda = Math.cos(lambda); 132 | sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) 133 | + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) 134 | * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); 135 | if (sinSigma === 0) return 0; // co-incident points 136 | cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; 137 | sigma = Math.atan2(sinSigma, cosSigma); 138 | sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma; 139 | cosSqAlpha = 1 - sinAlpha * sinAlpha; 140 | cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha; 141 | if (Number.isNaN(cos2SigmaM)) cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6) 142 | C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)); 143 | lambdaP = lambda; 144 | lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))); 145 | } 146 | if (iterLimit === 0) return Number.NaN; // formula failed to converge 147 | 148 | const uSq: number = cosSqAlpha * (a * a - b * b) / (b * b); 149 | const A: number = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))); 150 | const B: number = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); 151 | const deltaSigma: number = B * sinSigma * (cos2SigmaM + B / 4 152 | * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM 153 | * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM))); 154 | const distance: number = b * A * (sigma - deltaSigma); 155 | 156 | // initial bearing 157 | const fwdAz: number = MathUtils.radiansToDegrees(Math.atan2(cosU2 * sinLambda, cosU1 158 | * sinU2 - sinU1 * cosU2 * cosLambda)); 159 | // final bearing 160 | const revAz: number = MathUtils.radiansToDegrees(Math.atan2(cosU1 * sinLambda, -sinU1 161 | * cosU2 + cosU1 * sinU2 * cosLambda)); 162 | if (formula === GeoLocationUtils.DISTANCE) { 163 | return distance; 164 | } else if (formula === GeoLocationUtils.INITIAL_BEARING) { 165 | return fwdAz; 166 | } else if (formula === GeoLocationUtils.FINAL_BEARING) { 167 | return revAz; 168 | } 169 | // should never happen 170 | return Number.NaN; 171 | } 172 | 173 | /** 174 | * Returns the rhumb line 175 | * bearing from the current location to the GeoLocation passed in. 176 | * 177 | * @param location 178 | * the initial location 179 | * @param destination 180 | * the destination location 181 | * @return the bearing in degrees 182 | */ 183 | public static getRhumbLineBearing(location: GeoLocation, destination: GeoLocation): number { 184 | let dLon: number = MathUtils.degreesToRadians(destination.getLongitude() - location.getLongitude()); 185 | const dPhi: number = Math.log(Math.tan(MathUtils.degreesToRadians(destination.getLatitude()) 186 | / 2 + Math.PI / 4) 187 | / Math.tan(MathUtils.degreesToRadians(location.getLatitude()) / 2 + Math.PI / 4)); 188 | if (Math.abs(dLon) > Math.PI) dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon); 189 | return MathUtils.radiansToDegrees(Math.atan2(dLon, dPhi)); 190 | } 191 | 192 | /** 193 | * Returns the rhumb line distance from the current 194 | * location to the GeoLocation passed in. Ported from Chris Veness' 195 | * Javascript Implementation. 196 | * 197 | * @param location 198 | * the initial location 199 | * @param destination 200 | * the destination location 201 | * @return the distance in Meters 202 | */ 203 | public static getRhumbLineDistance(location: GeoLocation, destination: GeoLocation): number { 204 | const earthRadius: number = 6378137; // earth's mean radius in km 205 | const dLat: number = MathUtils.degreesToRadians(location.getLatitude()) 206 | - MathUtils.degreesToRadians(destination.getLatitude()); 207 | let dLon: number = Math.abs(MathUtils.degreesToRadians(location.getLongitude()) 208 | - MathUtils.degreesToRadians(destination.getLongitude())); 209 | const dPhi: number = Math.log(Math.tan(MathUtils.degreesToRadians(location.getLatitude()) / 2 + Math.PI / 4) 210 | / Math.tan(MathUtils.degreesToRadians(destination.getLatitude()) / 2 + Math.PI / 4)); 211 | 212 | let q: number = dLat / dPhi; 213 | if (!Number.isFinite(q)) { 214 | q = Math.cos(MathUtils.degreesToRadians(destination.getLatitude())); 215 | } 216 | 217 | // if dLon over 180° take shorter rhumb across 180° meridian: 218 | if (dLon > Math.PI) { 219 | dLon = 2 * Math.PI - dLon; 220 | } 221 | const d: number = Math.sqrt(dLat * dLat + q * q * dLon * dLon); 222 | return d * earthRadius; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/util/SunTimesCalculator.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { GeoLocation } from './GeoLocation'; 4 | import { AstronomicalCalculator } from './AstronomicalCalculator'; 5 | import { MathUtils } from '../polyfills/Utils'; 6 | 7 | /** 8 | * Implementation of sunrise and sunset methods to calculate astronomical times. This calculator uses the Java algorithm 9 | * written by Kevin 10 | * Boone that is based on the US Naval Observatory'sAstronomical Almanac and used with his permission. Added to Kevin's 12 | * code is adjustment of the zenith to account for elevation. This algorithm returns the same time every year and does not 13 | * account for leap years. It is not as accurate as the Jean Meeus based {@link NOAACalculator} that is the default calculator 14 | * use by the KosherJava zmanim library. 15 | * 16 | * @author © Eliyahu Hershfeld 2004 - 2023 17 | * @author © Kevin Boone 2000 18 | */ 19 | export class SunTimesCalculator extends AstronomicalCalculator { 20 | /** 21 | * Default constructor of the SunTimesCalculator. 22 | */ 23 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 24 | constructor() { 25 | super(); 26 | } 27 | 28 | /** 29 | * @see AstronomicalCalculator#getCalculatorName() 30 | */ 31 | // eslint-disable-next-line class-methods-use-this 32 | public getCalculatorName(): string { 33 | return 'US Naval Almanac Algorithm'; 34 | } 35 | 36 | /** 37 | * @see AstronomicalCalculator#getUTCSunrise(Calendar, GeoLocation, double, boolean) 38 | */ 39 | public getUTCSunrise(date: DateTime, geoLocation: GeoLocation, zenith: number, adjustForElevation: boolean): number { 40 | const elevation: number = adjustForElevation ? geoLocation.getElevation() : 0; 41 | const adjustedZenith: number = this.adjustZenith(zenith, elevation); 42 | 43 | return SunTimesCalculator.getTimeUTC(date, geoLocation, adjustedZenith, true); 44 | } 45 | 46 | /** 47 | * @see AstronomicalCalculator#getUTCSunset(Calendar, GeoLocation, double, boolean) 48 | */ 49 | public getUTCSunset(date: DateTime, geoLocation: GeoLocation, zenith: number, adjustForElevation: boolean): number { 50 | const elevation: number = adjustForElevation ? geoLocation.getElevation() : 0; 51 | const adjustedZenith: number = this.adjustZenith(zenith, elevation); 52 | 53 | return SunTimesCalculator.getTimeUTC(date, geoLocation, adjustedZenith, false); 54 | } 55 | 56 | /** 57 | * The number of degrees of longitude that corresponds to one hour of time difference. 58 | */ 59 | private static readonly DEG_PER_HOUR: number = 360 / 24; 60 | 61 | /** 62 | * The sine in degrees. 63 | * @param deg the degrees 64 | * @return sin of the angle in degrees 65 | */ 66 | private static sinDeg(deg: number): number { 67 | // return Math.sin(deg * 2 * Math.PI / 360); 68 | return Math.sin(MathUtils.degreesToRadians(deg)); 69 | } 70 | 71 | /** 72 | * Return the arc cosine in degrees. 73 | * @param x angle 74 | * @return acos of the angle in degrees 75 | */ 76 | private static acosDeg(x: number): number { 77 | // return Math.acos(x) * 360 / (2 * Math.PI); 78 | return MathUtils.radiansToDegrees(Math.acos(x)); 79 | } 80 | 81 | /** 82 | * Return the arc sine in degrees. 83 | * @param x angle 84 | * @return asin of the angle in degrees 85 | */ 86 | private static asinDeg(x: number): number { 87 | // return Math.asin(x) * 360 / (2 * Math.PI); 88 | return MathUtils.radiansToDegrees(Math.asin(x)); 89 | } 90 | 91 | /** 92 | * Return the tangent in degrees. 93 | * @param deg degrees 94 | * @return tan of the angle in degrees 95 | */ 96 | private static tanDeg(deg: number): number { 97 | // return Math.tan(deg * 2 * Math.PI / 360); 98 | return Math.tan(MathUtils.degreesToRadians(deg)); 99 | } 100 | 101 | /** 102 | * Calculate cosine of the angle in degrees 103 | * 104 | * @param deg degrees 105 | * @return cosine of the angle in degrees 106 | */ 107 | private static cosDeg(deg: number): number { 108 | // return Math.cos(deg * 2 * Math.PI / 360); 109 | return Math.cos(MathUtils.degreesToRadians(deg)); 110 | } 111 | 112 | /** 113 | * Get time difference between location's longitude and the Meridian, in hours. 114 | * 115 | * @param longitude the longitude 116 | * @return time difference between the location's longitude and the Meridian, in hours. West of Meridian has a negative time difference 117 | */ 118 | private static getHoursFromMeridian(longitude: number): number { 119 | return longitude / SunTimesCalculator.DEG_PER_HOUR; 120 | } 121 | 122 | /** 123 | * Calculate the approximate time of sunset or sunrise in days since midnight Jan 1st, assuming 6am and 6pm events. We 124 | * need this figure to derive the Sun's mean anomaly. 125 | * 126 | * @param dayOfYear the day of year 127 | * @param hoursFromMeridian hours from the meridian 128 | * @param isSunrise true for sunrise and false for sunset 129 | * 130 | * @return the approximate time of sunset or sunrise in days since midnight Jan 1st, assuming 6am and 6pm events. We 131 | * need this figure to derive the Sun's mean anomaly. 132 | */ 133 | private static getApproxTimeDays(dayOfYear: number, hoursFromMeridian: number, isSunrise: boolean): number { 134 | if (isSunrise) { 135 | return dayOfYear + ((6 - hoursFromMeridian) / 24); 136 | } 137 | // sunset 138 | return dayOfYear + ((18 - hoursFromMeridian) / 24); 139 | } 140 | 141 | /** 142 | * Calculate the Sun's mean anomaly in degrees, at sunrise or sunset, given the longitude in degrees 143 | * 144 | * @param dayOfYear the day of the year 145 | * @param longitude longitude 146 | * @param isSunrise true for sunrise and false for sunset 147 | * @return the Sun's mean anomaly in degrees 148 | */ 149 | private static getMeanAnomaly(dayOfYear: number, longitude: number, isSunrise: boolean): number { 150 | return (0.9856 * SunTimesCalculator.getApproxTimeDays(dayOfYear, SunTimesCalculator.getHoursFromMeridian(longitude), isSunrise)) - 3.289; 151 | } 152 | 153 | /** 154 | * Returns the Sun's true longitude in degrees. 155 | * @param sunMeanAnomaly the Sun's mean anomaly in degrees 156 | * @return the Sun's true longitude in degrees. The result is an angle >= 0 and <= 360. 157 | */ 158 | private static getSunTrueLongitude(sunMeanAnomaly: number): number { 159 | let l: number = sunMeanAnomaly + (1.916 * SunTimesCalculator.sinDeg(sunMeanAnomaly)) + (0.020 * SunTimesCalculator.sinDeg(2 * sunMeanAnomaly)) + 282.634; 160 | 161 | // get longitude into 0-360 degree range 162 | if (l >= 360) { 163 | l = l - 360; 164 | } 165 | if (l < 0) { 166 | l = l + 360; 167 | } 168 | return l; 169 | } 170 | 171 | /** 172 | * Calculates the Sun's right ascension in hours. 173 | * @param sunTrueLongitude the Sun's true longitude in degrees > 0 and < 360. 174 | * @return the Sun's right ascension in hours in angles > 0 and < 360. 175 | */ 176 | private static getSunRightAscensionHours(sunTrueLongitude: number): number { 177 | const a: number = 0.91764 * SunTimesCalculator.tanDeg(sunTrueLongitude); 178 | let ra: number = 360 / (2 * Math.PI) * Math.atan(a); 179 | 180 | const lQuadrant: number = Math.floor(sunTrueLongitude / 90) * 90; 181 | const raQuadrant: number = Math.floor(ra / 90) * 90; 182 | ra += (lQuadrant - raQuadrant); 183 | 184 | return ra / SunTimesCalculator.DEG_PER_HOUR; // convert to hours 185 | } 186 | 187 | /** 188 | * Calculate the cosine of the Sun's local hour angle 189 | * 190 | * @param sunTrueLongitude the sun's true longitude 191 | * @param latitude the latitude 192 | * @param zenith the zenith 193 | * @return the cosine of the Sun's local hour angle 194 | */ 195 | private static getCosLocalHourAngle(sunTrueLongitude: number, latitude: number, zenith: number): number { 196 | const sinDec: number = 0.39782 * SunTimesCalculator.sinDeg(sunTrueLongitude); 197 | const cosDec: number = SunTimesCalculator.cosDeg(SunTimesCalculator.asinDeg(sinDec)); 198 | return (SunTimesCalculator.cosDeg(zenith) - (sinDec * SunTimesCalculator.sinDeg(latitude))) / (cosDec * SunTimesCalculator.cosDeg(latitude)); 199 | } 200 | 201 | /** 202 | * Calculate local mean time of rising or setting. By `local' is meant the exact time at the location, assuming that 203 | * there were no time zone. That is, the time difference between the location and the Meridian depended entirely on 204 | * the longitude. We can't do anything with this time directly; we must convert it to UTC and then to a local time. 205 | * The result is expressed as a fractional number of hours since midnight 206 | * 207 | * @param localHour the local hour 208 | * @param sunRightAscensionHours the sun's right ascension in hours 209 | * @param approxTimeDays approximate time days 210 | * 211 | * @return the fractional number of hours since midnight as a double 212 | */ 213 | private static getLocalMeanTime(localHour: number, sunRightAscensionHours: number, approxTimeDays: number): number { 214 | return localHour + sunRightAscensionHours - (0.06571 * approxTimeDays) - 6.622; 215 | } 216 | 217 | /** 218 | * Get sunrise or sunset time in UTC, according to flag. This time is returned as 219 | * a double and is not adjusted for time-zone. 220 | * 221 | * @param calendar 222 | * the Calendar object to extract the day of year for calculation 223 | * @param geoLocation 224 | * the GeoLocation object that contains the latitude and longitude 225 | * @param zenith 226 | * Sun's zenith, in degrees 227 | * @param isSunrise 228 | * True for sunrise and false for sunset. 229 | * @return the time as a double. If an error was encountered in the calculation 230 | * (expected behavior for some locations such as near the poles, 231 | * {@link Double#NaN} will be returned. 232 | */ 233 | private static getTimeUTC(date: DateTime, geoLocation: GeoLocation, zenith: number, isSunrise: boolean): number { 234 | const dayOfYear: number = date.ordinal; 235 | const sunMeanAnomaly: number = SunTimesCalculator.getMeanAnomaly(dayOfYear, geoLocation.getLongitude(), isSunrise); 236 | const sunTrueLong: number = SunTimesCalculator.getSunTrueLongitude(sunMeanAnomaly); 237 | const sunRightAscensionHours: number = SunTimesCalculator.getSunRightAscensionHours(sunTrueLong); 238 | const cosLocalHourAngle: number = SunTimesCalculator.getCosLocalHourAngle(sunTrueLong, geoLocation.getLatitude(), zenith); 239 | 240 | let localHourAngle: number; 241 | if (isSunrise) { 242 | localHourAngle = 360 - SunTimesCalculator.acosDeg(cosLocalHourAngle); 243 | } else { // sunset 244 | localHourAngle = SunTimesCalculator.acosDeg(cosLocalHourAngle); 245 | } 246 | const localHour: number = localHourAngle / SunTimesCalculator.DEG_PER_HOUR; 247 | 248 | const localMeanTime: number = SunTimesCalculator.getLocalMeanTime(localHour, sunRightAscensionHours, 249 | SunTimesCalculator.getApproxTimeDays(dayOfYear, SunTimesCalculator.getHoursFromMeridian(geoLocation.getLatitude()), isSunrise)); 250 | const processedTime = localMeanTime - SunTimesCalculator.getHoursFromMeridian(geoLocation.getLongitude()); 251 | 252 | return processedTime > 0 ? processedTime % 24 : (processedTime % 24) + 24; // ensure that the time is >= 0 and < 24 253 | } 254 | 255 | /** 256 | * Return the Universal Coordinated Time (UTC) 257 | * of solar noon for the given day at the given location 258 | * on earth. This implementation returns solar noon as the time halfway between sunrise and sunset. 259 | * {@link NOAACalculator}, the default calculator, returns true solar noon. See The Definition of Chatzos for details on solar 261 | * noon calculations. 262 | * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation) 263 | * @see NOAACalculator 264 | * 265 | * @param date 266 | * The Calendar representing the date to calculate solar noon for 267 | * @param geoLocation 268 | * The location information used for astronomical calculating sun times. 269 | * @return the time in minutes from zero UTC. If an error was encountered in the calculation (expected behavior for 270 | * some locations such as near the poles, {@link Double#NaN} will be returned. 271 | */ 272 | public getUTCNoon(date: DateTime, geoLocation: GeoLocation): number { 273 | const sunrise: number = this.getUTCSunrise(date, geoLocation, 90, false); 274 | const sunset: number = this.getUTCSunset(date, geoLocation, 90, false); 275 | 276 | let noon: number = (sunrise + ((sunset - sunrise) / 2)); 277 | if (noon < 0) noon += 12; 278 | if (noon < sunrise) noon -= 12; 279 | 280 | return noon; 281 | } 282 | 283 | /** 284 | * Return the Universal Coordinated Time (UTC) 285 | * of midnight for the given day at the given location on earth. This implementation returns solar midnight as 12 hours 286 | * after utc noon that is halfway between sunrise and sunset. 287 | * {@link NOAACalculator}, the default calculator, returns true solar noon. See The Definition of Chatzos for details on solar 289 | * noon calculations. 290 | * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation) 291 | * @see NOAACalculator 292 | * 293 | * @param calendar 294 | * The Calendar representing the date to calculate solar noon for 295 | * @param geoLocation 296 | * The location information used for astronomical calculating sun times. 297 | * @return the time in minutes from zero UTC. If an error was encountered in the calculation (expected behavior for 298 | * some locations such as near the poles, {@link Double#NaN} will be returned. 299 | */ 300 | public getUTCMidnight(date: DateTime, geoLocation: GeoLocation): number { 301 | return this.getUTCNoon(date, geoLocation) + 12; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/hebrewcalendar/Daf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An Object representing a daf (page) in the Daf Yomi cycle. 3 | * 4 | * @author © Eliyahu Hershfeld 2011 - 2023 5 | */ 6 | export class Daf { 7 | /** 8 | * See {@link #getMasechtaNumber()} and {@link #setMasechtaNumber(int)}. 9 | */ 10 | private masechtaNumber: number; 11 | 12 | /** 13 | * See {@link #getDaf()} and {@link #setDaf(int)}. 14 | */ 15 | private daf: number; 16 | 17 | /** 18 | * See {@link #getMasechtaTransliterated()} and {@link #setMasechtaTransliterated(String[])}. 19 | */ 20 | private static masechtosBavliTransliterated: string[] = ['Berachos', 'Shabbos', 'Eruvin', 'Pesachim', 'Shekalim', 21 | 'Yoma', 'Sukkah', 'Beitzah', 'Rosh Hashana', 'Taanis', 'Megillah', 'Moed Katan', 'Chagigah', 'Yevamos', 22 | 'Kesubos', 'Nedarim', 'Nazir', 'Sotah', 'Gitin', 'Kiddushin', 'Bava Kamma', 'Bava Metzia', 'Bava Basra', 23 | 'Shevuos', 'Makkos', 'Sanhedrin', 'Avodah Zarah', 'Horiyos', 'Zevachim', 'Menachos', 'Chullin', 'Bechoros', 24 | 'Arachin', 'Temurah', 'Kerisos', 'Meilah', 'Kinnim', 'Tamid', 'Midos', 'Niddah']; 25 | 26 | /** 27 | * See {@link #getMasechta()}. 28 | */ 29 | private static readonly masechtosBavli: string[] = ['\u05D1\u05E8\u05DB\u05D5\u05EA', '\u05E9\u05D1\u05EA', 30 | '\u05E2\u05D9\u05E8\u05D5\u05D1\u05D9\u05DF', '\u05E4\u05E1\u05D7\u05D9\u05DD', 31 | '\u05E9\u05E7\u05DC\u05D9\u05DD', '\u05D9\u05D5\u05DE\u05D0', '\u05E1\u05D5\u05DB\u05D4', 32 | '\u05D1\u05D9\u05E6\u05D4', '\u05E8\u05D0\u05E9 \u05D4\u05E9\u05E0\u05D4', 33 | '\u05EA\u05E2\u05E0\u05D9\u05EA', '\u05DE\u05D2\u05D9\u05DC\u05D4', 34 | '\u05DE\u05D5\u05E2\u05D3 \u05E7\u05D8\u05DF', '\u05D7\u05D2\u05D9\u05D2\u05D4', 35 | '\u05D9\u05D1\u05DE\u05D5\u05EA', '\u05DB\u05EA\u05D5\u05D1\u05D5\u05EA', '\u05E0\u05D3\u05E8\u05D9\u05DD', 36 | '\u05E0\u05D6\u05D9\u05E8', '\u05E1\u05D5\u05D8\u05D4', '\u05D2\u05D9\u05D8\u05D9\u05DF', 37 | '\u05E7\u05D9\u05D3\u05D5\u05E9\u05D9\u05DF', '\u05D1\u05D1\u05D0 \u05E7\u05DE\u05D0', 38 | '\u05D1\u05D1\u05D0 \u05DE\u05E6\u05D9\u05E2\u05D0', '\u05D1\u05D1\u05D0 \u05D1\u05EA\u05E8\u05D0', 39 | '\u05E1\u05E0\u05D4\u05D3\u05E8\u05D9\u05DF', '\u05DE\u05DB\u05D5\u05EA', 40 | '\u05E9\u05D1\u05D5\u05E2\u05D5\u05EA', '\u05E2\u05D1\u05D5\u05D3\u05D4 \u05D6\u05E8\u05D4', 41 | '\u05D4\u05D5\u05E8\u05D9\u05D5\u05EA', '\u05D6\u05D1\u05D7\u05D9\u05DD', '\u05DE\u05E0\u05D7\u05D5\u05EA', 42 | '\u05D7\u05D5\u05DC\u05D9\u05DF', '\u05D1\u05DB\u05D5\u05E8\u05D5\u05EA', '\u05E2\u05E8\u05DB\u05D9\u05DF', 43 | '\u05EA\u05DE\u05D5\u05E8\u05D4', '\u05DB\u05E8\u05D9\u05EA\u05D5\u05EA', '\u05DE\u05E2\u05D9\u05DC\u05D4', 44 | '\u05E7\u05D9\u05E0\u05D9\u05DD', '\u05EA\u05DE\u05D9\u05D3', '\u05DE\u05D9\u05D3\u05D5\u05EA', 45 | '\u05E0\u05D3\u05D4']; 46 | 47 | /** 48 | * See {@link #getYerushlmiMasechtaTransliterated()}. 49 | */ 50 | private static masechtosYerushalmiTransliterated: string[] = ['Berachos', 'Pe\'ah', 'Demai', 'Kilayim', 'Shevi\'is', 51 | 'Terumos', 'Ma\'asros', 'Ma\'aser Sheni', 'Chalah', 'Orlah', 'Bikurim', 'Shabbos', 'Eruvin', 'Pesachim', 52 | 'Beitzah', 'Rosh Hashanah', 'Yoma', 'Sukah', 'Ta\'anis', 'Shekalim', 'Megilah', 'Chagigah', 'Moed Katan', 53 | 'Yevamos', 'Kesuvos', 'Sotah', 'Nedarim', 'Nazir', 'Gitin', 'Kidushin', 'Bava Kama', 'Bava Metzia', 54 | 'Bava Basra', 'Sanhedrin', 'Makos', 'Shevuos', 'Avodah Zarah', 'Horayos', 'Nidah', 'No Daf Today']; 55 | 56 | /** 57 | * See {@link #getYerushalmiMasechta()}. 58 | */ 59 | private static readonly masechtosYerushalmi: string[] = ['\u05d1\u05e8\u05db\u05d5\u05ea', '\u05e4\u05d9\u05d0\u05d4', 60 | '\u05d3\u05de\u05d0\u05d9', '\u05db\u05dc\u05d0\u05d9\u05dd', '\u05e9\u05d1\u05d9\u05e2\u05d9\u05ea', 61 | '\u05ea\u05e8\u05d5\u05de\u05d5\u05ea', '\u05de\u05e2\u05e9\u05e8\u05d5\u05ea', '\u05de\u05e2\u05e9\u05e8 \u05e9\u05e0\u05d9', 62 | '\u05d7\u05dc\u05d4', '\u05e2\u05d5\u05e8\u05dc\u05d4', '\u05d1\u05d9\u05db\u05d5\u05e8\u05d9\u05dd', 63 | '\u05e9\u05d1\u05ea', '\u05e2\u05d9\u05e8\u05d5\u05d1\u05d9\u05df', '\u05e4\u05e1\u05d7\u05d9\u05dd', 64 | '\u05d1\u05d9\u05e6\u05d4', '\u05e8\u05d0\u05e9 \u05d4\u05e9\u05e0\u05d4', '\u05d9\u05d5\u05de\u05d0', 65 | '\u05e1\u05d5\u05db\u05d4', '\u05ea\u05e2\u05e0\u05d9\u05ea', '\u05e9\u05e7\u05dc\u05d9\u05dd', '\u05de\u05d2\u05d9\u05dc\u05d4', 66 | '\u05d7\u05d2\u05d9\u05d2\u05d4', '\u05de\u05d5\u05e2\u05d3 \u05e7\u05d8\u05df', '\u05d9\u05d1\u05de\u05d5\u05ea', 67 | '\u05db\u05ea\u05d5\u05d1\u05d5\u05ea', '\u05e1\u05d5\u05d8\u05d4', '\u05e0\u05d3\u05e8\u05d9\u05dd', '\u05e0\u05d6\u05d9\u05e8', 68 | '\u05d2\u05d9\u05d8\u05d9\u05df', '\u05e7\u05d9\u05d3\u05d5\u05e9\u05d9\u05df', '\u05d1\u05d1\u05d0 \u05e7\u05de\u05d0', 69 | '\u05d1\u05d1\u05d0 \u05de\u05e6\u05d9\u05e2\u05d0', '\u05d1\u05d1\u05d0 \u05d1\u05ea\u05e8\u05d0', 70 | '\u05e9\u05d1\u05d5\u05e2\u05d5\u05ea', '\u05de\u05db\u05d5\u05ea', '\u05e1\u05e0\u05d4\u05d3\u05e8\u05d9\u05df', 71 | '\u05e2\u05d1\u05d5\u05d3\u05d4 \u05d6\u05e8\u05d4', '\u05d4\u05d5\u05e8\u05d9\u05d5\u05ea', '\u05e0\u05d9\u05d3\u05d4', 72 | '\u05d0\u05d9\u05df \u05d3\u05e3 \u05d4\u05d9\u05d5\u05dd']; 73 | 74 | /** 75 | * Gets the masechta number of the currently set Daf. The sequence is: Berachos, Shabbos, Eruvin, 76 | * Pesachim, Shekalim, Yoma, Sukkah, Beitzah, Rosh Hashana, Taanis, Megillah, Moed Katan, Chagigah, Yevamos, Kesubos, 77 | * Nedarim, Nazir, Sotah, Gitin, Kiddushin, Bava Kamma, Bava Metzia, Bava Basra, Sanhedrin, Makkos, Shevuos, Avodah 78 | * Zarah, Horiyos, Zevachim, Menachos, Chullin, Bechoros, Arachin, Temurah, Kerisos, Meilah, Kinnim, Tamid, Midos and 79 | * Niddah. 80 | * @return the masechtaNumber. 81 | * @see #setMasechtaNumber(int) 82 | */ 83 | public getMasechtaNumber(): number { 84 | return this.masechtaNumber; 85 | } 86 | 87 | /** 88 | * Set the masechta number in the order of the Daf Yomi. The sequence is: Berachos, Shabbos, Eruvin, Pesachim, 89 | * Shekalim, Yoma, Sukkah, Beitzah, Rosh Hashana, Taanis, Megillah, Moed Katan, Chagigah, Yevamos, Kesubos, Nedarim, 90 | * Nazir, Sotah, Gitin, Kiddushin, Bava Kamma, Bava Metzia, Bava Basra, Sanhedrin, Makkos, Shevuos, Avodah Zarah, 91 | * Horiyos, Zevachim, Menachos, Chullin, Bechoros, Arachin, Temurah, Kerisos, Meilah, Kinnim, Tamid, Midos and 92 | * Niddah. 93 | * 94 | * @param masechtaNumber 95 | * the masechta number in the order of the Daf Yomi to set. 96 | */ 97 | public setMasechtaNumber(masechtaNumber: number): void { 98 | this.masechtaNumber = masechtaNumber; 99 | } 100 | 101 | /** 102 | * Constructor that creates a Daf setting the {@link #setMasechtaNumber(int) masechta number} and 103 | * {@link #setDaf(int) daf number}. 104 | * 105 | * @param masechtaNumber the masechta number in the order of the Daf Yomi to set as the current masechta. 106 | * @param daf the daf (page) number to set. 107 | */ 108 | constructor(masechtaNumber: number, daf: number) { 109 | this.masechtaNumber = masechtaNumber; 110 | this.daf = daf; 111 | } 112 | 113 | /** 114 | * Returns the daf (page) number of the Daf Yomi. 115 | * @return the daf (page) number of the Daf Yomi. 116 | */ 117 | public getDaf(): number { 118 | return this.daf; 119 | } 120 | 121 | /** 122 | * Sets the daf (page) number of the Daf Yomi. 123 | * @param daf the daf (page) number. 124 | */ 125 | public setDaf(daf: number): void { 126 | this.daf = daf; 127 | } 128 | 129 | /** 130 | * Returns the transliterated name of the masechta (tractate) of the Daf Yomi. The list of mashechtos 131 | * is: Berachos, Shabbos, Eruvin, Pesachim, Shekalim, Yoma, Sukkah, Beitzah, Rosh Hashana, Taanis, Megillah, Moed Katan, 132 | * Chagigah, Yevamos, Kesubos, Nedarim, Nazir, Sotah, Gitin, Kiddushin, Bava Kamma, Bava Metzia, Bava Basra, Sanhedrin, 133 | * Makkos, Shevuos, Avodah Zarah, Horiyos, Zevachim, Menachos, Chullin, Bechoros, Arachin, Temurah, Kerisos, Meilah, 134 | * Kinnim, Tamid, Midos and Niddah. 135 | * 136 | * @return the transliterated name of the masechta (tractate) of the Daf Yomi such as Berachos. 137 | * @see #setMasechtaTransliterated(String[]) 138 | */ 139 | public getMasechtaTransliterated(): string { 140 | return Daf.masechtosBavliTransliterated[this.masechtaNumber]; 141 | } 142 | 143 | /** 144 | * Setter method to allow overriding of the default list of masechtos transliterated into Latin chars. 145 | * The default values use Ashkenazi American English transliteration. 146 | * 147 | * @param masechtosBavliTransliterated the list of transliterated Bavli masechtos to set. 148 | * @see #getMasechtaTransliterated() 149 | */ 150 | public static setMasechtaTransliterated(masechtosBavliTransliterated: string[]): void { 151 | Daf.masechtosBavliTransliterated = masechtosBavliTransliterated; 152 | } 153 | 154 | /** 155 | * Returns the masechta (tractate) of the Daf Yomi in Hebrew. The list is in the following format
156 | * ["ברכות", 157 | * "שבת", "עירובין", 158 | * "פסחים", "שקלים", "יומא", 159 | * "סוכה", "ביצה", "ראש השנה", 160 | * "תענית", "מגילה", "מועד 161 | * קטן", "חגיגה", "יבמות", 162 | * "כתובות", "נדרים","נזיר", 163 | * "סוטה", "גיטין", "קידושין", 164 | * "בבא קמא", "בבא מציעא", 165 | * "בבא בתרא", "סנהדרין", 166 | * "מכות", "שבועות", "עבודה 167 | * זרה", "הוריות", "זבחים", 168 | * "מנחות", "חולין", "בכורות", 169 | * "ערכין", "תמורה", "כריתות", 170 | * "מעילה", "קינים", "תמיד", 171 | * "מידות", "נדה"]. 172 | * 173 | * @return the masechta (tractate) of the Daf Yomi in Hebrew. As an example, it will return 174 | * ברכות for Berachos. 175 | */ 176 | public getMasechta(): string { 177 | return Daf.masechtosBavli[this.masechtaNumber]; 178 | } 179 | 180 | /** 181 | * Returns the transliterated name of the masechta (tractate) of the Daf Yomi in Yerushalmi. The list of 182 | * mashechtos is: 183 | * Berachos, Pe'ah, Demai, Kilayim, Shevi'is, Terumos, Ma'asros, Ma'aser Sheni, Chalah, Orlah, Bikurim, 184 | * Shabbos, Eruvin, Pesachim, Beitzah, Rosh Hashanah, Yoma, Sukah, Ta'anis, Shekalim, Megilah, Chagigah, 185 | * Moed Katan, Yevamos, Kesuvos, Sotah, Nedarim, Nazir, Gitin, Kidushin, Bava Kama, Bava Metzia, 186 | * Bava Basra, Shevuos, Makos, Sanhedrin, Avodah Zarah, Horayos, Nidah and No Daf Today. 187 | * 188 | * @return the transliterated name of the masechta (tractate) of the Daf Yomi such as Berachos. 189 | */ 190 | public getYerushalmiMasechtaTransliterated(): string { 191 | return Daf.masechtosYerushalmiTransliterated[this.masechtaNumber]; 192 | } 193 | 194 | /** 195 | * @see #getYerushalmiMasechtaTransliterated() 196 | * @deprecated misspelled method name to be removed. 197 | * @return the transliterated name of the masechta (tractate) of the Daf Yomi such as Berachos. 198 | */ 199 | public getYerushlmiMasechtaTransliterated(): string { 200 | return this.getYerushalmiMasechtaTransliterated(); 201 | } 202 | 203 | /** 204 | * Setter method to allow overriding of the default list of Yerushalmi masechtos transliterated into Latin chars. 205 | * The default uses Ashkenazi American English transliteration. 206 | * 207 | * @param masechtosYerushalmiTransliterated the list of transliterated Yerushalmi masechtos to set. 208 | */ 209 | public static setYerushalmiMasechtaTransliterated(masechtosYerushalmiTransliterated: string[]): void { 210 | Daf.masechtosYerushalmiTransliterated = masechtosYerushalmiTransliterated; 211 | } 212 | 213 | /** 214 | * @see #setYerushalmiMasechtaTransliterated(String[]) 215 | * @deprecated misspelled method name to be removed. 216 | * @param masechtosYerushalmiTransliterated the list of transliterated Yerushalmi masechtos to set. 217 | */ 218 | public static setYerushlmiMasechtaTransliterated(masechtosYerushalmiTransliterated: string[]): void { 219 | Daf.setYerushalmiMasechtaTransliterated(masechtosYerushalmiTransliterated); 220 | } 221 | 222 | /** 223 | * Getter method to allow retrieving the list of Yerushalmi masechtos transliterated into Latin chars. 224 | * The default uses Ashkenazi American English transliteration. 225 | * 226 | * @return the array of transliterated masechta (tractate) names of the Daf Yomi Yerushalmi. 227 | */ 228 | public static getYerushalmiMasechtosTransliterated(): string[] { 229 | return Daf.masechtosYerushalmiTransliterated; 230 | } 231 | 232 | /** 233 | * @see #getYerushalmiMasechtosTransliterated() 234 | * @deprecated misspelled method name to be removed. 235 | * @return the array of transliterated masechta (tractate) names of the Daf Yomi Yerushalmi. 236 | */ 237 | public static getYerushlmiMasechtosTransliterated(): string[] { 238 | return Daf.getYerushalmiMasechtosTransliterated(); 239 | } 240 | 241 | /** 242 | * Getter method to allow retrieving the list of Yerushalmi masechtos. 243 | * 244 | * @return the array of Hebrew masechta (tractate) names of the Daf Yomi Yerushalmi. 245 | */ 246 | public static getYerushalmiMasechtos(): string[] { 247 | return Daf.masechtosYerushalmi; 248 | } 249 | 250 | /** 251 | * @see #getYerushalmiMasechtos() 252 | * @deprecated misspelled method name to be removed in 3.0.0. 253 | * @return the array of Hebrew masechta (tractate) names of the Daf Yomi Yerushalmi. 254 | */ 255 | public static getYerushlmiMasechtos(): string[] { 256 | return Daf.getYerushalmiMasechtos(); 257 | } 258 | 259 | /** 260 | * Returns the Yerushlmi masechta (tractate) of the Daf Yomi in Hebrew, It will return 261 | * ברכות for Berachos. 262 | * 263 | * @return the Yerushalmi masechta (tractate) of the Daf Yomi in Hebrew, It will return 264 | * ברכות for Berachos. 265 | */ 266 | public getYerushalmiMasechta(): string { 267 | return Daf.masechtosYerushalmi[this.masechtaNumber]; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/util/AstronomicalCalculator.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { GeoLocation } from './GeoLocation'; 4 | import { MathUtils } from '../polyfills/Utils'; 5 | import { UnsupportedError } from '../polyfills/errors'; 6 | 7 | /** 8 | * An abstract class that all sun time calculating classes extend. This allows the algorithm used to be changed at 9 | * runtime, easily allowing comparison the results of using different algorithms. 10 | * TODO: Consider methods that would allow atmospheric modeling. This can currently be adjusted by {@link 11 | * #setRefraction(double) setting the refraction}. 12 | * 13 | * @author © Eliyahu Hershfeld 2004 - 2020 14 | */ 15 | export abstract class AstronomicalCalculator { 16 | /** 17 | * The commonly used average solar refraction. Calendrical Calculations lists a more accurate global average of 34.478885263888294 19 | * 20 | * @see #getRefraction() 21 | */ 22 | private refraction: number = 34 / 60; 23 | 24 | /** 25 | * The commonly used average solar radius in minutes of a degree. 26 | * 27 | * @see #getSolarRadius() 28 | */ 29 | private solarRadius: number = 16 / 60; 30 | 31 | /** 32 | * The commonly used average earth radius in KM. At this time, this only affects elevation adjustment and not the 33 | * sunrise and sunset calculations. The value currently defaults to 6356.9 KM. 34 | * 35 | * @see #getEarthRadius() 36 | * @see #setEarthRadius(double) 37 | */ 38 | private earthRadius: number = 6356.9; // in KM 39 | 40 | /** 41 | * Default constructor using the default {@link #refraction refraction}, {@link #solarRadius solar radius} and 42 | * {@link #earthRadius earth radius}. 43 | */ 44 | public AstronomicalCalculator() { 45 | // keep the defaults for now. 46 | } 47 | 48 | /** 49 | * A method that returns the earth radius in KM. The value currently defaults to 6356.9 KM if not set. 50 | * 51 | * @return the earthRadius the earth radius in KM. 52 | */ 53 | public getEarthRadius(): number { 54 | return this.earthRadius; 55 | } 56 | 57 | /** 58 | * A method that allows setting the earth's radius. 59 | * 60 | * @param earthRadius 61 | * the earthRadius to set in KM 62 | */ 63 | public setEarthRadius(earthRadius: number): void { 64 | this.earthRadius = earthRadius; 65 | } 66 | 67 | /** 68 | * The zenith of astronomical sunrise and sunset. The sun is 90° from the vertical 0° 69 | */ 70 | private static readonly GEOMETRIC_ZENITH: number = 90; 71 | 72 | /** 73 | * Returns the default class for calculating sunrise and sunset. This is currently the more accurate 74 | * {@link NOAACalculator}, but this may change in the future. 75 | * 76 | * @return AstronomicalCalculator the default class for calculating sunrise and sunset. In the current 77 | * implementation the default calculator returned is the more accurate {@link NOAACalculator}. 78 | * @deprecated This depends on a circular dependency. Use
new NOAACalculator()
instead 79 | */ 80 | public static getDefault(): void { 81 | throw new UnsupportedError('This method is deprecated, due to the fact that it depends on a circular dependency. ' 82 | + 'Use `new NOAACalculator()` instead.'); 83 | } 84 | 85 | /** 86 | * Returns the name of the algorithm. 87 | * 88 | * @return the descriptive name of the algorithm. 89 | */ 90 | public abstract getCalculatorName(): string; 91 | 92 | /** 93 | * A method that calculates UTC sunrise as well as any time based on an angle above or below sunrise. This abstract 94 | * method is implemented by the classes that extend this class. 95 | * 96 | * @param calendar 97 | * Used to calculate day of year. 98 | * @param geoLocation 99 | * The location information used for astronomical calculating sun times. 100 | * @param zenith 101 | * the azimuth below the vertical zenith of 90 degrees. for sunrise typically the {@link #adjustZenith 102 | * zenith} used for the calculation uses geometric zenith of 90° and {@link #adjustZenith adjusts} 103 | * this slightly to account for solar refraction and the sun's radius. Another example would be 104 | * {@link AstronomicalCalendar#getBeginNauticalTwilight()} that passes 105 | * {@link AstronomicalCalendar#NAUTICAL_ZENITH} to this method. 106 | * @param adjustForElevation 107 | * Should the time be adjusted for elevation 108 | * @return The UTC time of sunrise in 24-hour format. 5:45:00 AM will return 5.75.0. If an error was encountered in 109 | * the calculation (expected behavior for some locations such as near the poles, 110 | * {@link java.lang.Double#NaN} will be returned. 111 | * @see #getElevationAdjustment(double) 112 | */ 113 | public abstract getUTCSunrise(date: DateTime, geoLocation: GeoLocation, zenith: number, 114 | adjustForElevation: boolean): number; // eslint-disable-line @typescript-eslint/indent 115 | 116 | /** 117 | * A method that calculates UTC sunset as well as any time based on an angle above or below sunset. This abstract 118 | * method is implemented by the classes that extend this class. 119 | * 120 | * @param calendar 121 | * Used to calculate day of year. 122 | * @param geoLocation 123 | * The location information used for astronomical calculating sun times. 124 | * @param zenith 125 | * the azimuth below the vertical zenith of 90°. For sunset typically the {@link #adjustZenith 126 | * zenith} used for the calculation uses geometric zenith of 90° and {@link #adjustZenith adjusts} 127 | * this slightly to account for solar refraction and the sun's radius. Another example would be 128 | * {@link AstronomicalCalendar#getEndNauticalTwilight()} that passes 129 | * {@link AstronomicalCalendar#NAUTICAL_ZENITH} to this method. 130 | * @param adjustForElevation 131 | * Should the time be adjusted for elevation 132 | * @return The UTC time of sunset in 24-hour format. 5:45:00 AM will return 5.75.0. If an error was encountered in 133 | * the calculation (expected behavior for some locations such as near the poles, 134 | * {@link java.lang.Double#NaN} will be returned. 135 | * @see #getElevationAdjustment(double) 136 | */ 137 | public abstract getUTCSunset(date: DateTime, geoLocation: GeoLocation, zenith: number, adjustForElevation: boolean): number; 138 | 139 | /** 140 | * Return solar noon (UTC) for the given day at the 141 | * given location on earth. The {@link com.kosherjava.zmanim.util.NOAACalculator} implementation calculates 142 | * true solar noon, while the {@link com.kosherjava.zmanim.util.SunTimesCalculator} approximates it, calculating 143 | * the time as halfway between sunrise and sunset. 144 | * 145 | * @param date 146 | * Used to calculate day of year. 147 | * @param geoLocation 148 | * The location information used for astronomical calculating sun times. 149 | * 150 | * @return the time in minutes from zero UTC 151 | */ 152 | public abstract getUTCNoon(date: DateTime, geoLocation: GeoLocation): number; 153 | 154 | /** 155 | * Return solar midnight (UTC) for the given day at the 156 | * given location on earth. The the {@link com.kosherjava.zmanim.util.NOAACalculator} implementation calculates 157 | * true solar midnight, while the {@link com.kosherjava.zmanim.util.SunTimesCalculator} approximates it, calculating 158 | * the time as 12 hours after halfway between sunrise and sunset. 159 | * 160 | * @param calendar 161 | * Used to calculate day of year. 162 | * @param geoLocation 163 | * The location information used for astronomical calculating sun times. 164 | * 165 | * @return the time in minutes from zero UTC 166 | */ 167 | public abstract getUTCMidnight(date: DateTime, geoLocation: GeoLocation): number; 168 | 169 | /** 170 | * Method to return the adjustment to the zenith required to account for the elevation. Since a person at a higher 171 | * elevation can see farther below the horizon, the calculation for sunrise / sunset is calculated below the horizon 172 | * used at sea level. This is only used for sunrise and sunset and not times before or after it such as 173 | * {@link AstronomicalCalendar#getBeginNauticalTwilight() nautical twilight} since those 174 | * calculations are based on the level of available light at the given dip below the horizon, something that is not 175 | * affected by elevation, the adjustment should only be made if the zenith == 90° {@link #adjustZenith adjusted} 176 | * for refraction and solar radius. The algorithm used is 177 | * 178 | *
179 |    * elevationAdjustment = Math.toDegrees(Math.acos(earthRadiusInMeters / (earthRadiusInMeters + elevationMeters)));
180 |    * 
181 | * 182 | * The source of this algorithm is Calendrical 183 | * Calculations by Edward M. Reingold and Nachum Dershowitz. An alternate algorithm that produces similar (but 184 | * not completely accurate) result found in Ma'aglay Tzedek by Moishe Kosower and other sources is: 185 | * 186 | *
187 |    * elevationAdjustment = 0.0347 * Math.sqrt(elevationMeters);
188 |    * 
189 | * 190 | * @param elevation 191 | * elevation in Meters. 192 | * @return the adjusted zenith 193 | */ 194 | public getElevationAdjustment(elevation: number): number { 195 | const elevationAdjustment: number = MathUtils.radiansToDegrees(Math.acos(this.earthRadius / (this.earthRadius + (elevation / 1000)))); 196 | return elevationAdjustment; 197 | } 198 | 199 | /** 200 | * Adjusts the zenith of astronomical sunrise and sunset to account for solar refraction, solar radius and 201 | * elevation. The value for Sun's zenith and true rise/set Zenith (used in this class and subclasses) is the angle 202 | * that the center of the Sun makes to a line perpendicular to the Earth's surface. If the Sun were a point and the 203 | * Earth were without an atmosphere, true sunset and sunrise would correspond to a 90° zenith. Because the Sun 204 | * is not a point, and because the atmosphere refracts light, this 90° zenith does not, in fact, correspond to 205 | * true sunset or sunrise, instead the centre of the Sun's disk must lie just below the horizon for the upper edge 206 | * to be obscured. This means that a zenith of just above 90° must be used. The Sun subtends an angle of 16 207 | * minutes of arc (this can be changed via the {@link #setSolarRadius(double)} method , and atmospheric refraction 208 | * accounts for 34 minutes or so (this can be changed via the {@link #setRefraction(double)} method), giving a total 209 | * of 50 arcminutes. The total value for ZENITH is 90+(5/6) or 90.8333333° for true sunrise/sunset. Since a 210 | * person at an elevation can see below the horizon of a person at sea level, this will also adjust the zenith to 211 | * account for elevation if available. Note that this will only adjust the value if the zenith is exactly 90 degrees. 212 | * For values below and above this no correction is done. As an example, astronomical twilight is when the sun is 213 | * 18° below the horizon or {@link AstronomicalCalendar#ASTRONOMICAL_ZENITH 108° 214 | * below the zenith}. This is traditionally calculated with none of the above mentioned adjustments. The same goes 215 | * for various tzais and alos times such as the 216 | * {@link ZmanimCalendar#ZENITH_16_POINT_1 16.1°} dip used in 217 | * {@link ComplexZmanimCalendar#getAlos16Point1Degrees()}. 218 | * 219 | * @param zenith 220 | * the azimuth below the vertical zenith of 90°. For sunset typically the {@link #adjustZenith 221 | * zenith} used for the calculation uses geometric zenith of 90° and {@link #adjustZenith adjusts} 222 | * this slightly to account for solar refraction and the sun's radius. Another example would be 223 | * {@link AstronomicalCalendar#getEndNauticalTwilight()} that passes 224 | * {@link AstronomicalCalendar#NAUTICAL_ZENITH} to this method. 225 | * @param elevation 226 | * elevation in Meters. 227 | * @return The zenith adjusted to include the {@link #getSolarRadius sun's radius}, {@link #getRefraction 228 | * refraction} and {@link #getElevationAdjustment elevation} adjustment. This will only be adjusted for 229 | * sunrise and sunset (if the zenith == 90°) 230 | * @see #getElevationAdjustment(double) 231 | */ 232 | public adjustZenith(zenith: number, elevation: number) { 233 | let adjustedZenith: number = zenith; 234 | if (zenith === AstronomicalCalculator.GEOMETRIC_ZENITH) { // only adjust if it is exactly sunrise or sunset 235 | adjustedZenith = zenith + (this.getSolarRadius() + this.getRefraction() + this.getElevationAdjustment(elevation)); 236 | } 237 | return adjustedZenith; 238 | } 239 | 240 | /** 241 | * Method to get the refraction value to be used when calculating sunrise and sunset. The default value is 34 242 | * arcminutes. The Errata and Notes 243 | * for Calendrical Calculations: The Millennium Edition by Edward M. Reingold and Nachum Dershowitz lists the 244 | * actual average refraction value as 34.478885263888294 or approximately 34' 29". The refraction value as well 245 | * as the solarRadius and elevation adjustment are added to the zenith used to calculate sunrise and sunset. 246 | * 247 | * @return The refraction in arcminutes. 248 | */ 249 | public getRefraction(): number { 250 | return this.refraction; 251 | } 252 | 253 | /** 254 | * A method to allow overriding the default refraction of the calculator. 255 | * @todo At some point in the future, an AtmosphericModel or Refraction object that models the atmosphere of different 256 | * locations might be used for increased accuracy. 257 | * 258 | * @param refraction 259 | * The refraction in arcminutes. 260 | * @see #getRefraction() 261 | */ 262 | public setRefraction(refraction: number): void { 263 | this.refraction = refraction; 264 | } 265 | 266 | /** 267 | * Method to get the sun's radius. The default value is 16 arcminutes. The sun's radius as it appears from earth is 268 | * almost universally given as 16 arcminutes but in fact it differs by the time of the year. At the perihelion it has an apparent radius of 16.293, while at the 270 | * aphelion it has an apparent radius of 15.755. There is little 271 | * affect for most location, but at high and low latitudes the difference becomes more apparent. My Calculations for 272 | * the difference at the location of the Royal Observatory, Greenwich 273 | * shows only a 4.494-second difference between the perihelion and aphelion radii, but moving into the arctic circle the 274 | * difference becomes more noticeable. Tests for Tromso, Norway (latitude 69.672312, longitude 19.049787) show that 275 | * on May 17, the rise of the midnight sun, a 2 minute 23 second difference is observed between the perihelion and 276 | * aphelion radii using the USNO algorithm, but only 1 minute and 6 seconds difference using the NOAA algorithm. 277 | * Areas farther north show an even greater difference. Note that these test are not real valid test cases because 278 | * they show the extreme difference on days that are not the perihelion or aphelion, but are shown for illustrative 279 | * purposes only. 280 | * 281 | * @return The sun's radius in arcminutes. 282 | */ 283 | public getSolarRadius(): number { 284 | return this.solarRadius; 285 | } 286 | 287 | /** 288 | * Method to set the sun's radius. 289 | * 290 | * @param solarRadius 291 | * The sun's radius in arcminutes. 292 | * @see #getSolarRadius() 293 | */ 294 | public setSolarRadius(solarRadius: number): void { 295 | this.solarRadius = solarRadius; 296 | } 297 | 298 | /** 299 | * @see java.lang.Object#clone() 300 | * @since 1.1 301 | */ 302 | public clone(): AstronomicalCalculator { 303 | return JSON.parse(JSON.stringify(this)); 304 | } 305 | 306 | public equals(object: object): boolean { 307 | return this === object; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/util/ZmanimFormatter.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { TimeZone, Utils, padZeros } from '../polyfills/Utils'; 4 | import { Time } from './Time'; 5 | import { AstronomicalCalendar } from '../AstronomicalCalendar'; 6 | import { ZmanimCalendar } from '../ZmanimCalendar'; 7 | import { ComplexZmanimCalendar } from '../ComplexZmanimCalendar'; 8 | import { Zman, ZmanWithDuration, ZmanWithZmanDate } from './Zman'; 9 | import { UnsupportedError } from '../polyfills/errors'; 10 | 11 | const methodBlacklist: string[] = [ 12 | 'getAdjustedDate', 13 | 'getDate', 14 | 'getElevationAdjustedSunrise', 15 | 'getElevationAdjustedSunset', 16 | 'getMidnightLastNight', 17 | 'getMidnightTonight', 18 | 'getSunriseBaalHatanya', 19 | 'getSunsetBaalHatanya', 20 | ]; 21 | 22 | const methodWhitelist: string[] = [ 23 | // These methods have parameters, but have default values. 24 | 'getMinchaGedola', 25 | 'getMinchaKetana', 26 | 'getPlagHamincha', 27 | 'getSofZmanKidushLevana15Days', 28 | 'getSofZmanKidushLevanaBetweenMoldos', 29 | 'getSunTransit', 30 | 'getTchilasZmanKidushLevana3Days', 31 | 'getTchilasZmanKidushLevana7Days', 32 | 'getTemporalHour', 33 | ]; 34 | 35 | /** 36 | * A class used to format both non {@link java.util.Date} times generated by the Zmanim package as well as Dates. For 37 | * example the {@link AstronomicalCalendar#getTemporalHour()} returns the length of the hour in 38 | * milliseconds. This class can format this time. 39 | * 40 | * @author © Eliyahu Hershfeld 2004 - 2019 41 | * @version 1.2 42 | */ 43 | export class ZmanimFormatter { 44 | /** 45 | * Setting to prepend a zero to single digit hours. 46 | * @see #setSettings(boolean, boolean, boolean) 47 | */ 48 | private prependZeroHours: boolean = false; 49 | 50 | /** 51 | * Should seconds be used in formatting time. 52 | * @see #setSettings(boolean, boolean, boolean) 53 | */ 54 | private useSeconds: boolean = false; 55 | 56 | /** 57 | * @see #setSettings(boolean, boolean, boolean) 58 | */ 59 | private useMillis: boolean = false; 60 | 61 | /** 62 | * the formatter for minutes as seconds. 63 | */ 64 | private static readonly minuteSecondNF: number = 2; 65 | 66 | /** 67 | * the formatter for hours. 68 | */ 69 | private hourNF!: number; 70 | 71 | /** 72 | * the formatter for minutes as milliseconds. 73 | */ 74 | private static readonly milliNF: number = 3; 75 | 76 | /** 77 | * @see #setDateFormat(SimpleDateFormat) 78 | */ 79 | private dateFormat!: string; 80 | 81 | /** 82 | * Method to return the TimeZone. 83 | * @see #setTimeZone(TimeZone) 84 | */ 85 | private timeZoneId!: string; // TimeZone.getTimeZone("UTC"); 86 | 87 | /** 88 | * @return the timeZone 89 | */ 90 | public getTimeZone(): string { 91 | return this.timeZoneId; 92 | } 93 | 94 | /** 95 | * Method to set the TimeZone. 96 | * @param timeZoneId 97 | * the timeZone to set 98 | */ 99 | public setTimeZone(timeZoneId: string): void { 100 | this.timeZoneId = timeZoneId; 101 | } 102 | 103 | /** 104 | * Format using hours, minutes, seconds and milliseconds using the xsd:time format. This format will return 105 | * 00.00.00.0 when formatting 0. 106 | */ 107 | public static readonly SEXAGESIMAL_XSD_FORMAT: number = 0; 108 | 109 | /** 110 | * Defaults to {@link #SEXAGESIMAL_XSD_FORMAT}. 111 | * @see #setTimeFormat(int) 112 | */ 113 | private timeFormat: number = ZmanimFormatter.SEXAGESIMAL_XSD_FORMAT; 114 | 115 | /** 116 | * Format using standard decimal format with 5 positions after the decimal. 117 | */ 118 | public static readonly DECIMAL_FORMAT: number = 1; 119 | 120 | /** Format using hours and minutes. */ 121 | public static readonly SEXAGESIMAL_FORMAT: number = 2; 122 | 123 | /** Format using hours, minutes and seconds. */ 124 | public static readonly SEXAGESIMAL_SECONDS_FORMAT: number = 3; 125 | 126 | /** Format using hours, minutes, seconds and milliseconds. */ 127 | public static readonly SEXAGESIMAL_MILLIS_FORMAT: number = 4; 128 | 129 | /** constant for milliseconds in a minute (60,000) */ 130 | public static readonly MINUTE_MILLIS: number = 60 * 1000; 131 | 132 | /** constant for milliseconds in an hour (3,600,000) */ 133 | public static readonly HOUR_MILLIS: number = ZmanimFormatter.MINUTE_MILLIS * 60; 134 | 135 | /** 136 | * Format using the XSD Duration format. This is in the format of PT1H6M7.869S (P for period (duration), T for time, 137 | * H, M and S indicate hours, minutes and seconds. 138 | */ 139 | public static readonly XSD_DURATION_FORMAT: number = 5; 140 | 141 | public static readonly XSD_DATE_FORMAT = 'yyyy-LL-dd\'T\'HH:mm:ss'; 142 | 143 | /** 144 | * Constructor that defaults to this will use the format "h:mm:ss" for dates and 00.00.00.0 for {@link Time}. 145 | * @param timeZone the TimeZone Object 146 | */ 147 | /* 148 | public ZmanimFormatter(timeZone: TimeZone) { 149 | this(0, new SimpleDateFormat("h:mm:ss"), timeZone); 150 | } 151 | */ 152 | 153 | // public ZmanimFormatter() { 154 | // this(0, new SimpleDateFormat("h:mm:ss"), TimeZone.getTimeZone("UTC")); 155 | // } 156 | 157 | /** 158 | * ZmanimFormatter constructor using a formatter 159 | * 160 | * @param format 161 | * int The formatting style to use. Using ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the 162 | * time of 90*60*1000 + 1 as 1:30:00 163 | * @param dateFormat the SimpleDateFormat Object 164 | * @param timeZone the TimeZone Object 165 | */ 166 | constructor(timeZoneId: string) 167 | constructor(format: number, dateFormat: string, timeZoneId: string) 168 | constructor(formatOrTimeZone: number | string, dateFormat?: string, timeZoneId?: string) { 169 | let format: number; 170 | if (dateFormat) { 171 | format = formatOrTimeZone as number; 172 | } else { 173 | format = 0; 174 | dateFormat = 'h:mm:ss'; 175 | timeZoneId = formatOrTimeZone as string; 176 | } 177 | 178 | this.setTimeZone(timeZoneId!); 179 | 180 | if (this.prependZeroHours) { 181 | this.hourNF = 2; 182 | } 183 | 184 | this.setTimeFormat(format); 185 | this.setDateFormat(dateFormat); 186 | } 187 | 188 | /** 189 | * Sets the format to use for formatting. 190 | * 191 | * @param format 192 | * int the format constant to use. 193 | */ 194 | public setTimeFormat(format: number): void { 195 | this.timeFormat = format; 196 | switch (format) { 197 | case ZmanimFormatter.SEXAGESIMAL_XSD_FORMAT: 198 | this.setSettings(true, true, true); 199 | break; 200 | case ZmanimFormatter.SEXAGESIMAL_FORMAT: 201 | this.setSettings(false, false, false); 202 | break; 203 | case ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT: 204 | this.setSettings(false, true, false); 205 | break; 206 | case ZmanimFormatter.SEXAGESIMAL_MILLIS_FORMAT: 207 | this.setSettings(false, true, true); 208 | break; 209 | // case DECIMAL_FORMAT: 210 | // default: 211 | } 212 | } 213 | 214 | /** 215 | * Sets the SimpleDateFormat Object 216 | * @param dateFormat the SimpleDateFormat Object to set 217 | */ 218 | public setDateFormat(dateFormat: string): void { 219 | this.dateFormat = dateFormat; 220 | } 221 | 222 | /** 223 | * returns the SimpleDateFormat Object 224 | * @return the SimpleDateFormat Object 225 | */ 226 | public getDateFormat(): string { 227 | return this.dateFormat; 228 | } 229 | 230 | /** 231 | * Sets various format settings. 232 | * @param prependZeroHours if to prepend a zero for single digit hours (so that 1 o'clock is displayed as 01) 233 | * @param useSeconds should seconds be used in the time format 234 | * @param useMillis should milliseconds be used in formatting time. 235 | */ 236 | private setSettings(prependZeroHours: boolean, useSeconds: boolean, useMillis: boolean): void { 237 | this.prependZeroHours = prependZeroHours; 238 | this.useSeconds = useSeconds; 239 | this.useMillis = useMillis; 240 | } 241 | 242 | /** 243 | * A method that formats milliseconds into a time format. 244 | * 245 | * @param milliseconds 246 | * The time in milliseconds. 247 | * @return String The formatted String 248 | */ 249 | /* 250 | public format(milliseconds: number): string { 251 | return this.format(milliseconds); 252 | } 253 | */ 254 | 255 | /** 256 | * A method that formats milliseconds into a time format. 257 | * 258 | * @param millis 259 | * The time in milliseconds. 260 | * @return String The formatted String 261 | */ 262 | 263 | /* 264 | public format(millis: number): string { 265 | return format(new Time(millis)); 266 | } 267 | */ 268 | 269 | /** 270 | * A method that formats {@link Time} objects. 271 | * 272 | * @param time 273 | * The time Object to be formatted. 274 | * @return String The formatted String 275 | */ 276 | public format(timeOrMillis: Time | number): string { 277 | let time: Time; 278 | if (timeOrMillis instanceof Time) { 279 | time = timeOrMillis as Time; 280 | } else { 281 | time = new Time(timeOrMillis as number); 282 | } 283 | 284 | if (this.timeFormat === ZmanimFormatter.XSD_DURATION_FORMAT) { 285 | return ZmanimFormatter.formatXSDDurationTime(time); 286 | } 287 | let sb: string = padZeros(time.getHours(), this.hourNF) 288 | .concat(':') 289 | .concat(padZeros(time.getMinutes(), ZmanimFormatter.minuteSecondNF)); 290 | if (this.useSeconds) { 291 | sb = sb.concat(':') 292 | .concat(padZeros(time.getSeconds(), ZmanimFormatter.minuteSecondNF)); 293 | } 294 | if (this.useMillis) { 295 | sb = sb.concat('.') 296 | .concat(padZeros(time.getMilliseconds(), ZmanimFormatter.milliNF)); 297 | } 298 | return sb; 299 | } 300 | 301 | /** 302 | * Formats a date using this class's {@link #getDateFormat() date format}. 303 | * 304 | * @param dateTime - the date to format 305 | * @return the formatted String 306 | */ 307 | public formatDateTime(dateTime: DateTime): string { 308 | // eslint-disable-next-line @typescript-eslint/naming-convention 309 | const _dateTime = dateTime.setZone(this.getTimeZone()); 310 | 311 | if (this.dateFormat === ZmanimFormatter.XSD_DATE_FORMAT) { 312 | return this.getXSDateTime(_dateTime); 313 | } 314 | return _dateTime.toFormat(this.dateFormat); 315 | } 316 | 317 | /** 318 | * The date:date-time function returns the current date and time as a date/time string. The date/time string that's 319 | * returned must be a string in the format defined as the lexical representation of xs:dateTime in [3.3.8 dateTime] of [XML Schema 1.1 Part 2: Datatypes]. The date/time format is 322 | * basically CCYY-MM-DDThh:mm:ss, although implementers should consult [XML Schema 1.1 Part 2: Datatypes] and [ISO 8601] for details. The date/time string format must include a 325 | * time zone, either a Z to indicate Coordinated Universal Time or a + or - followed by the difference between the 326 | * difference from UTC represented as hh:mm. 327 | * @param dateTime - the UTC Date Object 328 | * @return the XSD dateTime 329 | */ 330 | public getXSDateTime(dateTime: DateTime): string { 331 | return dateTime.setZone(this.getTimeZone()) 332 | .toFormat(ZmanimFormatter.XSD_DATE_FORMAT.concat('ZZ')); 333 | } 334 | 335 | /** 336 | * This returns the xml representation of an xsd:duration object. 337 | * 338 | * @param millis 339 | * the duration in milliseconds 340 | * @return the xsd:duration formatted String 341 | */ 342 | 343 | /* 344 | public formatXSDDurationTime(millis: number): string { 345 | return formatXSDDurationTime(new Time(millis)); 346 | } 347 | */ 348 | 349 | /** 350 | * This returns the xml representation of an xsd:duration object. 351 | * 352 | * @param time 353 | * the duration as a Time object 354 | * @return the xsd:duration formatted String 355 | */ 356 | public static formatXSDDurationTime(timeOrMillis: Time | number): string { 357 | let time: Time; 358 | if (timeOrMillis instanceof Time) { 359 | time = timeOrMillis as Time; 360 | } else { 361 | time = new Time(timeOrMillis as number); 362 | } 363 | 364 | let duration: string; 365 | if (time.getHours() !== 0 || time.getMinutes() !== 0 || time.getSeconds() !== 0 || time.getMilliseconds() !== 0) { 366 | duration = ('P').concat('T'); 367 | 368 | if (time.getHours() !== 0) duration = duration.concat(`${time.getHours()}H`); 369 | 370 | if (time.getMinutes() !== 0) duration = duration.concat(`${time.getMinutes()}M`); 371 | 372 | if (time.getSeconds() !== 0 || time.getMilliseconds() !== 0) { 373 | duration = duration.concat(`${time.getSeconds()}.${padZeros(time.getMilliseconds(), ZmanimFormatter.milliNF)}`); 374 | duration = duration.concat('S'); 375 | } 376 | 377 | if (duration.length === 1) duration.concat('T0S'); // zero seconds 378 | 379 | if (time.isNegative()) { 380 | duration = duration.substr(0, 0).concat('-').concat(duration.substr(0, duration.length)); 381 | } 382 | } 383 | return duration!.toString(); 384 | } 385 | 386 | public static formatDecimal(num: number): string { 387 | const hasDecimal = num - Math.trunc(num) > 0; 388 | return hasDecimal ? num.toString() : num.toFixed(1); 389 | } 390 | 391 | /** 392 | * A method that returns an XML formatted String representing the serialized Object. The 393 | * format used is: 394 | * 395 | *
396 |    *  <AstronomicalTimes date="1969-02-08" type="AstronomicalCalendar algorithm="US Naval Almanac Algorithm" location="Lakewood, NJ" latitude="40.095965" longitude="-74.22213" elevation="31.0" timeZoneName="Eastern Standard Time" timeZoneID="America/New_York" timeZoneOffset="-5">
397 |    *     <Sunrise>2007-02-18T06:45:27-05:00</Sunrise>
398 |    *     <TemporalHour>PT54M17.529S</TemporalHour>
399 |    *     ...
400 |    *   </AstronomicalTimes>
401 |    * 
402 | * 403 | * Note that the output uses the xsd:dateTime format for 404 | * times such as sunrise, and xsd:duration format for 405 | * times that are a duration such as the length of a 406 | * {@link AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is 407 | * returned by the {@link #toString() toString}. 408 | * 409 | * @param astronomicalCalendar the AstronomicalCalendar Object 410 | * 411 | * @return The XML formatted String. The format will be: 412 | * 413 | *
414 |    *  <AstronomicalTimes date="1969-02-08" type="AstronomicalCalendar algorithm="US Naval Almanac Algorithm" location="Lakewood, NJ" latitude="40.095965" longitude="-74.22213" elevation="31.0" timeZoneName="Eastern Standard Time" timeZoneID="America/New_York" timeZoneOffset="-5">
415 |    *     <Sunrise>2007-02-18T06:45:27-05:00</Sunrise>
416 |    *     <TemporalHour>PT54M17.529S</TemporalHour>
417 |    *     ...
418 |    *  </AstronomicalTimes>
419 |    * 
420 | * 421 | * TODO: add proper schema, and support for nulls. XSD duration (for solar hours), should probably return 422 | * nil and not P 423 | * @deprecated 424 | */ 425 | public static toXML(): void { 426 | throw new UnsupportedError('This method is not supported.'); 427 | } 428 | 429 | /** 430 | * A method that returns a JSON formatted String representing the serialized Object. The 431 | * format used is: 432 | *
433 |    * {
434 |    *    "metadata":{
435 |    *      "date":"1969-02-08",
436 |    *      "type":"AstronomicalCalendar",
437 |    *      "algorithm":"US Naval Almanac Algorithm",
438 |    *      "location":"Lakewood, NJ",
439 |    *      "latitude":"40.095965",
440 |    *      "longitude":"-74.22213",
441 |    *      "elevation:"31.0",
442 |    *      "timeZoneName":"Eastern Standard Time",
443 |    *      "timeZoneID":"America/New_York",
444 |    *      "timeZoneOffset":"-5"},
445 |    *    "AstronomicalTimes":{
446 |    *     "Sunrise":"2007-02-18T06:45:27-05:00",
447 |    *     "TemporalHour":"PT54M17.529S"
448 |    *     ...
449 |    *     }
450 |    * }
451 |    * 
452 | * 453 | * Note that the output uses the xsd:dateTime format for 454 | * times such as sunrise, and xsd:duration format for 455 | * times that are a duration such as the length of a 456 | * {@link AstronomicalCalendar#getTemporalHour() temporal hour}. 457 | * 458 | * @param astronomicalCalendar the AstronomicalCalendar Object 459 | * 460 | * @return The JSON formatted String. The format will be: 461 | *
462 |    * {
463 |    *    "metadata":{
464 |    *      "date":"1969-02-08",
465 |    *      "type":"AstronomicalCalendar",
466 |    *      "algorithm":"US Naval Almanac Algorithm",
467 |    *      "location":"Lakewood, NJ",
468 |    *      "latitude":"40.095965",
469 |    *      "longitude":"-74.22213",
470 |    *      "elevation:"31.0",
471 |    *      "timeZoneName":"Eastern Standard Time",
472 |    *      "timeZoneID":"America/New_York",
473 |    *      "timeZoneOffset":"-5"},
474 |    *    "AstronomicalTimes":{
475 |    *     "Sunrise":"2007-02-18T06:45:27-05:00",
476 |    *     "TemporalHour":"PT54M17.529S"
477 |    *     ...
478 |    *     }
479 |    * }
480 |    * 
481 | */ 482 | public static toJSON(astronomicalCalendar: AstronomicalCalendar): JsonOutput { 483 | const json: JsonOutput = { 484 | metadata: ZmanimFormatter.getOutputMetadata(astronomicalCalendar), 485 | }; 486 | const key: string = ZmanimFormatter.getOutputKey(astronomicalCalendar); 487 | json[key] = ZmanimFormatter.getZmanimOutput(astronomicalCalendar); 488 | 489 | return json; 490 | } 491 | 492 | // @ts-ignore 493 | private static getOutputKey(astronomicalCalendar: AstronomicalCalendar): string { 494 | switch (true) { 495 | case astronomicalCalendar instanceof ComplexZmanimCalendar: 496 | return 'Zmanim'; 497 | case astronomicalCalendar instanceof ZmanimCalendar: 498 | return 'BasicZmanim'; 499 | case astronomicalCalendar instanceof AstronomicalCalendar: 500 | return 'AstronomicalTimes'; 501 | } 502 | } 503 | 504 | private static getOutputMetadata(astronomicalCalendar: AstronomicalCalendar): OutputMetadata { 505 | const df: string = 'yyyy-MM-dd'; 506 | 507 | return { 508 | date: astronomicalCalendar.getDate().toFormat(df), 509 | type: astronomicalCalendar.getClassName(), 510 | algorithm: astronomicalCalendar.getAstronomicalCalculator().getCalculatorName(), 511 | location: astronomicalCalendar.getGeoLocation().getLocationName(), 512 | latitude: astronomicalCalendar.getGeoLocation().getLatitude().toString(), 513 | longitude: astronomicalCalendar.getGeoLocation().getLongitude().toString(), 514 | elevation: ZmanimFormatter.formatDecimal(astronomicalCalendar.getGeoLocation().getElevation()), 515 | timeZoneName: TimeZone.getDisplayName(astronomicalCalendar.getGeoLocation().getTimeZone(), astronomicalCalendar.getDate()), 516 | timeZoneID: astronomicalCalendar.getGeoLocation().getTimeZone(), 517 | timeZoneOffset: ZmanimFormatter.formatDecimal(TimeZone.getOffset(astronomicalCalendar.getGeoLocation().getTimeZone(), 518 | astronomicalCalendar.getDate().valueOf()) / ZmanimFormatter.HOUR_MILLIS), 519 | }; 520 | } 521 | 522 | private static getZmanimOutput(astronomicalCalendar: AstronomicalCalendar): Record { 523 | const formatter: ZmanimFormatter = new ZmanimFormatter(ZmanimFormatter.XSD_DURATION_FORMAT, ZmanimFormatter.XSD_DATE_FORMAT, 524 | astronomicalCalendar.getGeoLocation().getTimeZone()); 525 | 526 | const dateList: ZmanWithZmanDate[] = []; 527 | let durationList: ZmanWithDuration[] = []; 528 | const otherList: string[] = []; 529 | 530 | // Get al the methods in the calendar 531 | Utils.getAllMethodNames(astronomicalCalendar, true) 532 | // Filter out methods that we don't want 533 | .filter(method => includeMethod(method, astronomicalCalendar)) 534 | // Call each method and get the return values 535 | .map(method => ({ 536 | methodName: method, 537 | value: (astronomicalCalendar as any as Record)[method].call(astronomicalCalendar), 538 | })) 539 | // Filter for return values of type Date or number 540 | .filter(methodObj => DateTime.isDateTime(methodObj.value) || typeof methodObj.value === 'number' || methodObj.value === null) 541 | // Separate the Dates and numbers 542 | .forEach(methodObj => { 543 | const tagName: string = methodObj.methodName.substring(3); 544 | if (DateTime.isDateTime(methodObj.value)) { 545 | dateList.push(new Zman(methodObj.value as DateTime, tagName) as ZmanWithZmanDate); 546 | } else if (typeof methodObj.value === 'number') { 547 | durationList.push(new Zman(methodObj.value, tagName) as ZmanWithDuration); 548 | } else { 549 | otherList.push(tagName); 550 | } 551 | }); 552 | 553 | dateList.sort(Zman.compareDateOrder); 554 | // Filter for values in milliseconds, and not values in minutes 555 | durationList = durationList.filter((zman: ZmanWithDuration) => zman.duration > 1000) 556 | .sort(Zman.compareDurationOrder); 557 | 558 | const timesData: Record = {}; 559 | dateList.forEach((zman: ZmanWithZmanDate) => { 560 | timesData[zman.label as string] = formatter.formatDateTime(zman.zman); 561 | }); 562 | durationList.forEach((zman: ZmanWithDuration) => { 563 | timesData[zman.label as string] = formatter.format(Math.trunc(zman.duration)); 564 | }); 565 | otherList.forEach((tagName: string) => { 566 | timesData[tagName] = 'N/A'; 567 | }); 568 | 569 | return timesData; 570 | } 571 | } 572 | 573 | /** 574 | * Determines if a method should be output by the {@link #toJSON(AstronomicalCalendar)} 575 | * 576 | * @param {string} method - the method in question 577 | * @param {AstronomicalCalendar} astronomicalCalendar - The astronomical calendar, to be able to 578 | * check the parameterlist 579 | * @return if the method should be included in serialization 580 | */ 581 | function includeMethod(method: string, astronomicalCalendar: AstronomicalCalendar): boolean { 582 | if (methodWhitelist.includes(method)) return true; 583 | 584 | // Filter out excluded methods 585 | return !methodBlacklist.includes(method) 586 | // Filter out methods with parameters since we don't know what value(s) to pass 587 | && (astronomicalCalendar as any as Record)[method].length === 0 588 | // Filter out methods that don't start with 'get' 589 | && method.startsWith('get'); 590 | } 591 | 592 | export interface JsonOutput { 593 | metadata: OutputMetadata 594 | 595 | [key: string]: object; 596 | } 597 | 598 | export interface OutputMetadata { 599 | date: string; 600 | type: string; 601 | algorithm: string; 602 | location: string | null; 603 | latitude: string; 604 | longitude: string; 605 | elevation: string; 606 | timeZoneName: string; 607 | timeZoneID: string; 608 | timeZoneOffset: string; 609 | } 610 | -------------------------------------------------------------------------------- /src/util/NOAACalculator.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { GeoLocation } from './GeoLocation'; 4 | import { AstronomicalCalculator } from './AstronomicalCalculator'; 5 | import { MathUtils, ValueOf } from '../polyfills/Utils'; 6 | 7 | /** 8 | * Implementation of sunrise and sunset methods to calculate astronomical times based on the NOAA algorithm. This calculator uses the Java algorithm based on the implementation by NOAA - National Oceanic and Atmospheric Administration's Surface Radiation Research Branch. NOAA's implementation is based on equations from Astronomical Algorithms by Jean Meeus. Added to the algorithm is an adjustment of the zenith 15 | * to account for elevation. The algorithm can be found in the Wikipedia Sunrise Equation article. 17 | * 18 | * @author © Eliyahu Hershfeld 2011 - 2019 19 | */ 20 | export class NOAACalculator extends AstronomicalCalculator { 21 | /** 22 | * The Julian day of January 1, 2000, known as 23 | * J2000.0. 24 | */ 25 | private static readonly JULIAN_DAY_JAN_1_2000: number = 2451545; 26 | 27 | /** 28 | * Julian days per century 29 | */ 30 | private static readonly JULIAN_DAYS_PER_CENTURY: number = 36525; 31 | 32 | /** 33 | * An enum to indicate what type of solar event ({@link #SUNRISE SUNRISE}, {@link #SUNSET SUNSET}, 34 | * {@link #NOON NOON} or {@link #MIDNIGHT MIDNIGHT}) is being calculated. 35 | */ 36 | protected static readonly SolarEvent = { 37 | /** SUNRISE A solar event related to sunrise */ 38 | SUNRISE: 0, 39 | /** SUNSET A solar event related to sunset */ 40 | SUNSET: 1, 41 | /** NOON A solar event related to noon */ 42 | NOON: 2, 43 | /** MIDNIGHT A solar event related to midnight */ 44 | MIDNIGHT: 3, 45 | } as const; 46 | 47 | /** 48 | * Default constructor of the NOAACalculator. 49 | */ 50 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 51 | constructor() { 52 | super(); 53 | } 54 | 55 | /** 56 | * @see AstronomicalCalculator#getCalculatorName() 57 | */ 58 | // eslint-disable-next-line class-methods-use-this 59 | public getCalculatorName(): string { 60 | return 'US National Oceanic and Atmospheric Administration Algorithm'; // Implementation of the Jean Meeus algorithm 61 | } 62 | 63 | /** 64 | * @see AstronomicalCalculator#getUTCSunrise(Calendar, GeoLocation, double, boolean) 65 | */ 66 | public getUTCSunrise(date: DateTime, geoLocation: GeoLocation, zenith: number, adjustForElevation: boolean): number { 67 | const elevation: number = adjustForElevation ? geoLocation.getElevation() : 0; 68 | const adjustedZenith: number = this.adjustZenith(zenith, elevation); 69 | 70 | let sunrise: number = NOAACalculator.getSunRiseSetUTC(date, geoLocation.getLatitude(), -geoLocation.getLongitude(), 71 | adjustedZenith, NOAACalculator.SolarEvent.SUNRISE); 72 | sunrise = sunrise / 60; 73 | 74 | return sunrise > 0 ? sunrise % 24 : (sunrise % 24) + 24; // ensure that the time is >= 0 and < 24 75 | } 76 | 77 | /** 78 | * @see AstronomicalCalculator#getUTCSunset(Calendar, GeoLocation, double, boolean) 79 | */ 80 | public getUTCSunset(date: DateTime, geoLocation: GeoLocation, zenith: number, adjustForElevation: boolean): number { 81 | const elevation: number = adjustForElevation ? geoLocation.getElevation() : 0; 82 | const adjustedZenith: number = this.adjustZenith(zenith, elevation); 83 | 84 | let sunset: number = NOAACalculator.getSunRiseSetUTC(date, geoLocation.getLatitude(), -geoLocation.getLongitude(), 85 | adjustedZenith, NOAACalculator.SolarEvent.SUNSET); 86 | sunset = sunset / 60; 87 | 88 | return sunset > 0 ? sunset % 24 : (sunset % 24) + 24; // ensure that the time is >= 0 and < 24 89 | } 90 | 91 | /** 92 | * Return the Julian day from a Java Calendar. 93 | * 94 | * @param calendar 95 | * The Java Calendar 96 | * @return the Julian day corresponding to the date Note: Number is returned for start of day. Fractional days 97 | * should be added later. 98 | */ 99 | private static getJulianDay(date: DateTime): number { 100 | let { year, month } = date; 101 | const { day } = date; 102 | if (month <= 2) { 103 | year -= 1; 104 | month += 12; 105 | } 106 | const a: number = Math.trunc(year / 100); 107 | const b: number = Math.trunc(2 - a + a / 4); 108 | 109 | return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + b - 1524.5; 110 | } 111 | 112 | /** 113 | * Convert Julian day to centuries since J2000.0. 115 | * 116 | * @param julianDay 117 | * the Julian Day to convert 118 | * @return the centuries since 2000 Julian corresponding to the Julian Day 119 | */ 120 | private static getJulianCenturiesFromJulianDay(julianDay: number): number { 121 | return (julianDay - NOAACalculator.JULIAN_DAY_JAN_1_2000) / NOAACalculator.JULIAN_DAYS_PER_CENTURY; 122 | } 123 | 124 | /** 125 | * Returns the Geometric Mean Longitude of the Sun. 126 | * 127 | * @param julianCenturies 128 | * the number of Julian centuries since J2000.0. 130 | * @return the Geometric Mean Longitude of the Sun in degrees 131 | */ 132 | private static getSunGeometricMeanLongitude(julianCenturies: number): number { 133 | const longitude: number = 280.46646 + julianCenturies * (36000.76983 + 0.0003032 * julianCenturies); 134 | return longitude > 0 ? longitude % 360 : (longitude % 360) + 360; // ensure that the longitude is >= 0 and < 360 135 | } 136 | 137 | /** 138 | * Returns the Geometric Mean Anomaly of the Sun. 139 | * 140 | * @param julianCenturies 141 | * the number of Julian centuries since J2000.0. 143 | * @return the Geometric Mean Anomaly of the Sun in degrees 144 | */ 145 | private static getSunGeometricMeanAnomaly(julianCenturies: number): number { 146 | return 357.52911 + julianCenturies * (35999.05029 - 0.0001537 * julianCenturies); 147 | } 148 | 149 | /** 150 | * Return the unitless eccentricity of earth's orbit. 151 | * 152 | * @param julianCenturies 153 | * the number of Julian centuries since J2000.0. 155 | * @return the unitless eccentricity 156 | */ 157 | private static getEarthOrbitEccentricity(julianCenturies: number): number { 158 | return 0.016708634 - julianCenturies * (0.000042037 + 0.0000001267 * julianCenturies); 159 | } 160 | 161 | /** 162 | * Returns the equation of center for the sun in degrees. 163 | * 164 | * @param julianCenturies 165 | * the number of Julian centuries since J2000.0. 167 | * @return the equation of center for the sun in degrees 168 | */ 169 | private static getSunEquationOfCenter(julianCenturies: number): number { 170 | const m: number = NOAACalculator.getSunGeometricMeanAnomaly(julianCenturies); 171 | 172 | const mrad: number = MathUtils.degreesToRadians(m); 173 | const sinm: number = Math.sin(mrad); 174 | const sin2m: number = Math.sin(mrad + mrad); 175 | const sin3m: number = Math.sin(mrad + mrad + mrad); 176 | 177 | return sinm * (1.914602 - julianCenturies * (0.004817 + 0.000014 * julianCenturies)) + sin2m 178 | * (0.019993 - 0.000101 * julianCenturies) + sin3m * 0.000289; 179 | } 180 | 181 | /** 182 | * Return the true longitude of the sun. 183 | * 184 | * @param julianCenturies 185 | * the number of Julian centuries since J2000.0. 187 | * @return the sun's true longitude in degrees 188 | */ 189 | private static getSunTrueLongitude(julianCenturies: number): number { 190 | const sunLongitude: number = NOAACalculator.getSunGeometricMeanLongitude(julianCenturies); 191 | const center: number = NOAACalculator.getSunEquationOfCenter(julianCenturies); 192 | 193 | return sunLongitude + center; 194 | } 195 | 196 | // /** 197 | // * Returns the true anamoly of the sun. 198 | // * 199 | // * @param julianCenturies 200 | // * the number of Julian centuries since J2000.0 201 | // * @return the sun's true anamoly in degrees 202 | // */ 203 | // private static double getSunTrueAnomaly(double julianCenturies) { 204 | // double meanAnomaly = getSunGeometricMeanAnomaly(julianCenturies); 205 | // double equationOfCenter = getSunEquationOfCenter(julianCenturies); 206 | // 207 | // return meanAnomaly + equationOfCenter; 208 | // } 209 | 210 | /** 211 | * Return the apparent longitude of the sun. 212 | * 213 | * @param julianCenturies 214 | * the number of Julian centuries since J2000.0. 216 | * @return sun's apparent longitude in degrees 217 | */ 218 | private static getSunApparentLongitude(julianCenturies: number): number { 219 | const sunTrueLongitude: number = NOAACalculator.getSunTrueLongitude(julianCenturies); 220 | 221 | const omega: number = 125.04 - 1934.136 * julianCenturies; 222 | const lambda = sunTrueLongitude - 0.00569 - 0.00478 * Math.sin(MathUtils.degreesToRadians(omega)); 223 | return lambda; 224 | } 225 | 226 | /** 227 | * Returns the mean obliquity of the ecliptic (Axial tilt). 228 | * 229 | * @param julianCenturies 230 | * the number of Julian centuries since J2000.0. 232 | * @return the mean obliquity in degrees 233 | */ 234 | private static getMeanObliquityOfEcliptic(julianCenturies: number): number { 235 | const seconds: number = 21.448 - julianCenturies 236 | * (46.8150 + julianCenturies * (0.00059 - julianCenturies * (0.001813))); 237 | return 23 + (26 + (seconds / 60)) / 60; 238 | } 239 | 240 | /** 241 | * Returns the corrected obliquity of the ecliptic (Axial 242 | * tilt). 243 | * 244 | * @param julianCenturies 245 | * the number of Julian centuries since J2000.0. 247 | * @return the corrected obliquity in degrees 248 | */ 249 | private static getObliquityCorrection(julianCenturies: number): number { 250 | const obliquityOfEcliptic: number = NOAACalculator.getMeanObliquityOfEcliptic(julianCenturies); 251 | 252 | const omega: number = 125.04 - 1934.136 * julianCenturies; 253 | return obliquityOfEcliptic + 0.00256 * Math.cos(MathUtils.degreesToRadians(omega)); 254 | } 255 | 256 | /** 257 | * Return the declination of the sun. 258 | * 259 | * @param julianCenturies 260 | * the number of Julian centuries since J2000.0. 262 | * @return 263 | * the sun's declination in degrees 264 | */ 265 | private static getSunDeclination(julianCenturies: number): number { 266 | const obliquityCorrection: number = NOAACalculator.getObliquityCorrection(julianCenturies); 267 | const lambda: number = NOAACalculator.getSunApparentLongitude(julianCenturies); 268 | 269 | const sint: number = Math.sin(MathUtils.degreesToRadians(obliquityCorrection)) * Math.sin(MathUtils.degreesToRadians(lambda)); 270 | const theta = MathUtils.radiansToDegrees(Math.asin(sint)); 271 | return theta; 272 | } 273 | 274 | /** 275 | * Return the Equation of Time - the difference between 276 | * true solar time and mean solar time 277 | * 278 | * @param julianCenturies 279 | * the number of Julian centuries since J2000.0. 281 | * @return equation of time in minutes of time 282 | */ 283 | private static getEquationOfTime(julianCenturies: number): number { 284 | const epsilon: number = NOAACalculator.getObliquityCorrection(julianCenturies); 285 | const geomMeanLongSun: number = NOAACalculator.getSunGeometricMeanLongitude(julianCenturies); 286 | const eccentricityEarthOrbit: number = NOAACalculator.getEarthOrbitEccentricity(julianCenturies); 287 | const geomMeanAnomalySun: number = NOAACalculator.getSunGeometricMeanAnomaly(julianCenturies); 288 | 289 | let y: number = Math.tan(MathUtils.degreesToRadians(epsilon) / 2); 290 | y *= y; 291 | 292 | const sin2l0: number = Math.sin(2 * MathUtils.degreesToRadians(geomMeanLongSun)); 293 | const sinm: number = Math.sin(MathUtils.degreesToRadians(geomMeanAnomalySun)); 294 | const cos2l0: number = Math.cos(2 * MathUtils.degreesToRadians(geomMeanLongSun)); 295 | const sin4l0: number = Math.sin(4 * MathUtils.degreesToRadians(geomMeanLongSun)); 296 | const sin2m: number = Math.sin(2 * MathUtils.degreesToRadians(geomMeanAnomalySun)); 297 | 298 | const equationOfTime: number = y * sin2l0 - 2 * eccentricityEarthOrbit * sinm + 4 * eccentricityEarthOrbit * y 299 | * sinm * cos2l0 - 0.5 * y * y * sin4l0 - 1.25 * eccentricityEarthOrbit * eccentricityEarthOrbit * sin2m; 300 | return MathUtils.radiansToDegrees(equationOfTime) * 4; 301 | } 302 | 303 | /** 304 | * Return the hour angle of the sun in 305 | * radians at sunrise for the latitude. 306 | * 307 | * @param latitude 308 | * the latitude of observer in degrees 309 | * @param solarDeclination 310 | * the declination angle of sun in degrees 311 | * @param zenith 312 | * the zenith 313 | * @param solarEvent 314 | * If the hour angle is for {@link SolarEvent#SUNRISE SUNRISE} or {@link SolarEvent#SUNSET SUNSET} 315 | * @return hour angle of sunrise in radians 316 | */ 317 | private static getSunHourAngle(latitude: number, solarDeclination: number, zenith: number, solarEvent: ValueOf): number { 318 | const latRad: number = MathUtils.degreesToRadians(latitude); 319 | const sdRad: number = MathUtils.degreesToRadians(solarDeclination); 320 | 321 | let hourAngle: number = (Math.acos(Math.cos(MathUtils.degreesToRadians(zenith)) / (Math.cos(latRad) * Math.cos(sdRad)) 322 | - Math.tan(latRad) * Math.tan(sdRad))); 323 | 324 | if (solarEvent === NOAACalculator.SolarEvent.SUNSET) { 325 | hourAngle = -hourAngle; 326 | } 327 | return hourAngle; 328 | } 329 | 330 | /** 331 | * Return the Solar Elevation for the 332 | * horizontal coordinate system at the given location at the given time. Can be negative if the sun is below the 333 | * horizon. Not corrected for altitude. 334 | * 335 | * @param calendar 336 | * time of calculation 337 | * @param latitude 338 | * latitude of location for calculation 339 | * @param longitude 340 | * longitude of location for calculation 341 | * @return solar elevation in degrees - horizon is 0 degrees, civil twilight is -6 degrees 342 | */ 343 | 344 | public static getSolarElevation(date: DateTime, latitude: number, longitude: number): number { 345 | const julianDay: number = NOAACalculator.getJulianDay(date); 346 | const julianCenturies: number = NOAACalculator.getJulianCenturiesFromJulianDay(julianDay); 347 | const eot: number = NOAACalculator.getEquationOfTime(julianCenturies); 348 | let adjustedLongitude: number = (date.hour + 12) + (date.minute + eot + date.second / 60) / 60; 349 | adjustedLongitude = -((adjustedLongitude * 360.0) / 24.0) % 360.0; 350 | const hourAngleRad: number = MathUtils.degreesToRadians(longitude - adjustedLongitude); 351 | 352 | const declination: number = NOAACalculator.getSunDeclination(julianCenturies); 353 | const decRad: number = MathUtils.degreesToRadians(declination); 354 | const latRad: number = MathUtils.degreesToRadians(latitude); 355 | return MathUtils.radiansToDegrees(Math.asin((Math.sin(latRad) * Math.sin(decRad)) 356 | + (Math.cos(latRad) * Math.cos(decRad) * Math.cos(hourAngleRad)))); 357 | } 358 | 359 | /** 360 | * Return the Solar Azimuth for the 361 | * horizontal coordinate system at the given location at the given time. Not corrected for altitude. True south is 0 362 | * degrees. 363 | * 364 | * @param cal 365 | * time of calculation 366 | * @param latitude 367 | * latitude of location for calculation 368 | * @param longitude 369 | * longitude of location for calculation 370 | * @return FIXME 371 | */ 372 | 373 | public static getSolarAzimuth(date: DateTime, latitude: number, longitude: number): number { 374 | const julianDay: number = NOAACalculator.getJulianDay(date); 375 | const julianCenturies: number = NOAACalculator.getJulianCenturiesFromJulianDay(julianDay); 376 | const eot: number = NOAACalculator.getEquationOfTime(julianCenturies); 377 | let adjustedLongitude: number = (date.hour + 12) + (date.minute + eot + date.second / 60) / 60; 378 | adjustedLongitude = -((adjustedLongitude * 360.0) / 24.0) % 360.0; 379 | const hourAngleRad: number = MathUtils.degreesToRadians(longitude - adjustedLongitude); 380 | 381 | const declination: number = NOAACalculator.getSunDeclination(julianCenturies); 382 | const decRad: number = MathUtils.degreesToRadians(declination); 383 | const latRad: number = MathUtils.degreesToRadians(latitude); 384 | 385 | return MathUtils.radiansToDegrees(Math.atan(Math.sin(hourAngleRad) 386 | / ((Math.cos(hourAngleRad) * Math.sin(latRad)) - (Math.tan(decRad) * Math.cos(latRad))))) + 180; 387 | } 388 | 389 | /** 390 | * Return the Universal Coordinated Time (UTC) 391 | * of solar noon for the given day at the given location 392 | * on earth. This implementation returns true solar noon as opposed to the time halfway between sunrise and sunset. 393 | * Other calculators may return a more simplified calculation of halfway between sunrise and sunset. See The Definition of Chatzos for details on 395 | * solar noon calculations. 396 | * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation) 397 | * @see #getSolarNoonMidnightUTC(double, double, SolarEvent) 398 | * 399 | * @param date 400 | * The Calendar representing the date to calculate solar noon for 401 | * @param geoLocation 402 | * The location information used for astronomical calculating sun times. This class uses only requires 403 | * the longitude for calculating noon since it is the same time anywhere along the longitude line. 404 | * @return the time in minutes from zero UTC 405 | */ 406 | public getUTCNoon(date: DateTime, geoLocation: GeoLocation): number { 407 | let noon = NOAACalculator.getSolarNoonMidnightUTC(NOAACalculator.getJulianDay(date), -geoLocation.getLongitude(), NOAACalculator.SolarEvent.NOON); 408 | noon = noon / 60; 409 | 410 | return noon > 0 ? noon % 24 : (noon % 24) + 24; // ensure that the time is >= 0 and < 24 411 | } 412 | 413 | /** 414 | * Return the Universal Coordinated Time 415 | * (UTC) of the solar midnight for the end of the given civil 416 | * day at the given location on earth (about 12 hours after solar noon). This implementation returns true solar 417 | * midnight as opposed to the time halfway between sunrise and sunset. Other calculators may return a more 418 | * simplified calculation of halfway between sunrise and sunset. See The Definition of Chatzos for details on 420 | * solar noon / midnight calculations. 421 | * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation) 422 | * @see #getSolarNoonMidnightUTC(double, double, SolarEvent) 423 | * 424 | * @param calendar 425 | * The Calendar representing the date to calculate solar noon for 426 | * @param geoLocation 427 | * The location information used for astronomical calculating sun times. This class uses only requires 428 | * the longitude for calculating noon since it is the same time anywhere along the longitude line. 429 | * @return the time in minutes from zero UTC 430 | */ 431 | public getUTCMidnight(date: DateTime, geoLocation: GeoLocation): number { 432 | let midnight: number = NOAACalculator.getSolarNoonMidnightUTC(NOAACalculator.getJulianDay(date), -geoLocation.getLongitude(), NOAACalculator.SolarEvent.MIDNIGHT); 433 | midnight = midnight / 60; 434 | return midnight > 0 ? midnight % 24 : (midnight % 24) + 24; // ensure that the time is >= 0 and < 24 435 | } 436 | 437 | /** 438 | * Return the Universal Coordinated Time (UTC) 439 | * midnight (about 12 hours after solar noon) of the given day at the given location on earth. 440 | * 441 | * @param julianDay 442 | * The Julian day since J2000.0. 444 | * @param longitude 445 | * the longitude of observer in degrees 446 | * @param solarEvent 447 | * If the calculation is for {@link SolarEvent#NOON NOON} or {@link SolarEvent#MIDNIGHT MIDNIGHT} 448 | * 449 | * @return the time in minutes from zero UTC 450 | * 451 | * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation) 452 | * @see #getUTCNoon(Calendar, GeoLocation) 453 | */ 454 | private static getSolarNoonMidnightUTC(julianDay: number, longitude: number, solarEvent: ValueOf): number { 455 | // eslint-disable-next-line no-param-reassign 456 | julianDay = (solarEvent === NOAACalculator.SolarEvent.NOON) ? julianDay : julianDay + 0.5; 457 | // First pass for approximate solar noon to calculate equation of time 458 | const tnoon: number = NOAACalculator.getJulianCenturiesFromJulianDay(julianDay + longitude / 360); 459 | let equationOfTime: number = NOAACalculator.getEquationOfTime(tnoon); 460 | const solNoonUTC: number = (longitude * 4) - equationOfTime; // minutes 461 | 462 | // second pass 463 | const newt: number = NOAACalculator.getJulianCenturiesFromJulianDay(julianDay + solNoonUTC / 1440); 464 | 465 | equationOfTime = NOAACalculator.getEquationOfTime(newt); 466 | return (solarEvent === NOAACalculator.SolarEvent.NOON ? 720 : 1440) + (longitude * 4) - equationOfTime; 467 | } 468 | 469 | /** 470 | * Return the Universal Coordinated Time (UTC) 471 | * of sunrise or sunset for the given day at the given location on earth. 472 | * @todo Possibly increase the number of passes for improved accuracy, especially in the Arctic areas. 473 | * 474 | * @param calendar 475 | * The calendar 476 | * @param latitude 477 | * The latitude of observer in degrees 478 | * @param longitude 479 | * Longitude of observer in degrees 480 | * @param zenith 481 | * Zenith 482 | * @param solarEvent 483 | * If the calculation is for {@link SolarEvent#SUNRISE SUNRISE} or {@link SolarEvent#SUNSET SUNSET} 484 | * @return the time in minutes from zero Universal Coordinated Time (UTC) 485 | */ 486 | private static getSunRiseSetUTC(date: DateTime, latitude: number, longitude: number, zenith: number, solarEvent: ValueOf): number { 487 | const julianDay: number = this.getJulianDay(date); 488 | 489 | // Find the time of solar noon at the location, and use that declination. 490 | // This is better than start of the Julian day 491 | // TODO really not needed since the Julian day starts from local fixed noon. Changing this would be more 492 | // efficient but would likely cause a very minor discrepancy in the calculated times (likely not reducing 493 | // accuracy, just slightly different, thus potentially breaking test cases). Regardless, it would be within 494 | // milliseconds. 495 | const noonmin: number = NOAACalculator.getSolarNoonMidnightUTC(julianDay, longitude, NOAACalculator.SolarEvent.NOON); 496 | const tnoon: number = NOAACalculator.getJulianCenturiesFromJulianDay(julianDay + noonmin / 1440); 497 | 498 | // First calculates sunrise and approximate length of day 499 | let equationOfTime: number = NOAACalculator.getEquationOfTime(tnoon); 500 | let solarDeclination: number = NOAACalculator.getSunDeclination(tnoon); 501 | let hourAngle: number = NOAACalculator.getSunHourAngle(latitude, solarDeclination, zenith, solarEvent); 502 | 503 | let delta: number = longitude - MathUtils.radiansToDegrees(hourAngle); 504 | let timeDiff: number = 4 * delta; 505 | let timeUTC: number = 720 + timeDiff - equationOfTime; 506 | 507 | // Second pass includes fractional Julian Day in gamma calc 508 | const newt: number = NOAACalculator.getJulianCenturiesFromJulianDay(julianDay + timeUTC / 1440); 509 | 510 | equationOfTime = NOAACalculator.getEquationOfTime(newt); 511 | 512 | solarDeclination = NOAACalculator.getSunDeclination(newt); 513 | hourAngle = NOAACalculator.getSunHourAngle(latitude, solarDeclination, zenith, solarEvent); 514 | 515 | delta = longitude - MathUtils.radiansToDegrees(hourAngle); 516 | timeDiff = 4 * delta; 517 | timeUTC = 720 + timeDiff - equationOfTime; 518 | return timeUTC; 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /src/util/GeoLocation.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { MathUtils, TimeZone } from '../polyfills/Utils'; 4 | import { IllegalArgumentException, UnsupportedError } from '../polyfills/errors'; 5 | 6 | /** 7 | * A class that contains location information such as latitude and longitude required for astronomical calculations. The 8 | * elevation field may not be used by some calculation engines and would be ignored if set. Check the documentation for 9 | * specific implementations of the {@link AstronomicalCalculator} to see if elevation is calculated as part of the 10 | * algorithm. 11 | * 12 | * @author © Eliyahu Hershfeld 2004 - 2016 13 | * @version 1.1 14 | */ 15 | export class GeoLocation { 16 | /** 17 | * The latitude, for example 40.096 for Lakewood, NJ. 18 | * @see #getLatitude() 19 | * @see #setLatitude(double) 20 | * @see #setLatitude(int, int, double, String) 21 | */ 22 | private latitude!: number; 23 | 24 | /** 25 | * The longitude, for example -74.222 for Lakewood, NJ. 26 | * @see #getLongitude() 27 | * @see #setLongitude(double) 28 | * @see #setLongitude(int, int, double, String) 29 | */ 30 | private longitude!: number; 31 | 32 | /** 33 | * The location name used for display, for example "Lakewood, NJ". 34 | * @see #getLocationName() 35 | * @see #setLocationName(String) 36 | */ 37 | private locationName: string | null = null; 38 | 39 | /** 40 | * The location's time zone. 41 | * @see #getTimeZone() 42 | * @see #setTimeZone(TimeZone) 43 | */ 44 | private timeZoneId!: string; 45 | 46 | /** 47 | * The elevation in Meters above sea level. 48 | * @see #getElevation() 49 | * @see #setElevation(double) 50 | */ 51 | private elevation!: number; 52 | 53 | /** 54 | * Constant for a distance type calculation. 55 | * @see #getGeodesicDistance(GeoLocation) 56 | */ 57 | private static readonly DISTANCE: number = 0; 58 | 59 | /** 60 | * Constant for an initial bearing type calculation. 61 | * @see #getGeodesicInitialBearing(GeoLocation) 62 | */ 63 | private static readonly INITIAL_BEARING: number = 1; 64 | 65 | /** 66 | * Constant for a final bearing type calculation. 67 | * @see #getGeodesicFinalBearing(GeoLocation) 68 | */ 69 | private static readonly FINAL_BEARING: number = 2; 70 | 71 | /** constant for milliseconds in a minute (60,000) */ 72 | private static readonly MINUTE_MILLIS: number = 60 * 1000; 73 | 74 | /** constant for milliseconds in an hour (3,600,000) */ 75 | private static readonly HOUR_MILLIS: number = GeoLocation.MINUTE_MILLIS * 60; 76 | 77 | /** 78 | * Method to return the elevation in Meters above sea level. 79 | * 80 | * @return Returns the elevation in Meters. 81 | * @see #setElevation(double) 82 | */ 83 | public getElevation(): number { 84 | return this.elevation; 85 | } 86 | 87 | /** 88 | * Method to set the elevation in Meters above sea level. 89 | * 90 | * @param elevation 91 | * The elevation to set in Meters. An IllegalArgumentException will be thrown if the value is a negative, 92 | * {@link java.lang.Double#isNaN(double) NaN} or {@link java.lang.Double#isInfinite(double) infinite}. 93 | */ 94 | public setElevation(elevation: number): void { 95 | if (elevation < 0) { 96 | throw new IllegalArgumentException('Elevation cannot be negative'); 97 | } 98 | if (Number.isNaN(elevation) || !Number.isFinite(elevation)) { 99 | throw new IllegalArgumentException('Elevation cannot be NaN or infinite'); 100 | } 101 | this.elevation = elevation; 102 | } 103 | 104 | /** 105 | * GeoLocation constructor with parameters for all required fields. 106 | * 107 | * @param name 108 | * The location name for display, for example "Lakewood, NJ". 109 | * @param latitude 110 | * the latitude as a double, for example 40.096 for Lakewood, NJ. 111 | * Note: For latitudes south of the equator, a negative value should be used. 112 | * @param longitude 113 | * the longitude as a double, for example -74.222 for Lakewood, NJ. Note: For longitudes 114 | * east of the Prime Meridian (Greenwich), 115 | * a negative value should be used. 116 | * @param timeZone 117 | * the TimeZone for the location. 118 | */ 119 | 120 | /* 121 | public GeoLocation(String name, double latitude, double longitude, TimeZone timeZone) { 122 | this(name, latitude, longitude, 0, timeZone); 123 | } 124 | */ 125 | 126 | /** 127 | * GeoLocation constructor with parameters for all required fields. 128 | * 129 | * @param name 130 | * The location name for display, for example "Lakewood, NJ". 131 | * @param latitude 132 | * the latitude as a double, for example 40.096 for Lakewood, NJ. 133 | * Note: For latitudes south of the equator, a negative value should be used. 134 | * @param longitude 135 | * double the longitude as a double, for example -74.222 for Lakewood, NJ. 136 | * Note: For longitudes east of the Prime 137 | * Meridian (Greenwich), a negative value should be used. 138 | * @param elevation 139 | * the elevation above sea level in Meters. 140 | * @param timeZoneId 141 | * the TimeZone for the location. 142 | */ 143 | constructor(name: string | null, latitude: number, longitude: number, elevation: number, timeZoneId?: string) 144 | constructor(name: string | null, latitude: number, longitude: number, timeZoneId: string) 145 | constructor() 146 | constructor(name: string | null = 'Greenwich, England', latitude: number = 51.4772, 147 | longitude: number = 0, elevationOrTimeZoneId?: number | string, timeZoneId?: string) { 148 | let elevation: number = 0; 149 | if (timeZoneId) { 150 | elevation = elevationOrTimeZoneId as number; 151 | } else { 152 | timeZoneId = elevationOrTimeZoneId as string; 153 | } 154 | 155 | this.setLocationName(name); 156 | this.setLatitude(latitude); 157 | this.setLongitude(longitude); 158 | this.setElevation(elevation); 159 | this.setTimeZone(timeZoneId); 160 | } 161 | 162 | /** 163 | * Default GeoLocation constructor will set location to the Prime Meridian at Greenwich, England and a TimeZone of 164 | * GMT. The longitude will be set to 0 and the latitude will be 51.4772 to match the location of the Royal Observatory, Greenwich . No daylight savings time will be used. 166 | */ 167 | /* 168 | public GeoLocation() { 169 | setLocationName("Greenwich, England"); 170 | setLongitude(0); // added for clarity 171 | setLatitude(51.4772); 172 | setTimeZone(TimeZone.getTimeZone("GMT")); 173 | } 174 | */ 175 | 176 | /** 177 | * Method to set the latitude as a double, for example 40.096 for Lakewood, NJ. 178 | * 179 | * @param latitude 180 | * The degrees of latitude to set. The values should be between -90° and 90°. For example 40.096 181 | * would be used for Lakewood, NJ. Note: For latitudes south of the equator, a negative value 182 | * should be used. An IllegalArgumentException will be thrown if the value exceeds the limits. 183 | */ 184 | 185 | /* 186 | public setLatitude(latitude: number): void { 187 | if (latitude > 90 || latitude < -90 || Number.isNaN(latitude)) { 188 | throw new IllegalArgumentException("Latitude must be between -90 and 90"); 189 | } 190 | this.latitude = latitude; 191 | } 192 | */ 193 | 194 | /** 195 | * Method to set the latitude in degrees, minutes and seconds. For example, set the degrees to 40, minutes to 5, 196 | * seconds to 45.6 and direction to "N" for Lakewood, NJ. 197 | * 198 | * @param degrees 199 | * The degrees of latitude to set between 0° and 90°, for example 40 would be used for Lakewood, NJ. 200 | * An IllegalArgumentException will be thrown if the value exceeds the limit. 201 | * @param minutes 202 | * minutes of arc, for example 5 203 | * would be used for Lakewood, NJ. 204 | * @param seconds 205 | * seconds of arc, for example 45.6 206 | * would be used for Lakewood, NJ. 207 | * @param direction 208 | * "N" for north and "S" for south, for example "N" would be used for Lakewood, NJ. An 209 | * IllegalArgumentException will be thrown if the value is not "S" or "N". 210 | */ 211 | public setLatitude(degrees: number, minutes: number, seconds: number, direction: 'N' | 'S'): void; 212 | public setLatitude(latitude: number): void; 213 | public setLatitude(degreesOrLatitude: number, minutes?: number, seconds?: number, direction?: 'N' | 'S'): void { 214 | if (minutes === undefined) { 215 | const latitude: number = degreesOrLatitude; 216 | 217 | if (latitude > 90 || latitude < -90 || Number.isNaN(latitude)) { 218 | throw new IllegalArgumentException('Latitude must be between -90 and 90'); 219 | } 220 | 221 | this.latitude = latitude; 222 | } else { 223 | const degrees: number = degreesOrLatitude; 224 | 225 | let tempLat: number = degrees + ((minutes + (seconds! / 60)) / 60); 226 | if (tempLat > 90 || tempLat < 0 || Number.isNaN(tempLat)) { // FIXME An exception should be thrown if degrees, minutes or seconds are negative 227 | throw new IllegalArgumentException('Latitude must be between 0 and 90. Use direction of S instead of negative.'); 228 | } 229 | if (direction === 'S') { 230 | tempLat *= -1; 231 | } else if (!(direction === 'N')) { 232 | throw new IllegalArgumentException('Latitude direction must be N or S'); 233 | } 234 | this.latitude = tempLat; 235 | } 236 | } 237 | 238 | /** 239 | * @return Returns the latitude as a double, for example 40.096 for Lakewood, NJ. 240 | */ 241 | public getLatitude(): number { 242 | return this.latitude; 243 | } 244 | 245 | /** 246 | * Method to set the longitude as a double, for example -74.222 for Lakewood, NJ 247 | * 248 | * @param longitude 249 | * The degrees of longitude to set as a double between -180.0° and 180.0°. For example 250 | * -74.222 would be used for Lakewood, NJ. Note: for longitudes east of the Prime Meridian (Greenwich) a negative value 252 | * should be used.An IllegalArgumentException will be thrown if the value exceeds the limit. 253 | */ 254 | 255 | /* 256 | public setLongitude(longitude: number): void { 257 | if (longitude > 180 || longitude < -180 || Number.isNaN(longitude)) { 258 | throw new IllegalArgumentException("Longitude must be between -180 and 180"); 259 | } 260 | this.longitude = longitude; 261 | } 262 | */ 263 | 264 | /** 265 | * Method to set the longitude in degrees, minutes and seconds. For example, set the degrees to 74, minutes to 13, 266 | * seconds to 19.2 and direction to "W" for Lakewood, NJ. 267 | * 268 | * @param degrees 269 | * The degrees of longitude to set between 0° and 180°. For example 74 would be set for Lakewood, NJ. 270 | * An IllegalArgumentException will be thrown if the value exceeds the limits. 271 | * @param minutes 272 | * minutes of arc. For example 13 273 | * would be set for Lakewood, NJ. 274 | * @param seconds 275 | * seconds of arc. For example 19.2 276 | * would be set for Lakewood, NJ. 277 | * @param direction 278 | * "E" for east of the Prime Meridian 279 | * or "W"for west of it. For example, "W" would be set for Lakewood, NJ. 280 | * An IllegalArgumentException will be thrown if the value is not E or W. 281 | */ 282 | public setLongitude(degrees: number, minutes: number, seconds: number, direction: 'E' | 'W'): void; 283 | public setLongitude(longitude: number): void; 284 | public setLongitude(degreesOrLongitude: number, minutes?: number, seconds?: number, direction?: 'E' | 'W'): void { 285 | if (minutes === undefined) { 286 | const longitude: number = degreesOrLongitude; 287 | 288 | if (longitude > 180 || longitude < -180 || Number.isNaN(longitude)) { 289 | throw new IllegalArgumentException('Longitude must be between -180 and 180'); 290 | } 291 | 292 | this.longitude = longitude; 293 | } else { 294 | const degrees: number = degreesOrLongitude; 295 | 296 | let longTemp: number = degrees + ((minutes + (seconds! / 60)) / 60); 297 | if (longTemp > 180 || this.longitude < 0 || Number.isNaN(longTemp)) { // FIXME An exception should be thrown if degrees, minutes or seconds are negative 298 | throw new IllegalArgumentException('Longitude must be between 0 and 180. Use a direction of W instead of negative.'); 299 | } 300 | if (direction === 'W') { 301 | longTemp *= -1; 302 | } else if (!(direction === 'E')) { 303 | throw new IllegalArgumentException('Longitude direction must be E or W'); 304 | } 305 | this.longitude = longTemp; 306 | } 307 | } 308 | 309 | /** 310 | * Method to return the longitude as a double, for example -74.222 for Lakewood, NJ. 311 | * @return Returns the longitude. 312 | */ 313 | public getLongitude(): number { 314 | return this.longitude; 315 | } 316 | 317 | /** 318 | * Method to return the location name (for display), for example "Lakewood, NJ". 319 | * @return Returns the location name. 320 | */ 321 | public getLocationName(): string | null { 322 | return this.locationName; 323 | } 324 | 325 | /** 326 | * Setter for the location name (for display), for example "Lakewood, NJ". 327 | * @param name 328 | * The setter method for the display name. 329 | */ 330 | public setLocationName(name: string | null): void { 331 | this.locationName = name; 332 | } 333 | 334 | /** 335 | * Method to return the time zone. 336 | * @return Returns the timeZone. 337 | */ 338 | public getTimeZone(): string { 339 | return this.timeZoneId; 340 | } 341 | 342 | /** 343 | * Method to set the TimeZone. If this is ever set after the GeoLocation is set in the 344 | * {@link AstronomicalCalendar}, it is critical that 345 | * {@link AstronomicalCalendar#getCalendar()}. 346 | * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} be called in order for the 347 | * AstronomicalCalendar to output times in the expected offset. This situation will arise if the 348 | * AstronomicalCalendar is ever {@link AstronomicalCalendar#clone() cloned}. 349 | * 350 | * @param timeZone 351 | * The timeZone to set. 352 | */ 353 | public setTimeZone(timeZoneId: string): void { 354 | this.timeZoneId = timeZoneId; 355 | } 356 | 357 | /** 358 | * A method that will return the location's local mean time offset in milliseconds from local standard time. The globe is split into 360°, with 360 | * 15° per hour of the day. For a local that is at a longitude that is evenly divisible by 15 (longitude % 15 == 361 | * 0), at solar {@link AstronomicalCalendar#getSunTransit() noon} (with adjustment for the equation of time) the sun should be directly overhead, 363 | * so a user who is 1° west of this will have noon at 4 minutes after standard time noon, and conversely, a user 364 | * who is 1° east of the 15° longitude will have noon at 11:56 AM. Lakewood, N.J., whose longitude is 365 | * -74.222, is 0.778 away from the closest multiple of 15 at -75°. This is multiplied by 4 to yield 3 minutes 366 | * and 10 seconds earlier than standard time. The offset returned does not account for the Daylight saving time offset since this class is 368 | * unaware of dates. 369 | * 370 | * @return the offset in milliseconds not accounting for Daylight saving time. A positive value will be returned 371 | * East of the 15° timezone line, and a negative value West of it. 372 | */ 373 | public getLocalMeanTimeOffset(): number { 374 | return this.getLongitude() * 4 * GeoLocation.MINUTE_MILLIS - TimeZone.getRawOffset(this.getTimeZone()); 375 | } 376 | 377 | /** 378 | * Adjust the date for antimeridian crossover. This is 379 | * needed to deal with edge cases such as Samoa that use a different calendar date than expected based on their 380 | * geographic location. 381 | * 382 | * The actual Time Zone offset may deviate from the expected offset based on the longitude. Since the 'absolute time' 383 | * calculations are always based on longitudinal offset from UTC for a given date, the date is presumed to only 384 | * increase East of the Prime Meridian, and to only decrease West of it. For Time Zones that cross the antimeridian, 385 | * the date will be artificially adjusted before calculation to conform with this presumption. 386 | * 387 | * For example, Apia, Samoa with a longitude of -171.75 uses a local offset of +14:00. When calculating sunrise for 388 | * 2018-02-03, the calculator should operate using 2018-02-02 since the expected zone is -11. After determining the 389 | * UTC time, the local DST offset of UTC+14:00 should be applied 390 | * to bring the date back to 2018-02-03. 391 | * 392 | * @return the number of days to adjust the date This will typically be 0 unless the date crosses the antimeridian 393 | */ 394 | public getAntimeridianAdjustment(): -1 | 1 | 0 { 395 | const localHoursOffset: number = this.getLocalMeanTimeOffset() / GeoLocation.HOUR_MILLIS; 396 | 397 | // if the offset is 20 hours or more in the future (never expected anywhere other 398 | // than a location using a timezone across the antimeridian to the east such as Samoa) 399 | if (localHoursOffset >= 20) { 400 | // roll the date forward a day 401 | return 1; 402 | } else if (localHoursOffset <= -20) { 403 | // if the offset is 20 hours or more in the past (no current location is known 404 | // that crosses the antimeridian to the west, but better safe than sorry) 405 | // roll the date back a day 406 | return -1; 407 | } 408 | // 99.999% of the world will have no adjustment 409 | return 0; 410 | } 411 | 412 | /** 413 | * Calculate the initial geodesic bearing between this 414 | * Object and a second Object passed to this method using Thaddeus Vincenty's inverse formula See T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid 417 | * with application of nested equations", Survey Review, vol XXII no 176, 1975 418 | * 419 | * @param location 420 | * the destination location 421 | * @return the initial bearing 422 | */ 423 | public getGeodesicInitialBearing(location: GeoLocation): number { 424 | return this.vincentyInverseFormula(location, GeoLocation.INITIAL_BEARING); 425 | } 426 | 427 | /** 428 | * Calculate the final geodesic bearing between this Object 429 | * and a second Object passed to this method using Thaddeus 430 | * Vincenty's inverse formula See T Vincenty, "Direct and 431 | * Inverse Solutions of Geodesics on the Ellipsoid with application of nested equations", Survey Review, vol 432 | * XXII no 176, 1975 433 | * 434 | * @param location 435 | * the destination location 436 | * @return the final bearing 437 | */ 438 | public getGeodesicFinalBearing(location: GeoLocation): number { 439 | return this.vincentyInverseFormula(location, GeoLocation.FINAL_BEARING); 440 | } 441 | 442 | /** 443 | * Calculate geodesic distance in Meters between 444 | * this Object and a second Object passed to this method using Thaddeus Vincenty's inverse formula See T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid 447 | * with application of nested equations", Survey Review, vol XXII no 176, 1975 448 | * 449 | * @see #vincentyInverseFormula(GeoLocation, int) 450 | * @param location 451 | * the destination location 452 | * @return the geodesic distance in Meters 453 | */ 454 | public getGeodesicDistance(location: GeoLocation): number { 455 | return this.vincentyInverseFormula(location, GeoLocation.DISTANCE); 456 | } 457 | 458 | /** 459 | * Calculate geodesic distance in Meters between 460 | * this Object and a second Object passed to this method using Thaddeus Vincenty's inverse formula See T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid 463 | * with application of nested equations", Survey Review, vol XXII no 176, 1975 464 | * 465 | * @param location 466 | * the destination location 467 | * @param formula 468 | * This formula calculates initial bearing ({@link #INITIAL_BEARING}), final bearing ( 469 | * {@link #FINAL_BEARING}) and distance ({@link #DISTANCE}). 470 | * @return geodesic distance in Meters 471 | */ 472 | private vincentyInverseFormula(location: GeoLocation, formula: number): number { 473 | const majorSemiAxis: number = 6378137; 474 | const minorSemiAxis: number = 6356752.3142; 475 | const f: number = 1 / 298.257223563; // WGS-84 ellipsoid 476 | const L: number = MathUtils.degreesToRadians(location.getLongitude() - this.getLongitude()); 477 | const U1: number = Math.atan((1 - f) * Math.tan(MathUtils.degreesToRadians(this.getLatitude()))); 478 | const U2: number = Math.atan((1 - f) * Math.tan(MathUtils.degreesToRadians(location.getLatitude()))); 479 | const sinU1: number = Math.sin(U1); 480 | const cosU1: number = Math.cos(U1); 481 | const sinU2: number = Math.sin(U2); 482 | const cosU2: number = Math.cos(U2); 483 | 484 | let lambda: number = L; 485 | let lambdaP: number = 2 * Math.PI; 486 | let iterLimit: number = 20; 487 | let sinLambda: number = 0; 488 | let cosLambda: number = 0; 489 | let sinSigma: number = 0; 490 | let cosSigma: number = 0; 491 | let sigma: number = 0; 492 | let sinAlpha: number = 0; 493 | let cosSqAlpha: number = 0; 494 | let cos2SigmaM: number = 0; 495 | let C: number; 496 | 497 | while (Math.abs(lambda - lambdaP) > 1e-12 && --iterLimit > 0) { 498 | sinLambda = Math.sin(lambda); 499 | cosLambda = Math.cos(lambda); 500 | sinSigma = Math.sqrt((cosU2 * sinLambda) * (cosU2 * sinLambda) 501 | + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); 502 | if (sinSigma === 0) return 0; // co-incident points 503 | cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; 504 | sigma = Math.atan2(sinSigma, cosSigma); 505 | sinAlpha = (cosU1 * cosU2 * sinLambda) / sinSigma; 506 | cosSqAlpha = 1 - sinAlpha * sinAlpha; 507 | cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha; 508 | if (Number.isNaN(cos2SigmaM)) cos2SigmaM = 0; // equatorial line: cosSqAlpha=0 (§6) 509 | C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha)); 510 | lambdaP = lambda; 511 | lambda = L + (1 - C) * f * sinAlpha 512 | * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM))); 513 | } 514 | if (iterLimit === 0) return Number.NaN; // formula failed to converge 515 | 516 | const uSq: number = cosSqAlpha * (majorSemiAxis * majorSemiAxis - minorSemiAxis * minorSemiAxis) / (minorSemiAxis * minorSemiAxis); 517 | const A: number = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))); 518 | const B: number = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); 519 | const deltaSigma: number = B * sinSigma 520 | * (cos2SigmaM + B / 4 521 | * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B / 6 * cos2SigmaM 522 | * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM))); 523 | const distance: number = minorSemiAxis * A * (sigma - deltaSigma); 524 | 525 | // initial bearing 526 | const fwdAz: number = MathUtils.radiansToDegrees(Math.atan2(cosU2 * sinLambda, cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); 527 | // final bearing 528 | const revAz: number = MathUtils.radiansToDegrees(Math.atan2(cosU1 * sinLambda, -sinU1 * cosU2 + cosU1 * sinU2 * cosLambda)); 529 | if (formula === GeoLocation.DISTANCE) { 530 | return distance; 531 | } else if (formula === GeoLocation.INITIAL_BEARING) { 532 | return fwdAz; 533 | } else if (formula === GeoLocation.FINAL_BEARING) { 534 | return revAz; 535 | } 536 | // should never happen 537 | return Number.NaN; 538 | } 539 | 540 | /** 541 | * Returns the rhumb line bearing from the current location to 542 | * the GeoLocation passed in. 543 | * 544 | * @param location 545 | * destination location 546 | * @return the bearing in degrees 547 | */ 548 | public getRhumbLineBearing(location: GeoLocation): number { 549 | let dLon: number = MathUtils.degreesToRadians(location.getLongitude() - this.getLongitude()); 550 | const dPhi: number = Math.log(Math.tan(MathUtils.degreesToRadians(location.getLatitude()) / 2 + Math.PI / 4) 551 | / Math.tan(MathUtils.degreesToRadians(this.getLatitude()) / 2 + Math.PI / 4)); 552 | if (Math.abs(dLon) > Math.PI) dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon); 553 | return MathUtils.radiansToDegrees(Math.atan2(dLon, dPhi)); 554 | } 555 | 556 | /** 557 | * Returns the rhumb line distance from the current location 558 | * to the GeoLocation passed in. 559 | * 560 | * @param location 561 | * the destination location 562 | * @return the distance in Meters 563 | */ 564 | public getRhumbLineDistance(location: GeoLocation): number { 565 | const earthRadius: number = 6378137; // earth's mean radius in km 566 | const dLat: number = MathUtils.degreesToRadians(location.getLatitude()) - MathUtils.degreesToRadians(this.getLatitude()); 567 | let dLon: number = Math.abs(MathUtils.degreesToRadians(location.getLongitude()) - MathUtils.degreesToRadians(this.getLongitude())); 568 | const dPhi: number = Math.log(Math.tan(MathUtils.degreesToRadians(location.getLatitude()) / 2 + Math.PI / 4) 569 | / Math.tan(MathUtils.degreesToRadians(this.getLatitude()) / 2 + Math.PI / 4)); 570 | 571 | let q: number = dLat / dPhi; 572 | if (!Number.isFinite(q)) { 573 | q = Math.cos(MathUtils.degreesToRadians(this.getLatitude())); 574 | } 575 | 576 | // if dLon over 180° take shorter rhumb across 180° meridian: 577 | if (dLon > Math.PI) { 578 | dLon = 2 * Math.PI - dLon; 579 | } 580 | const d: number = Math.sqrt(dLat * dLat + q * q * dLon * dLon); 581 | return d * earthRadius; 582 | } 583 | 584 | /** 585 | * A method that returns an XML formatted String representing the serialized Object. Very 586 | * similar to the toString method but the return value is in an xml format. The format currently used (subject to 587 | * change) is: 588 | * 589 | *
590 |    *   <GeoLocation>
591 |    *        <LocationName>Lakewood, NJ</LocationName>
592 |    *        <Latitude>40.096&deg</Latitude>
593 |    *        <Longitude>-74.222&deg</Longitude>
594 |    *        <Elevation>0 Meters</Elevation>
595 |    *        <TimezoneName>America/New_York</TimezoneName>
596 |    *        <TimeZoneDisplayName>Eastern Standard Time</TimeZoneDisplayName>
597 |    *        <TimezoneGMTOffset>-5</TimezoneGMTOffset>
598 |    *        <TimezoneDSTOffset>1</TimezoneDSTOffset>
599 |    *   </GeoLocation>
600 |    * 
601 | * 602 | * @return The XML formatted String. 603 | * @deprecated 604 | */ 605 | // eslint-disable-next-line class-methods-use-this 606 | public toXML(): void { 607 | throw new UnsupportedError('This method is deprecated'); 608 | } 609 | 610 | /** 611 | * @see java.lang.Object#equals(Object) 612 | */ 613 | public equals(object: object): boolean { 614 | if (this === object) return true; 615 | if (!(object instanceof GeoLocation)) return false; 616 | 617 | const geo: GeoLocation = object as GeoLocation; 618 | return this.latitude === geo.latitude 619 | && this.longitude === geo.longitude 620 | && this.elevation === geo.elevation 621 | && this.locationName === geo.locationName 622 | && this.timeZoneId === geo.getTimeZone(); 623 | } 624 | 625 | /** 626 | * @see java.lang.Object#toString() 627 | */ 628 | public toString(): string { 629 | return (`Location Name:\t\t\t${this.getLocationName()}`) 630 | .concat(`\nLatitude:\t\t\t${this.getLatitude().toString()}\u00B0`) 631 | .concat(`\nLongitude:\t\t\t${this.getLongitude().toString()}\u00B0`) 632 | .concat(`\nElevation:\t\t\t${this.getElevation().toString()} Meters`) 633 | .concat(`\nTimezone ID:\t\t\t${this.getTimeZone()}`) 634 | .concat(`\nTimezone Display Name:\t\t${TimeZone.getDisplayName(this.getTimeZone())}`) 635 | .concat(` (${TimeZone.getDisplayName(this.getTimeZone(), DateTime.local(), true)})`) 636 | .concat(`\nTimezone GMT Offset:\t\t${(TimeZone.getRawOffset(this.getTimeZone()) / GeoLocation.HOUR_MILLIS).toString()}`) 637 | .concat(`\nTimezone DST Offset:\t\t${(TimeZone.getDSTSavings(this.getTimeZone()) / GeoLocation.HOUR_MILLIS).toString()}`); 638 | } 639 | 640 | /** 641 | * An implementation of the {@link java.lang.Object#clone()} method that creates a deep copy of the object. 643 | * Note: If the {@link java.util.TimeZone} in the clone will be changed from the original, it is critical 644 | * that {@link AstronomicalCalendar#getCalendar()}. 645 | * {@link java.util.Calendar#setTimeZone(TimeZone) setTimeZone(TimeZone)} is called after cloning in order for the 646 | * AstronomicalCalendar to output times in the expected offset. 647 | * 648 | * @see java.lang.Object#clone() 649 | * @since 1.1 650 | */ 651 | public clone(): GeoLocation { 652 | return JSON.parse(JSON.stringify(this)); 653 | } 654 | } 655 | --------------------------------------------------------------------------------