@bash-app/bash-common 30.201.0 → 30.208.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/dist/extendedSchemas.d.ts +40 -3
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/__tests__/eventTimezoneIana.test.d.ts +2 -0
- package/dist/utils/__tests__/eventTimezoneIana.test.d.ts.map +1 -0
- package/dist/utils/__tests__/eventTimezoneIana.test.js +60 -0
- package/dist/utils/__tests__/eventTimezoneIana.test.js.map +1 -0
- package/dist/utils/eventTimezoneIana.d.ts +38 -0
- package/dist/utils/eventTimezoneIana.d.ts.map +1 -0
- package/dist/utils/eventTimezoneIana.js +277 -0
- package/dist/utils/eventTimezoneIana.js.map +1 -0
- package/dist/utils/service/serviceUtils.d.ts +8 -0
- package/dist/utils/service/serviceUtils.d.ts.map +1 -1
- package/dist/utils/service/serviceUtils.js +16 -0
- package/dist/utils/service/serviceUtils.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +37 -0
- package/src/extendedSchemas.ts +45 -2
- package/src/index.ts +241 -7
- package/src/utils/__tests__/eventTimezoneIana.test.ts +92 -0
- package/src/utils/eventTimezoneIana.ts +331 -0
- package/src/utils/service/serviceUtils.ts +32 -0
package/src/index.ts
CHANGED
|
@@ -4,16 +4,238 @@ export * from "./extendedSchemas.js";
|
|
|
4
4
|
export * from "./bashFeedTypes.js";
|
|
5
5
|
export * from "./membershipDefinitions.js";
|
|
6
6
|
|
|
7
|
-
// Re-export
|
|
7
|
+
// Re-export ALL Prisma enums as values (usable as runtime constants, e.g. ServiceTypes.Entertainment)
|
|
8
|
+
// Excludes enums already re-exported by definitions.ts and membershipDefinitions.ts:
|
|
9
|
+
// definitions.ts: YearsOfExperience, EntertainmentServiceType, MusicGenreType, ServiceTypes,
|
|
10
|
+
// CompetitionType, JudgingType, BracketType
|
|
11
|
+
// membershipDefinitions.ts: MembershipTier, ReferralTier, CreditTransactionType
|
|
12
|
+
export {
|
|
13
|
+
$Enums,
|
|
14
|
+
AISpecialistFormat,
|
|
15
|
+
AVTechnicianFormat,
|
|
16
|
+
AgeRange,
|
|
17
|
+
AmbashadorApplicationStatus,
|
|
18
|
+
AudioVisualSupportSubType,
|
|
19
|
+
AuditAction,
|
|
20
|
+
AuditCategory,
|
|
21
|
+
AuditSeverity,
|
|
22
|
+
BalloonArtFormat,
|
|
23
|
+
BalloonsFormat,
|
|
24
|
+
BartenderFormat,
|
|
25
|
+
BashAssociationType,
|
|
26
|
+
BashAvailabilityWindow,
|
|
27
|
+
BashEventDressTags,
|
|
28
|
+
BashEventSource,
|
|
29
|
+
BashEventType,
|
|
30
|
+
BashEventVibeTags,
|
|
31
|
+
BashFeedReportReason,
|
|
32
|
+
BashStatus,
|
|
33
|
+
BeverageCartFormat,
|
|
34
|
+
BirthdayCityResourceSuggestionStatus,
|
|
35
|
+
BirthdayCityResourceType,
|
|
36
|
+
BirthdayDealSuggestionStatus,
|
|
37
|
+
BirthdayFreebieCategory,
|
|
38
|
+
BirthdayFreebieExpirationType,
|
|
39
|
+
BirthdayFreebieSector,
|
|
40
|
+
BirthdayFreebieSource,
|
|
41
|
+
BirthdayOfferExpirationType,
|
|
42
|
+
BirthdayOfferQualityTier,
|
|
43
|
+
BirthdayOfferSourceType,
|
|
44
|
+
BookkeepersFormat,
|
|
45
|
+
CPAsFormat,
|
|
46
|
+
CateringFormat,
|
|
47
|
+
CelebrityAppearanceFormat,
|
|
48
|
+
CenterpiecesFormat,
|
|
49
|
+
ChairsFormat,
|
|
50
|
+
ClaimRequestStatus,
|
|
51
|
+
CleaningSubType,
|
|
52
|
+
ClubAdminRole,
|
|
53
|
+
ClubMemberStatus,
|
|
54
|
+
ConnectionStatus,
|
|
55
|
+
ConsentType,
|
|
56
|
+
ConsultantFormat,
|
|
57
|
+
CreditSourceType,
|
|
58
|
+
CreditTransactionStatus,
|
|
59
|
+
CrewHandFormat,
|
|
60
|
+
CustomSignageFormat,
|
|
61
|
+
DJLightingFormat,
|
|
62
|
+
DanceFloorFormat,
|
|
63
|
+
DayOfWeek,
|
|
64
|
+
DecorAndDesignSubType,
|
|
65
|
+
DecoratorFormat,
|
|
66
|
+
DeepCleaningFormat,
|
|
67
|
+
DessertBarFormat,
|
|
68
|
+
DiscountScope,
|
|
69
|
+
DiscountSourceType,
|
|
70
|
+
DroneShotsFormat,
|
|
71
|
+
Education,
|
|
72
|
+
EmceeFormat,
|
|
73
|
+
EntityType,
|
|
74
|
+
EventFormat,
|
|
75
|
+
EventManagerFormat,
|
|
76
|
+
EventPlannerFormat,
|
|
77
|
+
EventPlanningAndManagementSubType,
|
|
78
|
+
EventServiceType,
|
|
79
|
+
EventTemplateCategory,
|
|
80
|
+
ExhibitorBookingStatus,
|
|
81
|
+
FinancingSubType,
|
|
82
|
+
FloralFormat,
|
|
83
|
+
FoodAndBeverageSubType,
|
|
84
|
+
FoodCartStandFormat,
|
|
85
|
+
FoodTruckFormat,
|
|
86
|
+
FurnitureStylingFormat,
|
|
87
|
+
Gender,
|
|
88
|
+
GeneratorsFormat,
|
|
89
|
+
GoProWearerFormat,
|
|
90
|
+
GraphicDesignerFormat,
|
|
91
|
+
GreeterFormat,
|
|
92
|
+
GroupMemberStatus,
|
|
93
|
+
GroupVisibility,
|
|
94
|
+
GuestRegistrationFormat,
|
|
95
|
+
GuestShuttleFormat,
|
|
96
|
+
InfluencerFormat,
|
|
97
|
+
InteriorDesignerFormat,
|
|
98
|
+
InvestmentType,
|
|
99
|
+
InvestorFormat,
|
|
100
|
+
LawyersFormat,
|
|
101
|
+
LightCleaningFormat,
|
|
102
|
+
LightingFormat,
|
|
103
|
+
LightingTechFormat,
|
|
104
|
+
LimoFormat,
|
|
105
|
+
LinensFormat,
|
|
106
|
+
LongTermLoanFormat,
|
|
107
|
+
MediaEditorFormat,
|
|
108
|
+
MediaProductionAndCreativeSubType,
|
|
109
|
+
MediaType,
|
|
110
|
+
NotificationActionType,
|
|
111
|
+
NotificationPriority,
|
|
112
|
+
NotificationType,
|
|
113
|
+
Occupation,
|
|
114
|
+
OfferClaimStatus,
|
|
115
|
+
OfferDiscountType,
|
|
116
|
+
OrganizationType,
|
|
117
|
+
OtherSubType,
|
|
118
|
+
PASystemFormat,
|
|
119
|
+
ParkingManagementFormat,
|
|
120
|
+
ParticipantStatus,
|
|
121
|
+
PartyBusFormat,
|
|
122
|
+
PendingGiftStatus,
|
|
123
|
+
PhotoBoothFormat,
|
|
124
|
+
PhotographerFormat,
|
|
125
|
+
PredictionSource,
|
|
126
|
+
PredictionType,
|
|
127
|
+
PricingModel,
|
|
128
|
+
PricingType,
|
|
129
|
+
Privacy,
|
|
130
|
+
PrivateChefFormat,
|
|
131
|
+
PrizePaymentMethod,
|
|
132
|
+
PrizePaymentStatus,
|
|
133
|
+
PrizeType,
|
|
134
|
+
ProjectorSetupFormat,
|
|
135
|
+
PromoterFormat,
|
|
136
|
+
PromotionAndMarketingSubType,
|
|
137
|
+
RecurrenceEndsType,
|
|
138
|
+
RecurringFrequency,
|
|
139
|
+
RedemptionMethod,
|
|
140
|
+
RentalEquipmentSubType,
|
|
141
|
+
ReportStatus,
|
|
142
|
+
ReportingFormat,
|
|
143
|
+
RewardReportStatus,
|
|
144
|
+
ScoutTier,
|
|
145
|
+
SecurityFormat,
|
|
146
|
+
ServiceAddonCatalogKind,
|
|
147
|
+
ServiceAddonRequestStatus,
|
|
148
|
+
ServiceAddonStatus,
|
|
149
|
+
ServiceAnalyticsEvent,
|
|
150
|
+
ServiceBookingFeeType,
|
|
151
|
+
ServiceBookingRateOption,
|
|
152
|
+
ServiceBookingStatus,
|
|
153
|
+
ServiceBookingType,
|
|
154
|
+
ServiceCancellationPolicy,
|
|
155
|
+
ServiceCategoryRequestStatus,
|
|
156
|
+
ServiceCondition,
|
|
157
|
+
ServiceIncludeResponse,
|
|
158
|
+
ServiceIncludeStatus,
|
|
159
|
+
ServicePreference,
|
|
160
|
+
ServiceRatePricingType,
|
|
161
|
+
ServiceRateType,
|
|
162
|
+
ServiceStatus,
|
|
163
|
+
ServiceSubscriptionStatus,
|
|
164
|
+
Sex,
|
|
165
|
+
ShortTermLoanFormat,
|
|
166
|
+
SocialMediaPlatform,
|
|
167
|
+
SoundEngineerFormat,
|
|
168
|
+
SpecialOfferType,
|
|
169
|
+
SponsorBookingStatus,
|
|
170
|
+
SponsorTier,
|
|
171
|
+
SponsorType,
|
|
172
|
+
StaffingSubType,
|
|
173
|
+
StageTechFormat,
|
|
174
|
+
StagingFormat,
|
|
175
|
+
TableStylingFormat,
|
|
176
|
+
TablesFormat,
|
|
177
|
+
TaskStatus,
|
|
178
|
+
TentsFormat,
|
|
179
|
+
ThankYouCardsFormat,
|
|
180
|
+
TicketStatus,
|
|
181
|
+
TransferRequestStatus,
|
|
182
|
+
TransportationSubType,
|
|
183
|
+
UserIntent,
|
|
184
|
+
UserRole,
|
|
185
|
+
UserStatus,
|
|
186
|
+
UserSubscriptionStatus,
|
|
187
|
+
ValetFormat,
|
|
188
|
+
ValetServiceFormat,
|
|
189
|
+
VendedProductType,
|
|
190
|
+
VendorBidStatus,
|
|
191
|
+
VendorBookingStatus,
|
|
192
|
+
VendorServiceType,
|
|
193
|
+
VenuePricingPlan,
|
|
194
|
+
VideographerFormat,
|
|
195
|
+
VirtualAssistantFormat,
|
|
196
|
+
VisibilityPreference,
|
|
197
|
+
VolunteerServiceStatus,
|
|
198
|
+
VoteType,
|
|
199
|
+
VoucherStatus,
|
|
200
|
+
} from "@prisma/client";
|
|
201
|
+
|
|
202
|
+
// Re-export Prisma model types (type-only, no runtime value)
|
|
8
203
|
export type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
204
|
+
AmountOfGuests,
|
|
205
|
+
AssociatedBash,
|
|
206
|
+
AssociatedService,
|
|
12
207
|
BashEvent,
|
|
208
|
+
BashEventPromoCode,
|
|
13
209
|
BashFeedPost,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
210
|
+
BlockedUser,
|
|
211
|
+
CompetitionParticipant,
|
|
212
|
+
Contact,
|
|
213
|
+
CustomBashEventType,
|
|
214
|
+
DocumentID,
|
|
215
|
+
EventTask,
|
|
216
|
+
Exhibitor,
|
|
217
|
+
GoogleReview,
|
|
218
|
+
Invitation,
|
|
219
|
+
Link,
|
|
220
|
+
Media,
|
|
221
|
+
Organization,
|
|
222
|
+
Recurrence,
|
|
223
|
+
Service,
|
|
224
|
+
ServiceAddon,
|
|
225
|
+
ServiceBooking,
|
|
226
|
+
ServiceRate,
|
|
227
|
+
SocialMediaProfile,
|
|
228
|
+
Sponsor,
|
|
229
|
+
StripeAccount,
|
|
230
|
+
TargetAudience,
|
|
231
|
+
TaskInvitation,
|
|
232
|
+
Ticket,
|
|
233
|
+
TicketTier,
|
|
234
|
+
User,
|
|
235
|
+
UserPreferences,
|
|
236
|
+
UserSubscription,
|
|
237
|
+
Vendor,
|
|
238
|
+
} from "@prisma/client";
|
|
17
239
|
|
|
18
240
|
export * from "./utils/addressUtils.js";
|
|
19
241
|
export * from "./utils/apiUtils.js";
|
|
@@ -22,6 +244,18 @@ export * from "./utils/awsS3Utils.js";
|
|
|
22
244
|
export * from "./utils/bashPointsPaymentUtils.js";
|
|
23
245
|
export * from "./utils/contentFilterUtils.js";
|
|
24
246
|
export * from "./utils/dateTimeUtils.js";
|
|
247
|
+
export {
|
|
248
|
+
getIanaTimezoneFromUsStateCode,
|
|
249
|
+
isValidIanaTimezone,
|
|
250
|
+
getIanaTimezoneFromCoordinates,
|
|
251
|
+
getIanaTimezoneFromLocationStringHints,
|
|
252
|
+
inferEventTimezoneIana,
|
|
253
|
+
resolveEventTimezoneIanaForDisplay,
|
|
254
|
+
} from "./utils/eventTimezoneIana.js";
|
|
255
|
+
export type {
|
|
256
|
+
InferEventTimezoneArgs,
|
|
257
|
+
ResolveEventTimezoneForDisplayArgs,
|
|
258
|
+
} from "./utils/eventTimezoneIana.js";
|
|
25
259
|
export * from "./utils/displayDateUtils.js";
|
|
26
260
|
export * from "./utils/discountEngine/bestPriceResolver.js";
|
|
27
261
|
export * from "./utils/discountEngine/eligibilityValidator.js";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getIanaTimezoneFromCoordinates,
|
|
3
|
+
inferEventTimezoneIana,
|
|
4
|
+
resolveEventTimezoneIanaForDisplay,
|
|
5
|
+
} from "../eventTimezoneIana.js";
|
|
6
|
+
|
|
7
|
+
describe("eventTimezoneIana", () => {
|
|
8
|
+
describe("getIanaTimezoneFromCoordinates", () => {
|
|
9
|
+
it("returns Pacific for San Francisco", () => {
|
|
10
|
+
expect(getIanaTimezoneFromCoordinates(37.7749, -122.4194)).toBe(
|
|
11
|
+
"America/Los_Angeles"
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns Mountain for Salt Lake City area", () => {
|
|
16
|
+
expect(getIanaTimezoneFromCoordinates(39.7392, -111.0)).toBe(
|
|
17
|
+
"America/Denver"
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns Central for Chicago", () => {
|
|
22
|
+
expect(getIanaTimezoneFromCoordinates(41.8781, -87.6298)).toBe(
|
|
23
|
+
"America/Chicago"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns Eastern for New York", () => {
|
|
28
|
+
expect(getIanaTimezoneFromCoordinates(40.7128, -74.006)).toBe(
|
|
29
|
+
"America/New_York"
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns Phoenix for Tempe AZ", () => {
|
|
34
|
+
expect(getIanaTimezoneFromCoordinates(33.4484, -111.94)).toBe(
|
|
35
|
+
"America/Phoenix"
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns null when coordinates are missing", () => {
|
|
40
|
+
expect(getIanaTimezoneFromCoordinates(null, -122)).toBeNull();
|
|
41
|
+
expect(getIanaTimezoneFromCoordinates(40, undefined)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns null for Null Island (0, 0)", () => {
|
|
45
|
+
expect(getIanaTimezoneFromCoordinates(0, 0)).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("inferEventTimezoneIana", () => {
|
|
50
|
+
it("uses US state when coordinates are missing", () => {
|
|
51
|
+
expect(
|
|
52
|
+
inferEventTimezoneIana({
|
|
53
|
+
state: "UT",
|
|
54
|
+
country: "USA",
|
|
55
|
+
})
|
|
56
|
+
).toBe("America/Denver");
|
|
57
|
+
expect(
|
|
58
|
+
inferEventTimezoneIana({
|
|
59
|
+
state: "AZ",
|
|
60
|
+
country: "USA",
|
|
61
|
+
})
|
|
62
|
+
).toBe("America/Phoenix");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("falls back to UTC when nothing matches", () => {
|
|
66
|
+
expect(inferEventTimezoneIana({})).toBe("UTC");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("resolveEventTimezoneIanaForDisplay", () => {
|
|
71
|
+
it("prefers valid stored timezone", () => {
|
|
72
|
+
expect(
|
|
73
|
+
resolveEventTimezoneIanaForDisplay({
|
|
74
|
+
timezone: "America/Los_Angeles",
|
|
75
|
+
state: "UT",
|
|
76
|
+
country: "USA",
|
|
77
|
+
})
|
|
78
|
+
).toBe("America/Los_Angeles");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("infers from city/state when timezone is missing", () => {
|
|
82
|
+
expect(
|
|
83
|
+
resolveEventTimezoneIanaForDisplay({
|
|
84
|
+
timezone: null,
|
|
85
|
+
city: "Ogden",
|
|
86
|
+
state: "UT",
|
|
87
|
+
country: "United States",
|
|
88
|
+
})
|
|
89
|
+
).toBe("America/Denver");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
|
|
3
|
+
/** Primary IANA zone for each US state + DC (contiguous conventions; AZ/HI/AK use canonical zones). */
|
|
4
|
+
const US_STATE_CODE_TO_IANA: Record<string, string> = {
|
|
5
|
+
AL: "America/Chicago",
|
|
6
|
+
AK: "America/Anchorage",
|
|
7
|
+
AZ: "America/Phoenix",
|
|
8
|
+
AR: "America/Chicago",
|
|
9
|
+
CA: "America/Los_Angeles",
|
|
10
|
+
CO: "America/Denver",
|
|
11
|
+
CT: "America/New_York",
|
|
12
|
+
DE: "America/New_York",
|
|
13
|
+
DC: "America/New_York",
|
|
14
|
+
FL: "America/New_York",
|
|
15
|
+
GA: "America/New_York",
|
|
16
|
+
HI: "Pacific/Honolulu",
|
|
17
|
+
ID: "America/Boise",
|
|
18
|
+
IL: "America/Chicago",
|
|
19
|
+
IN: "America/Indiana/Indianapolis",
|
|
20
|
+
IA: "America/Chicago",
|
|
21
|
+
KS: "America/Chicago",
|
|
22
|
+
KY: "America/New_York",
|
|
23
|
+
LA: "America/Chicago",
|
|
24
|
+
ME: "America/New_York",
|
|
25
|
+
MD: "America/New_York",
|
|
26
|
+
MA: "America/New_York",
|
|
27
|
+
MI: "America/Detroit",
|
|
28
|
+
MN: "America/Chicago",
|
|
29
|
+
MS: "America/Chicago",
|
|
30
|
+
MO: "America/Chicago",
|
|
31
|
+
MT: "America/Denver",
|
|
32
|
+
NE: "America/Chicago",
|
|
33
|
+
NV: "America/Los_Angeles",
|
|
34
|
+
NH: "America/New_York",
|
|
35
|
+
NJ: "America/New_York",
|
|
36
|
+
NM: "America/Denver",
|
|
37
|
+
NY: "America/New_York",
|
|
38
|
+
NC: "America/New_York",
|
|
39
|
+
ND: "America/Chicago",
|
|
40
|
+
OH: "America/New_York",
|
|
41
|
+
OK: "America/Chicago",
|
|
42
|
+
OR: "America/Los_Angeles",
|
|
43
|
+
PA: "America/New_York",
|
|
44
|
+
RI: "America/New_York",
|
|
45
|
+
SC: "America/New_York",
|
|
46
|
+
SD: "America/Chicago",
|
|
47
|
+
TN: "America/Chicago",
|
|
48
|
+
TX: "America/Chicago",
|
|
49
|
+
UT: "America/Denver",
|
|
50
|
+
VT: "America/New_York",
|
|
51
|
+
VA: "America/New_York",
|
|
52
|
+
WA: "America/Los_Angeles",
|
|
53
|
+
WV: "America/New_York",
|
|
54
|
+
WI: "America/Chicago",
|
|
55
|
+
WY: "America/Denver",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function isUsCountry(country: string | null | undefined): boolean {
|
|
59
|
+
if (!country?.trim()) return true;
|
|
60
|
+
const c = country.trim().toLowerCase();
|
|
61
|
+
return (
|
|
62
|
+
c === "us" ||
|
|
63
|
+
c === "usa" ||
|
|
64
|
+
c === "united states" ||
|
|
65
|
+
c === "united states of america"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Maps a US state code (e.g. "UT") to IANA when country is US/unknown.
|
|
71
|
+
* Returns null if state is missing or not a 2-letter US code.
|
|
72
|
+
*/
|
|
73
|
+
export function getIanaTimezoneFromUsStateCode(
|
|
74
|
+
state: string | null | undefined,
|
|
75
|
+
country: string | null | undefined
|
|
76
|
+
): string | null {
|
|
77
|
+
if (!state?.trim()) return null;
|
|
78
|
+
const code = state.trim().toUpperCase();
|
|
79
|
+
if (code.length !== 2) return null;
|
|
80
|
+
if (!isUsCountry(country)) return null;
|
|
81
|
+
return US_STATE_CODE_TO_IANA[code] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isValidIanaTimezone(timezone: string | null | undefined): boolean {
|
|
85
|
+
const t = timezone?.trim();
|
|
86
|
+
if (!t) return false;
|
|
87
|
+
return DateTime.now().setZone(t).isValid;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Longitude/latitude → IANA for common regions. Returns null when coords are missing
|
|
92
|
+
* or no region matches (caller should fall back to address hints or UTC).
|
|
93
|
+
*/
|
|
94
|
+
export function getIanaTimezoneFromCoordinates(
|
|
95
|
+
latitude: number | null | undefined,
|
|
96
|
+
longitude: number | null | undefined
|
|
97
|
+
): string | null {
|
|
98
|
+
if (
|
|
99
|
+
latitude === null ||
|
|
100
|
+
latitude === undefined ||
|
|
101
|
+
longitude === null ||
|
|
102
|
+
longitude === undefined
|
|
103
|
+
) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Null Island / indeterminate — do not guess a zone from (0, 0).
|
|
111
|
+
if (latitude === 0 && longitude === 0) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Hawaii first
|
|
116
|
+
if (
|
|
117
|
+
longitude >= -160 &&
|
|
118
|
+
longitude <= -154 &&
|
|
119
|
+
latitude >= 18.9 &&
|
|
120
|
+
latitude <= 22.5
|
|
121
|
+
) {
|
|
122
|
+
return "Pacific/Honolulu";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Alaska / Aleutian
|
|
126
|
+
if (longitude >= -180 && longitude <= -140) {
|
|
127
|
+
if (longitude <= -169.5) {
|
|
128
|
+
return "America/Adak";
|
|
129
|
+
}
|
|
130
|
+
return "America/Anchorage";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Continental US — fixed west-to-east bands (avoids misclassifying Mountain vs Central)
|
|
134
|
+
if (latitude >= 18.9 && latitude <= 71.5388) {
|
|
135
|
+
if (longitude <= -114) {
|
|
136
|
+
return "America/Los_Angeles";
|
|
137
|
+
}
|
|
138
|
+
if (longitude > -114 && longitude <= -102) {
|
|
139
|
+
if (
|
|
140
|
+
latitude >= 31.3 &&
|
|
141
|
+
latitude <= 37.0 &&
|
|
142
|
+
longitude >= -114.8 &&
|
|
143
|
+
longitude <= -109.0
|
|
144
|
+
) {
|
|
145
|
+
return "America/Phoenix";
|
|
146
|
+
}
|
|
147
|
+
return "America/Denver";
|
|
148
|
+
}
|
|
149
|
+
if (longitude > -102 && longitude <= -87) {
|
|
150
|
+
return "America/Chicago";
|
|
151
|
+
}
|
|
152
|
+
if (longitude > -87 && longitude <= -67) {
|
|
153
|
+
return "America/New_York";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Europe / Africa (coarse)
|
|
158
|
+
if (longitude >= -10 && longitude <= 40) {
|
|
159
|
+
if (longitude >= -1 && longitude <= 1) {
|
|
160
|
+
return "Europe/London";
|
|
161
|
+
}
|
|
162
|
+
if (longitude >= 1 && longitude <= 3) {
|
|
163
|
+
return "Europe/Paris";
|
|
164
|
+
}
|
|
165
|
+
if (longitude >= 13 && longitude <= 15) {
|
|
166
|
+
return "Europe/Berlin";
|
|
167
|
+
}
|
|
168
|
+
if (longitude >= 20 && longitude <= 30) {
|
|
169
|
+
return "Europe/Helsinki";
|
|
170
|
+
}
|
|
171
|
+
} else if (longitude >= 100 && longitude <= 180) {
|
|
172
|
+
if (longitude >= 139 && longitude <= 141) {
|
|
173
|
+
return "Asia/Tokyo";
|
|
174
|
+
}
|
|
175
|
+
if (longitude >= 116 && longitude <= 118) {
|
|
176
|
+
return "Asia/Shanghai";
|
|
177
|
+
}
|
|
178
|
+
if (longitude >= 150 && longitude <= 155) {
|
|
179
|
+
return "Australia/Sydney";
|
|
180
|
+
}
|
|
181
|
+
if (longitude >= 115 && longitude <= 120) {
|
|
182
|
+
return "Australia/Perth";
|
|
183
|
+
}
|
|
184
|
+
} else if (longitude >= -80 && longitude <= -30) {
|
|
185
|
+
if (latitude !== null && latitude !== undefined && latitude >= 45) {
|
|
186
|
+
if (longitude >= -80 && longitude <= -74) {
|
|
187
|
+
return "America/Toronto";
|
|
188
|
+
}
|
|
189
|
+
if (longitude >= -110 && longitude <= -100) {
|
|
190
|
+
return "America/Winnipeg";
|
|
191
|
+
}
|
|
192
|
+
if (longitude >= -125 && longitude <= -115) {
|
|
193
|
+
return "America/Vancouver";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Substring hints for free-text location (pipe-delimited or legacy). Avoids single-letter
|
|
203
|
+
* state codes like "ut" that false-positive inside unrelated words.
|
|
204
|
+
*/
|
|
205
|
+
export function getIanaTimezoneFromLocationStringHints(
|
|
206
|
+
location: string | null | undefined
|
|
207
|
+
): string | null {
|
|
208
|
+
if (!location?.trim()) return null;
|
|
209
|
+
const locationLower = location.toLowerCase();
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
locationLower.includes("new jersey") ||
|
|
213
|
+
/\bnj\b/.test(locationLower) ||
|
|
214
|
+
locationLower.includes("new york") ||
|
|
215
|
+
/\bny\b/.test(locationLower) ||
|
|
216
|
+
locationLower.includes("massachusetts") ||
|
|
217
|
+
/\bma\b/.test(locationLower) ||
|
|
218
|
+
locationLower.includes("florida") ||
|
|
219
|
+
/\bfl\b/.test(locationLower) ||
|
|
220
|
+
locationLower.includes("georgia") ||
|
|
221
|
+
/\bga\b/.test(locationLower) ||
|
|
222
|
+
locationLower.includes("keansburg")
|
|
223
|
+
) {
|
|
224
|
+
return "America/New_York";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
locationLower.includes("california") ||
|
|
229
|
+
locationLower.includes(", ca") ||
|
|
230
|
+
locationLower.endsWith(" ca") ||
|
|
231
|
+
locationLower.includes("washington") ||
|
|
232
|
+
locationLower.includes(", wa") ||
|
|
233
|
+
locationLower.endsWith(" wa") ||
|
|
234
|
+
locationLower.includes("oregon") ||
|
|
235
|
+
locationLower.includes("nevada") ||
|
|
236
|
+
/\bnv\b/.test(locationLower)
|
|
237
|
+
) {
|
|
238
|
+
return "America/Los_Angeles";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
locationLower.includes("texas") ||
|
|
243
|
+
/\btx\b/.test(locationLower) ||
|
|
244
|
+
locationLower.includes("illinois") ||
|
|
245
|
+
/\bil\b/.test(locationLower) ||
|
|
246
|
+
locationLower.includes("chicago")
|
|
247
|
+
) {
|
|
248
|
+
return "America/Chicago";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (locationLower.includes("arizona") || /\baz\b/.test(locationLower)) {
|
|
252
|
+
return "America/Phoenix";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
locationLower.includes("colorado") ||
|
|
257
|
+
locationLower.includes(", co") ||
|
|
258
|
+
locationLower.endsWith(" co") ||
|
|
259
|
+
locationLower.includes("utah") ||
|
|
260
|
+
locationLower.includes(", ut") ||
|
|
261
|
+
locationLower.includes(" ut,") ||
|
|
262
|
+
locationLower.endsWith(" ut")
|
|
263
|
+
) {
|
|
264
|
+
return "America/Denver";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (locationLower.includes("london") || locationLower.includes("uk")) {
|
|
268
|
+
return "Europe/London";
|
|
269
|
+
}
|
|
270
|
+
if (locationLower.includes("paris") || locationLower.includes("france")) {
|
|
271
|
+
return "Europe/Paris";
|
|
272
|
+
}
|
|
273
|
+
if (locationLower.includes("tokyo") || locationLower.includes("japan")) {
|
|
274
|
+
return "Asia/Tokyo";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export type InferEventTimezoneArgs = {
|
|
281
|
+
location?: string | null;
|
|
282
|
+
latitude?: number | null;
|
|
283
|
+
longitude?: number | null;
|
|
284
|
+
city?: string | null;
|
|
285
|
+
state?: string | null;
|
|
286
|
+
country?: string | null;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Infer IANA from coordinates, then US state, then location/city string hints.
|
|
291
|
+
* Never uses the viewer's device timezone. Falls back to UTC.
|
|
292
|
+
*/
|
|
293
|
+
export function inferEventTimezoneIana(args: InferEventTimezoneArgs): string {
|
|
294
|
+
const fromCoords = getIanaTimezoneFromCoordinates(
|
|
295
|
+
args.latitude,
|
|
296
|
+
args.longitude
|
|
297
|
+
);
|
|
298
|
+
if (fromCoords) return fromCoords;
|
|
299
|
+
|
|
300
|
+
const fromState = getIanaTimezoneFromUsStateCode(args.state, args.country);
|
|
301
|
+
if (fromState) return fromState;
|
|
302
|
+
|
|
303
|
+
const fromLoc = getIanaTimezoneFromLocationStringHints(args.location);
|
|
304
|
+
if (fromLoc) return fromLoc;
|
|
305
|
+
|
|
306
|
+
const cityLine = [args.city, args.state, args.country]
|
|
307
|
+
.filter((p): p is string => !!p?.trim())
|
|
308
|
+
.join(", ");
|
|
309
|
+
const fromCityLine = getIanaTimezoneFromLocationStringHints(cityLine);
|
|
310
|
+
if (fromCityLine) return fromCityLine;
|
|
311
|
+
|
|
312
|
+
return "UTC";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export type ResolveEventTimezoneForDisplayArgs = InferEventTimezoneArgs & {
|
|
316
|
+
timezone?: string | null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* For public surfaces (e.g. BashCard): prefer a valid stored IANA, then infer from geography.
|
|
321
|
+
* Never uses the viewer's device timezone.
|
|
322
|
+
*/
|
|
323
|
+
export function resolveEventTimezoneIanaForDisplay(
|
|
324
|
+
args: ResolveEventTimezoneForDisplayArgs
|
|
325
|
+
): string {
|
|
326
|
+
const stored = args.timezone?.trim();
|
|
327
|
+
if (stored && isValidIanaTimezone(stored)) {
|
|
328
|
+
return stored;
|
|
329
|
+
}
|
|
330
|
+
return inferEventTimezoneIana(args);
|
|
331
|
+
}
|