ether-to-astro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -0
- package/.github/pull_request_template.md +16 -0
- package/.github/workflows/release.yml +35 -0
- package/.github/workflows/test.yml +32 -0
- package/AGENTS.md +99 -0
- package/LICENSE +18 -0
- package/NOTICE.md +45 -0
- package/README.md +301 -0
- package/SETUP.md +70 -0
- package/TESTING_SUMMARY.md +238 -0
- package/TEST_SUITE_STATUS.md +218 -0
- package/biome.json +48 -0
- package/dist/astro-service.d.ts +98 -0
- package/dist/astro-service.js +496 -0
- package/dist/chart-types.d.ts +52 -0
- package/dist/chart-types.js +51 -0
- package/dist/charts.d.ts +125 -0
- package/dist/charts.js +324 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +472 -0
- package/dist/constants.d.ts +81 -0
- package/dist/constants.js +76 -0
- package/dist/eclipses.d.ts +85 -0
- package/dist/eclipses.js +184 -0
- package/dist/ephemeris.d.ts +120 -0
- package/dist/ephemeris.js +379 -0
- package/dist/formatter.d.ts +2 -0
- package/dist/formatter.js +22 -0
- package/dist/houses.d.ts +82 -0
- package/dist/houses.js +169 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +150 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +31 -0
- package/dist/logger.d.ts +25 -0
- package/dist/logger.js +73 -0
- package/dist/profile-store.d.ts +48 -0
- package/dist/profile-store.js +156 -0
- package/dist/riseset.d.ts +82 -0
- package/dist/riseset.js +185 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.js +40 -0
- package/dist/time-utils.d.ts +68 -0
- package/dist/time-utils.js +136 -0
- package/dist/tool-registry.d.ts +35 -0
- package/dist/tool-registry.js +307 -0
- package/dist/tool-result.d.ts +175 -0
- package/dist/tool-result.js +188 -0
- package/dist/transits.d.ts +108 -0
- package/dist/transits.js +263 -0
- package/dist/types.d.ts +450 -0
- package/dist/types.js +161 -0
- package/example-usage.md +131 -0
- package/natal-chart.json +187 -0
- package/package.json +61 -0
- package/scripts/download-ephemeris.js +115 -0
- package/setup.sh +21 -0
- package/src/astro-service.ts +710 -0
- package/src/chart-types.ts +125 -0
- package/src/charts.ts +399 -0
- package/src/cli.ts +694 -0
- package/src/constants.ts +89 -0
- package/src/eclipses.ts +226 -0
- package/src/ephemeris.ts +437 -0
- package/src/formatter.ts +25 -0
- package/src/houses.ts +202 -0
- package/src/index.ts +170 -0
- package/src/loader.ts +36 -0
- package/src/logger.ts +104 -0
- package/src/profile-store.ts +285 -0
- package/src/riseset.ts +229 -0
- package/src/time-utils.ts +167 -0
- package/src/tool-registry.ts +357 -0
- package/src/tool-result.ts +283 -0
- package/src/transits.ts +352 -0
- package/src/types.ts +547 -0
- package/tests/README.md +173 -0
- package/tests/TESTING_STRATEGY.md +178 -0
- package/tests/fixtures/bowen-yang-chart.ts +69 -0
- package/tests/fixtures/calculate-expected.ts +81 -0
- package/tests/fixtures/expected-results.ts +117 -0
- package/tests/fixtures/generate-expected-simple.ts +94 -0
- package/tests/helpers/date-fixtures.ts +15 -0
- package/tests/helpers/ephem.ts +11 -0
- package/tests/helpers/temp.ts +9 -0
- package/tests/setup.ts +11 -0
- package/tests/unit/astro-service.test.ts +323 -0
- package/tests/unit/chart-types.test.ts +18 -0
- package/tests/unit/charts-errors.test.ts +42 -0
- package/tests/unit/charts.test.ts +157 -0
- package/tests/unit/cli-commands.test.ts +82 -0
- package/tests/unit/cli-profiles.test.ts +128 -0
- package/tests/unit/cli.test.ts +191 -0
- package/tests/unit/constants.test.ts +26 -0
- package/tests/unit/correctness-critical.test.ts +408 -0
- package/tests/unit/eclipses.test.ts +108 -0
- package/tests/unit/ephemeris.test.ts +213 -0
- package/tests/unit/error-handling.test.ts +116 -0
- package/tests/unit/formatter.test.ts +29 -0
- package/tests/unit/houses-errors.test.ts +27 -0
- package/tests/unit/houses-validation.test.ts +164 -0
- package/tests/unit/houses.test.ts +205 -0
- package/tests/unit/profile-store.test.ts +163 -0
- package/tests/unit/real-user-charts.test.ts +148 -0
- package/tests/unit/riseset.test.ts +106 -0
- package/tests/unit/solver-edges.test.ts +197 -0
- package/tests/unit/time-utils-temporal.test.ts +303 -0
- package/tests/unit/time-utils.test.ts +173 -0
- package/tests/unit/tool-registry.test.ts +222 -0
- package/tests/unit/tool-result.test.ts +45 -0
- package/tests/unit/transit-correctness.test.ts +78 -0
- package/tests/unit/transits.test.ts +238 -0
- package/tests/validation/README.md +32 -0
- package/tests/validation/adapters/astrolog.ts +306 -0
- package/tests/validation/adapters/internal.ts +184 -0
- package/tests/validation/compare/eclipses.ts +47 -0
- package/tests/validation/compare/houses.ts +76 -0
- package/tests/validation/compare/positions.ts +104 -0
- package/tests/validation/compare/riseSet.ts +48 -0
- package/tests/validation/compare/roots.ts +90 -0
- package/tests/validation/compare/transits.ts +69 -0
- package/tests/validation/fixtures/astrolog-parity/core.ts +194 -0
- package/tests/validation/fixtures/eclipses/core.ts +14 -0
- package/tests/validation/fixtures/houses/core.ts +47 -0
- package/tests/validation/fixtures/positions/core.ts +159 -0
- package/tests/validation/fixtures/rise-set/core.ts +20 -0
- package/tests/validation/fixtures/roots/core.ts +47 -0
- package/tests/validation/fixtures/transits/core.ts +61 -0
- package/tests/validation/fixtures/transits/dst.ts +21 -0
- package/tests/validation/oracle.spec.ts +129 -0
- package/tests/validation/utils/denseRootOracle.ts +269 -0
- package/tests/validation/utils/fixtureTypes.ts +146 -0
- package/tests/validation/utils/report.ts +60 -0
- package/tests/validation/utils/tolerances.ts +23 -0
- package/tests/validation/validation.spec.ts +836 -0
- package/tools/color-picker.html +388 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +31 -0
package/src/constants.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Log levels
|
|
2
|
+
export const LogLevel = {
|
|
3
|
+
DEBUG: 'DEBUG',
|
|
4
|
+
INFO: 'INFO',
|
|
5
|
+
WARN: 'WARN',
|
|
6
|
+
ERROR: 'ERROR',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export type LogLevelType = (typeof LogLevel)[keyof typeof LogLevel];
|
|
10
|
+
|
|
11
|
+
// Error categories
|
|
12
|
+
export const ErrorCategory = {
|
|
13
|
+
EPHEMERIS: 'EPHEMERIS',
|
|
14
|
+
CALCULATION: 'CALCULATION',
|
|
15
|
+
STORAGE: 'STORAGE',
|
|
16
|
+
VALIDATION: 'VALIDATION',
|
|
17
|
+
CHART_RENDERING: 'CHART_RENDERING',
|
|
18
|
+
SERVER: 'SERVER',
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type ErrorCategoryType = (typeof ErrorCategory)[keyof typeof ErrorCategory];
|
|
22
|
+
|
|
23
|
+
// Chart theme colors
|
|
24
|
+
export const LIGHT_THEME_COLORS = [
|
|
25
|
+
'#ffffff', // Aries - White (fire)
|
|
26
|
+
'#c1e6d1', // Taurus - Mint (earth)
|
|
27
|
+
'#ffffff', // Gemini - White (air)
|
|
28
|
+
'#c1e6d1', // Cancer - Mint (water)
|
|
29
|
+
'#ffffff', // Leo - White (fire)
|
|
30
|
+
'#c1e6d1', // Virgo - Mint (earth)
|
|
31
|
+
'#ffffff', // Libra - White (air)
|
|
32
|
+
'#c1e6d1', // Scorpio - Mint (water)
|
|
33
|
+
'#ffffff', // Sagittarius - White (fire)
|
|
34
|
+
'#c1e6d1', // Capricorn - Mint (earth)
|
|
35
|
+
'#ffffff', // Aquarius - White (air)
|
|
36
|
+
'#c1e6d1', // Pisces - Mint (water)
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const DARK_THEME_COLORS = [
|
|
40
|
+
'#282c34', // Aries - Dark (fire)
|
|
41
|
+
'#8545b0', // Taurus - Purple (earth)
|
|
42
|
+
'#282c34', // Gemini - Dark (air)
|
|
43
|
+
'#8545b0', // Cancer - Purple (water)
|
|
44
|
+
'#282c34', // Leo - Dark (fire)
|
|
45
|
+
'#8545b0', // Virgo - Purple (earth)
|
|
46
|
+
'#282c34', // Libra - Dark (air)
|
|
47
|
+
'#8545b0', // Scorpio - Purple (water)
|
|
48
|
+
'#282c34', // Sagittarius - Dark (fire)
|
|
49
|
+
'#8545b0', // Capricorn - Purple (earth)
|
|
50
|
+
'#282c34', // Aquarius - Dark (air)
|
|
51
|
+
'#8545b0', // Pisces - Purple (water)
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Aspect colors for light theme
|
|
55
|
+
export const LIGHT_ASPECT_COLORS = {
|
|
56
|
+
conjunction: { degree: 0, orbit: 10, color: 'transparent' },
|
|
57
|
+
square: { degree: 90, orbit: 8, color: '#fb923c' }, // Orange
|
|
58
|
+
trine: { degree: 120, orbit: 8, color: '#34d399' }, // Emerald
|
|
59
|
+
opposition: { degree: 180, orbit: 10, color: '#a78bfa' }, // Purple
|
|
60
|
+
sextile: { degree: 60, orbit: 6, color: '#22d3ee' }, // Cyan
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Aspect colors for dark theme
|
|
64
|
+
export const DARK_ASPECT_COLORS = {
|
|
65
|
+
conjunction: { degree: 0, orbit: 10, color: 'transparent' },
|
|
66
|
+
square: { degree: 90, orbit: 8, color: '#f97316' }, // Orange
|
|
67
|
+
trine: { degree: 120, orbit: 8, color: '#10b981' }, // Emerald
|
|
68
|
+
opposition: { degree: 180, orbit: 10, color: '#8b5cf6' }, // Purple
|
|
69
|
+
sextile: { degree: 60, orbit: 6, color: '#06b6d4' }, // Cyan
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Determine chart theme based on time of day
|
|
74
|
+
* Dark theme: 6 PM - 6 AM (18:00 - 06:00)
|
|
75
|
+
* Light theme: 6 AM - 6 PM (06:00 - 18:00)
|
|
76
|
+
*
|
|
77
|
+
* @param timezone - Optional IANA timezone to use (e.g., 'America/New_York'). Defaults to server local time.
|
|
78
|
+
* @returns 'dark' or 'light' theme
|
|
79
|
+
*/
|
|
80
|
+
export function getDefaultTheme(timezone?: string): 'light' | 'dark' {
|
|
81
|
+
const now = new Date();
|
|
82
|
+
const hour = timezone
|
|
83
|
+
? Number.parseInt(
|
|
84
|
+
now.toLocaleString('en-US', { timeZone: timezone, hour: '2-digit', hour12: false }),
|
|
85
|
+
10
|
|
86
|
+
)
|
|
87
|
+
: now.getHours();
|
|
88
|
+
return hour >= 18 || hour < 6 ? 'dark' : 'light';
|
|
89
|
+
}
|
package/src/eclipses.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { constants as Constants } from 'sweph';
|
|
2
|
+
import { ErrorCategory } from './constants.js';
|
|
3
|
+
import type { EphemerisCalculator } from './ephemeris.js';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
import type { EclipseInfo } from './types.js';
|
|
6
|
+
|
|
7
|
+
interface EclipseWhenResult {
|
|
8
|
+
flag: number;
|
|
9
|
+
error: string;
|
|
10
|
+
data: number[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isEclipseWhenResult(value: unknown): value is EclipseWhenResult {
|
|
14
|
+
if (typeof value !== 'object' || value == null) return false;
|
|
15
|
+
const obj = value as Record<string, unknown>;
|
|
16
|
+
return (
|
|
17
|
+
typeof obj.flag === 'number' &&
|
|
18
|
+
typeof obj.error === 'string' &&
|
|
19
|
+
Array.isArray(obj.data) &&
|
|
20
|
+
obj.data.every((v) => typeof v === 'number')
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculator for solar and lunar eclipses
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Finds upcoming solar and lunar eclipses using Swiss Ephemeris.
|
|
29
|
+
* Returns basic eclipse information including type and timing.
|
|
30
|
+
* TODO: Enhance with richer phase timing and visibility data.
|
|
31
|
+
*/
|
|
32
|
+
export class EclipseCalculator {
|
|
33
|
+
/** Ephemeris calculator instance */
|
|
34
|
+
private ephem: EphemerisCalculator;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a new eclipse calculator
|
|
38
|
+
*
|
|
39
|
+
* @param ephem - Initialized ephemeris calculator
|
|
40
|
+
* @throws Error if ephemeris is not initialized
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* The ephemeris calculator must be initialized before passing
|
|
44
|
+
* to the EclipseCalculator constructor.
|
|
45
|
+
*/
|
|
46
|
+
constructor(ephem: EphemerisCalculator) {
|
|
47
|
+
this.ephem = ephem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private callSolarEclipseWhenGlob(startJD: number): EclipseWhenResult {
|
|
51
|
+
if (!this.ephem.eph) {
|
|
52
|
+
throw new Error('Ephemeris not initialized');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// sweph typings currently declare `backwards` as number, but runtime expects boolean.
|
|
56
|
+
const callable = this.ephem.eph.sol_eclipse_when_glob as unknown as (
|
|
57
|
+
startJd: number,
|
|
58
|
+
flags: number,
|
|
59
|
+
eclipseType: number,
|
|
60
|
+
backwards: boolean
|
|
61
|
+
) => unknown;
|
|
62
|
+
|
|
63
|
+
const raw = callable(startJD, Constants.SEFLG_SWIEPH, 0, false);
|
|
64
|
+
if (!isEclipseWhenResult(raw)) {
|
|
65
|
+
throw new Error('Unexpected sol_eclipse_when_glob result shape');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Find the next solar eclipse after a given date
|
|
73
|
+
*
|
|
74
|
+
* @param startJD - Julian Day to start searching from
|
|
75
|
+
* @returns Solar eclipse info or null if none found
|
|
76
|
+
* @throws Error if ephemeris not initialized
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* Searches globally for the next solar eclipse. Returns basic
|
|
80
|
+
* information about the eclipse type and maximum time.
|
|
81
|
+
*/
|
|
82
|
+
findNextSolarEclipse(startJD: number): EclipseInfo | null {
|
|
83
|
+
if (!this.ephem.eph) {
|
|
84
|
+
throw new Error('Ephemeris not initialized');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const result = this.callSolarEclipseWhenGlob(startJD);
|
|
89
|
+
|
|
90
|
+
if (result.error || !result.data || result.data.length < 1) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const eclipseType = this.getSolarEclipseType(result.flag);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
type: 'solar',
|
|
98
|
+
date: this.ephem.julianDayToDate(result.data[0]),
|
|
99
|
+
eclipseType,
|
|
100
|
+
maxTime: this.ephem.julianDayToDate(result.data[0]),
|
|
101
|
+
};
|
|
102
|
+
} catch (e) {
|
|
103
|
+
logger.error(
|
|
104
|
+
'Solar eclipse calculation failed',
|
|
105
|
+
ErrorCategory.CALCULATION,
|
|
106
|
+
e instanceof Error ? e : new Error(String(e))
|
|
107
|
+
);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find the next lunar eclipse after a given date
|
|
114
|
+
*
|
|
115
|
+
* @param startJD - Julian Day to start searching from
|
|
116
|
+
* @returns Lunar eclipse info or null if none found
|
|
117
|
+
* @throws Error if ephemeris not initialized
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* Searches globally for the next lunar eclipse. Returns basic
|
|
121
|
+
* information about the eclipse type and maximum time.
|
|
122
|
+
*/
|
|
123
|
+
findNextLunarEclipse(startJD: number): EclipseInfo | null {
|
|
124
|
+
if (!this.ephem.eph) {
|
|
125
|
+
throw new Error('Ephemeris not initialized');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const result = this.ephem.eph.lun_eclipse_when(startJD, Constants.SEFLG_SWIEPH, 0, false);
|
|
130
|
+
|
|
131
|
+
if (result.error || !result.data || result.data.length < 1) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const eclipseType = this.getLunarEclipseType(result.flag);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
type: 'lunar',
|
|
139
|
+
date: this.ephem.julianDayToDate(result.data[0]),
|
|
140
|
+
eclipseType,
|
|
141
|
+
maxTime: this.ephem.julianDayToDate(result.data[0]),
|
|
142
|
+
};
|
|
143
|
+
} catch (e) {
|
|
144
|
+
logger.error(
|
|
145
|
+
'Lunar eclipse calculation failed',
|
|
146
|
+
ErrorCategory.CALCULATION,
|
|
147
|
+
e instanceof Error ? e : new Error(String(e))
|
|
148
|
+
);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the next eclipses (both solar and lunar) after a given date
|
|
155
|
+
*
|
|
156
|
+
* @param startJD - Julian Day to start searching from
|
|
157
|
+
* @returns Array of upcoming eclipses sorted by date
|
|
158
|
+
* @throws Error if ephemeris not initialized
|
|
159
|
+
*
|
|
160
|
+
* @remarks
|
|
161
|
+
* Finds the next solar and lunar eclipses. Returns them in
|
|
162
|
+
* chronological order. May return only one type if the other
|
|
163
|
+
* is too far in the future.
|
|
164
|
+
*/
|
|
165
|
+
async getNextEclipses(startJD: number): Promise<EclipseInfo[] | null> {
|
|
166
|
+
if (!this.ephem.eph) {
|
|
167
|
+
throw new Error('Ephemeris not initialized');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const solarEclipse = this.findNextSolarEclipse(startJD);
|
|
172
|
+
const lunarEclipse = this.findNextLunarEclipse(startJD);
|
|
173
|
+
|
|
174
|
+
const eclipses = await Promise.all([solarEclipse, lunarEclipse]);
|
|
175
|
+
|
|
176
|
+
const filteredEclipses = eclipses.filter((eclipse) => eclipse !== null);
|
|
177
|
+
|
|
178
|
+
if (filteredEclipses.length === 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return filteredEclipses.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
183
|
+
} catch (e) {
|
|
184
|
+
logger.error(
|
|
185
|
+
'Eclipse calculation failed',
|
|
186
|
+
ErrorCategory.CALCULATION,
|
|
187
|
+
e instanceof Error ? e : new Error(String(e))
|
|
188
|
+
);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get solar eclipse type from Swiss Ephemeris return code
|
|
195
|
+
*
|
|
196
|
+
* @param returnCode - Swiss Ephemeris solar eclipse return code
|
|
197
|
+
* @returns Human-readable eclipse type string
|
|
198
|
+
*
|
|
199
|
+
* @remarks
|
|
200
|
+
* Maps Swiss Ephemeris numeric codes to descriptive types.
|
|
201
|
+
* TODO: Should use constrained union types for better type safety.
|
|
202
|
+
*/
|
|
203
|
+
private getSolarEclipseType(returnCode: number): string {
|
|
204
|
+
if (returnCode & Constants.SE_ECL_TOTAL) return 'Total';
|
|
205
|
+
if (returnCode & Constants.SE_ECL_ANNULAR) return 'Annular';
|
|
206
|
+
if (returnCode & Constants.SE_ECL_PARTIAL) return 'Partial';
|
|
207
|
+
return 'Unknown';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get lunar eclipse type from Swiss Ephemeris return code
|
|
212
|
+
*
|
|
213
|
+
* @param returnCode - Swiss Ephemeris lunar eclipse return code
|
|
214
|
+
* @returns Human-readable eclipse type string
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* Maps Swiss Ephemeris numeric codes to descriptive types.
|
|
218
|
+
* TODO: Should use constrained union types for better type safety.
|
|
219
|
+
*/
|
|
220
|
+
private getLunarEclipseType(returnCode: number): string {
|
|
221
|
+
if (returnCode & Constants.SE_ECL_TOTAL) return 'Total';
|
|
222
|
+
if (returnCode & Constants.SE_ECL_PARTIAL) return 'Partial';
|
|
223
|
+
if (returnCode & Constants.SE_ECL_PENUMBRAL) return 'Penumbral';
|
|
224
|
+
return 'Unknown';
|
|
225
|
+
}
|
|
226
|
+
}
|