UNPKG

10.4 kBPlain TextView Raw
1import { Component, ParsedComponents, ParsedResult, ParsingReference } from "./types";
2
3import quarterOfYear from "dayjs/plugin/quarterOfYear";
4import dayjs, { QUnitType } from "dayjs";
5import { assignSimilarDate, assignSimilarTime, implySimilarTime } from "./utils/dayjs";
6import { toTimezoneOffset } from "./timezone";
7dayjs.extend(quarterOfYear);
8
9export class ReferenceWithTimezone {
10 readonly instant: Date;
11 readonly timezoneOffset?: number | null;
12
13 constructor(input?: ParsingReference | Date) {
14 input = input ?? new Date();
15 if (input instanceof Date) {
16 this.instant = input;
17 } else {
18 this.instant = input.instant ?? new Date();
19 this.timezoneOffset = toTimezoneOffset(input.timezone, this.instant);
20 }
21 }
22
23 /**
24 * Returns a JS date (system timezone) with the { year, month, day, hour, minute, second } equal to the reference.
25 * The output's instant is NOT the reference's instant when the reference's and system's timezone are different.
26 */
27 getDateWithAdjustedTimezone() {
28 return new Date(this.instant.getTime() + this.getSystemTimezoneAdjustmentMinute(this.instant) * 60000);
29 }
30
31 /**
32 * Returns the number minutes difference between the JS date's timezone and the reference timezone.
33 * @param date
34 * @param overrideTimezoneOffset
35 */
36 getSystemTimezoneAdjustmentMinute(date?: Date, overrideTimezoneOffset?: number): number {
37 if (!date || date.getTime() < 0) {
38 // Javascript date timezone calculation got effect when the time epoch < 0
39 // e.g. new Date('Tue Feb 02 1300 00:00:00 GMT+0900 (JST)') => Tue Feb 02 1300 00:18:59 GMT+0918 (JST)
40 date = new Date();
41 }
42
43 const currentTimezoneOffset = -date.getTimezoneOffset();
44 const targetTimezoneOffset = overrideTimezoneOffset ?? this.timezoneOffset ?? currentTimezoneOffset;
45 return currentTimezoneOffset - targetTimezoneOffset;
46 }
47}
48
49export class ParsingComponents implements ParsedComponents {
50 private knownValues: { [c in Component]?: number };
51 private impliedValues: { [c in Component]?: number };
52 private reference: ReferenceWithTimezone;
53 private _tags = new Set<string>();
54
55 constructor(reference: ReferenceWithTimezone, knownComponents?: { [c in Component]?: number }) {
56 this.reference = reference;
57 this.knownValues = {};
58 this.impliedValues = {};
59 if (knownComponents) {
60 for (const key in knownComponents) {
61 this.knownValues[key as Component] = knownComponents[key as Component];
62 }
63 }
64
65 const refDayJs = dayjs(reference.instant);
66 this.imply("day", refDayJs.date());
67 this.imply("month", refDayJs.month() + 1);
68 this.imply("year", refDayJs.year());
69 this.imply("hour", 12);
70 this.imply("minute", 0);
71 this.imply("second", 0);
72 this.imply("millisecond", 0);
73 }
74
75 get(component: Component): number | null {
76 if (component in this.knownValues) {
77 return this.knownValues[component];
78 }
79
80 if (component in this.impliedValues) {
81 return this.impliedValues[component];
82 }
83
84 return null;
85 }
86
87 isCertain(component: Component): boolean {
88 return component in this.knownValues;
89 }
90
91 getCertainComponents(): Array<Component> {
92 return Object.keys(this.knownValues) as Array<Component>;
93 }
94
95 imply(component: Component, value: number): ParsingComponents {
96 if (component in this.knownValues) {
97 return this;
98 }
99 this.impliedValues[component] = value;
100 return this;
101 }
102
103 assign(component: Component, value: number): ParsingComponents {
104 this.knownValues[component] = value;
105 delete this.impliedValues[component];
106 return this;
107 }
108
109 delete(component: Component) {
110 delete this.knownValues[component];
111 delete this.impliedValues[component];
112 }
113
114 clone(): ParsingComponents {
115 const component = new ParsingComponents(this.reference);
116 component.knownValues = {};
117 component.impliedValues = {};
118
119 for (const key in this.knownValues) {
120 component.knownValues[key as Component] = this.knownValues[key as Component];
121 }
122
123 for (const key in this.impliedValues) {
124 component.impliedValues[key as Component] = this.impliedValues[key as Component];
125 }
126
127 return component;
128 }
129
130 isOnlyDate(): boolean {
131 return !this.isCertain("hour") && !this.isCertain("minute") && !this.isCertain("second");
132 }
133
134 isOnlyTime(): boolean {
135 return (
136 !this.isCertain("weekday") && !this.isCertain("day") && !this.isCertain("month") && !this.isCertain("year")
137 );
138 }
139
140 isOnlyWeekdayComponent(): boolean {
141 return this.isCertain("weekday") && !this.isCertain("day") && !this.isCertain("month");
142 }
143
144 isDateWithUnknownYear(): boolean {
145 return this.isCertain("month") && !this.isCertain("year");
146 }
147
148 isValidDate(): boolean {
149 const date = this.dateWithoutTimezoneAdjustment();
150
151 if (date.getFullYear() !== this.get("year")) return false;
152 if (date.getMonth() !== this.get("month") - 1) return false;
153 if (date.getDate() !== this.get("day")) return false;
154 if (this.get("hour") != null && date.getHours() != this.get("hour")) return false;
155 if (this.get("minute") != null && date.getMinutes() != this.get("minute")) return false;
156
157 return true;
158 }
159
160 toString() {
161 return `[ParsingComponents {
162 tags: ${JSON.stringify(Array.from(this._tags).sort())},
163 knownValues: ${JSON.stringify(this.knownValues)},
164 impliedValues: ${JSON.stringify(this.impliedValues)}},
165 reference: ${JSON.stringify(this.reference)}]`;
166 }
167
168 dayjs() {
169 return dayjs(this.date());
170 }
171
172 date(): Date {
173 const date = this.dateWithoutTimezoneAdjustment();
174 const timezoneAdjustment = this.reference.getSystemTimezoneAdjustmentMinute(date, this.get("timezoneOffset"));
175 return new Date(date.getTime() + timezoneAdjustment * 60000);
176 }
177
178 addTag(tag: string): ParsingComponents {
179 this._tags.add(tag);
180 return this;
181 }
182
183 addTags(tags: string[] | Set<string>): ParsingComponents {
184 for (const tag of tags) {
185 this._tags.add(tag);
186 }
187 return this;
188 }
189
190 tags(): Set<string> {
191 return new Set(this._tags);
192 }
193
194 private dateWithoutTimezoneAdjustment() {
195 const date = new Date(
196 this.get("year"),
197 this.get("month") - 1,
198 this.get("day"),
199 this.get("hour"),
200 this.get("minute"),
201 this.get("second"),
202 this.get("millisecond")
203 );
204
205 date.setFullYear(this.get("year"));
206 return date;
207 }
208
209 static createRelativeFromReference(
210 reference: ReferenceWithTimezone,
211 fragments: { [c in QUnitType]?: number }
212 ): ParsingComponents {
213 let date = dayjs(reference.instant);
214 for (const key in fragments) {
215 date = date.add(fragments[key as QUnitType], key as QUnitType);
216 }
217
218 const components = new ParsingComponents(reference);
219 if (fragments["hour"] || fragments["minute"] || fragments["second"]) {
220 assignSimilarTime(components, date);
221 assignSimilarDate(components, date);
222 if (reference.timezoneOffset !== null) {
223 components.assign("timezoneOffset", -reference.instant.getTimezoneOffset());
224 }
225 } else {
226 implySimilarTime(components, date);
227 if (reference.timezoneOffset !== null) {
228 components.imply("timezoneOffset", -reference.instant.getTimezoneOffset());
229 }
230
231 if (fragments["d"]) {
232 components.assign("day", date.date());
233 components.assign("month", date.month() + 1);
234 components.assign("year", date.year());
235 } else if (fragments["week"]) {
236 components.assign("day", date.date());
237 components.assign("month", date.month() + 1);
238 components.assign("year", date.year());
239 components.imply("weekday", date.day());
240 } else {
241 components.imply("day", date.date());
242 if (fragments["month"]) {
243 components.assign("month", date.month() + 1);
244 components.assign("year", date.year());
245 } else {
246 components.imply("month", date.month() + 1);
247 if (fragments["year"]) {
248 components.assign("year", date.year());
249 } else {
250 components.imply("year", date.year());
251 }
252 }
253 }
254 }
255
256 return components;
257 }
258}
259
260export class ParsingResult implements ParsedResult {
261 refDate: Date;
262 index: number;
263 text: string;
264
265 reference: ReferenceWithTimezone;
266
267 start: ParsingComponents;
268 end?: ParsingComponents;
269
270 constructor(
271 reference: ReferenceWithTimezone,
272 index: number,
273 text: string,
274 start?: ParsingComponents,
275 end?: ParsingComponents
276 ) {
277 this.reference = reference;
278 this.refDate = reference.instant;
279 this.index = index;
280 this.text = text;
281 this.start = start || new ParsingComponents(reference);
282 this.end = end;
283 }
284
285 clone() {
286 const result = new ParsingResult(this.reference, this.index, this.text);
287 result.start = this.start ? this.start.clone() : null;
288 result.end = this.end ? this.end.clone() : null;
289 return result;
290 }
291
292 date(): Date {
293 return this.start.date();
294 }
295
296 tags(): Set<string> {
297 const combinedTags: Set<string> = new Set(this.start.tags());
298 if (this.end) {
299 for (const tag of this.end.tags()) {
300 combinedTags.add(tag);
301 }
302 }
303 return combinedTags;
304 }
305
306 toString() {
307 const tags = Array.from(this.tags()).sort();
308 return `[ParsingResult {index: ${this.index}, text: '${this.text}', tags: ${JSON.stringify(tags)} ...}]`;
309 }
310}