@craftguild/jscalendar 0.3.0 → 0.4.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/README.md CHANGED
@@ -100,7 +100,7 @@ const task = new JsCal.Task({
100
100
  JsCal.Location({ name: "Room A" }),
101
101
  ]),
102
102
  alerts: JsCal.alerts([
103
- JsCal.Alert({ trigger: { "@type": "OffsetTrigger", offset: "-PT15M" } }),
103
+ JsCal.Alert({ trigger: JsCal.OffsetTrigger({ offset: JsCal.duration.minutes(-15) }) }),
104
104
  ]),
105
105
  });
106
106
  ```
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { JsCal } from "../jscal.js";
3
- import { buildAlert, buildIdMap, buildLink, buildLocation, buildNDay, buildRecurrenceRule, buildRelation, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, } from "../jscal/builders.js";
3
+ import { buildAbsoluteTrigger, buildAlert, buildIdMap, buildLink, buildLocation, buildNDay, buildOffsetTrigger, buildRecurrenceRule, buildRelation, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, } from "../jscal/builders.js";
4
4
  describe("builders", () => {
5
5
  it("builds strict objects with @type", () => {
6
6
  const participant = JsCal.Participant({ name: "Alice", roles: { attendee: true } });
@@ -8,7 +8,8 @@ describe("builders", () => {
8
8
  const vloc = buildVirtualLocation({ name: "Zoom", uri: "https://example.com" });
9
9
  const link = buildLink({ href: "https://example.com" });
10
10
  const relation = buildRelation({ relation: { parent: true } });
11
- const alert = buildAlert({ trigger: { "@type": "OffsetTrigger", offset: "-PT15M" } });
11
+ const alert = buildAlert({ trigger: buildOffsetTrigger({ offset: JsCal.duration.minutes(-15) }) });
12
+ const absoluteTrigger = buildAbsoluteTrigger({ when: "2026-02-01T00:00:00Z" });
12
13
  const nday = buildNDay({ day: "mo" });
13
14
  const rule = buildRecurrenceRule({ frequency: "daily" });
14
15
  const tzRule = buildTimeZoneRule({
@@ -26,6 +27,7 @@ describe("builders", () => {
26
27
  expect(link["@type"]).toBe("Link");
27
28
  expect(relation["@type"]).toBe("Relation");
28
29
  expect(alert["@type"]).toBe("Alert");
30
+ expect(absoluteTrigger["@type"]).toBe("AbsoluteTrigger");
29
31
  expect(nday["@type"]).toBe("NDay");
30
32
  expect(rule["@type"]).toBe("RecurrenceRule");
31
33
  expect(tz["@type"]).toBe("TimeZone");
@@ -66,6 +68,10 @@ describe("builders", () => {
66
68
  .toThrowError("virtualLocation: must have @type VirtualLocation");
67
69
  expect(() => buildAlert({ "@type": "Link" }))
68
70
  .toThrowError("alert: must have @type Alert");
71
+ expect(() => buildOffsetTrigger({ "@type": "Alert" }))
72
+ .toThrowError("offsetTrigger: must have @type OffsetTrigger");
73
+ expect(() => buildAbsoluteTrigger({ "@type": "Alert" }))
74
+ .toThrowError("absoluteTrigger: must have @type AbsoluteTrigger");
69
75
  expect(() => buildRelation({ "@type": "Link" }))
70
76
  .toThrowError("relation: must have @type Relation");
71
77
  expect(() => buildLink({ "@type": "Relation" }))
@@ -49,6 +49,14 @@ describe("utils", () => {
49
49
  expect(JsCal.duration.days(1)).toBe("P1D");
50
50
  expect(JsCal.duration.from({ hours: 1, minutes: 15 })).toBe("PT1H15M");
51
51
  });
52
+ it("builds signed duration strings for negative values", () => {
53
+ expect(JsCal.duration.minutes(-15)).toBe("-PT15M");
54
+ expect(JsCal.duration.seconds(-1)).toBe("-PT1S");
55
+ expect(JsCal.duration.from({ minutes: -15 })).toBe("-PT15M");
56
+ });
57
+ it("avoids negative zero in duration strings", () => {
58
+ expect(JsCal.duration.seconds(-0)).toBe("PT0S");
59
+ });
52
60
  it("creates base64url ids", () => {
53
61
  const id = JsCal.createId();
54
62
  expect(id).toMatch(/^[A-Za-z0-9_-]+$/);
@@ -1,8 +1,10 @@
1
- import type { Alert, Link, Location, NDay, Participant, RecurrenceRule, Relation, TimeZone, TimeZoneRule, VirtualLocation, Id } from "../types.js";
1
+ import type { Alert, AbsoluteTrigger, Link, Location, NDay, OffsetTrigger, Participant, RecurrenceRule, Relation, TimeZone, TimeZoneRule, VirtualLocation, Id } from "../types.js";
2
2
  declare const TYPE_PARTICIPANT = "Participant";
3
3
  declare const TYPE_LOCATION = "Location";
4
4
  declare const TYPE_VIRTUAL_LOCATION = "VirtualLocation";
5
5
  declare const TYPE_ALERT = "Alert";
6
+ declare const TYPE_OFFSET_TRIGGER = "OffsetTrigger";
7
+ declare const TYPE_ABSOLUTE_TRIGGER = "AbsoluteTrigger";
6
8
  declare const TYPE_RELATION = "Relation";
7
9
  declare const TYPE_LINK = "Link";
8
10
  declare const TYPE_TIME_ZONE = "TimeZone";
@@ -16,6 +18,8 @@ export type ParticipantInput = WithOptionalType<Participant, typeof TYPE_PARTICI
16
18
  export type LocationInput = WithOptionalType<Location, typeof TYPE_LOCATION>;
17
19
  export type VirtualLocationInput = WithOptionalType<VirtualLocation, typeof TYPE_VIRTUAL_LOCATION>;
18
20
  export type AlertInput = WithOptionalType<Alert, typeof TYPE_ALERT>;
21
+ export type OffsetTriggerInput = WithOptionalType<OffsetTrigger, typeof TYPE_OFFSET_TRIGGER>;
22
+ export type AbsoluteTriggerInput = WithOptionalType<AbsoluteTrigger, typeof TYPE_ABSOLUTE_TRIGGER>;
19
23
  export type RelationInput = WithOptionalType<Relation, typeof TYPE_RELATION>;
20
24
  export type LinkInput = WithOptionalType<Link, typeof TYPE_LINK>;
21
25
  export type TimeZoneInput = WithOptionalType<TimeZone, typeof TYPE_TIME_ZONE>;
@@ -46,6 +50,18 @@ export declare function buildVirtualLocation(input: VirtualLocationInput): Virtu
46
50
  * @return Validated Alert object.
47
51
  */
48
52
  export declare function buildAlert(input: AlertInput): Alert;
53
+ /**
54
+ * Build an OffsetTrigger object with @type set and validated.
55
+ * @param input OffsetTrigger fields without @type.
56
+ * @return Validated OffsetTrigger object.
57
+ */
58
+ export declare function buildOffsetTrigger(input: OffsetTriggerInput): OffsetTrigger;
59
+ /**
60
+ * Build an AbsoluteTrigger object with @type set and validated.
61
+ * @param input AbsoluteTrigger fields without @type.
62
+ * @return Validated AbsoluteTrigger object.
63
+ */
64
+ export declare function buildAbsoluteTrigger(input: AbsoluteTriggerInput): AbsoluteTrigger;
49
65
  /**
50
66
  * Build a Relation object with @type set and validated.
51
67
  * @param input Relation fields without @type.
@@ -2,10 +2,13 @@ import { createId } from "./ids.js";
2
2
  import { validateAlert, validateLink, validateLocation, validateParticipant, validateRelation, validateTimeZoneObject, validateTimeZoneRule, validateVirtualLocation } from "../validate/validators-common.js";
3
3
  import { validateNDay, validateRecurrenceRule } from "../validate/validators-recurrence.js";
4
4
  import { fail } from "../validate/error.js";
5
+ import { assertSignedDuration, assertString, assertUtcDateTime } from "../validate/asserts.js";
5
6
  const TYPE_PARTICIPANT = "Participant";
6
7
  const TYPE_LOCATION = "Location";
7
8
  const TYPE_VIRTUAL_LOCATION = "VirtualLocation";
8
9
  const TYPE_ALERT = "Alert";
10
+ const TYPE_OFFSET_TRIGGER = "OffsetTrigger";
11
+ const TYPE_ABSOLUTE_TRIGGER = "AbsoluteTrigger";
9
12
  const TYPE_RELATION = "Relation";
10
13
  const TYPE_LINK = "Link";
11
14
  const TYPE_TIME_ZONE = "TimeZone";
@@ -64,6 +67,33 @@ export function buildAlert(input) {
64
67
  validateAlert(alert, "alert");
65
68
  return alert;
66
69
  }
70
+ /**
71
+ * Build an OffsetTrigger object with @type set and validated.
72
+ * @param input OffsetTrigger fields without @type.
73
+ * @return Validated OffsetTrigger object.
74
+ */
75
+ export function buildOffsetTrigger(input) {
76
+ if (input["@type"] && input["@type"] !== TYPE_OFFSET_TRIGGER) {
77
+ fail("offsetTrigger", `must have @type ${TYPE_OFFSET_TRIGGER}`);
78
+ }
79
+ const trigger = { ...input, "@type": TYPE_OFFSET_TRIGGER };
80
+ assertSignedDuration(trigger.offset, "offsetTrigger.offset");
81
+ assertString(trigger.relativeTo, "offsetTrigger.relativeTo");
82
+ return trigger;
83
+ }
84
+ /**
85
+ * Build an AbsoluteTrigger object with @type set and validated.
86
+ * @param input AbsoluteTrigger fields without @type.
87
+ * @return Validated AbsoluteTrigger object.
88
+ */
89
+ export function buildAbsoluteTrigger(input) {
90
+ if (input["@type"] && input["@type"] !== TYPE_ABSOLUTE_TRIGGER) {
91
+ fail("absoluteTrigger", `must have @type ${TYPE_ABSOLUTE_TRIGGER}`);
92
+ }
93
+ const trigger = { ...input, "@type": TYPE_ABSOLUTE_TRIGGER };
94
+ assertUtcDateTime(trigger.when, "absoluteTrigger.when");
95
+ return trigger;
96
+ }
67
97
  /**
68
98
  * Build a Relation object with @type set and validated.
69
99
  * @param input Relation fields without @type.
@@ -8,31 +8,31 @@ export declare const Duration: {
8
8
  /**
9
9
  * Convert seconds to a duration string.
10
10
  * @param value Total seconds.
11
- * @return ISO 8601 duration string.
11
+ * @return ISO 8601 duration string (signed when negative).
12
12
  */
13
13
  seconds(value: number): string;
14
14
  /**
15
15
  * Convert minutes to a duration string.
16
16
  * @param value Total minutes.
17
- * @return ISO 8601 duration string.
17
+ * @return ISO 8601 duration string (signed when negative).
18
18
  */
19
19
  minutes(value: number): string;
20
20
  /**
21
21
  * Convert hours to a duration string.
22
22
  * @param value Total hours.
23
- * @return ISO 8601 duration string.
23
+ * @return ISO 8601 duration string (signed when negative).
24
24
  */
25
25
  hours(value: number): string;
26
26
  /**
27
27
  * Convert days to a duration string.
28
28
  * @param value Total days.
29
- * @return ISO 8601 duration string.
29
+ * @return ISO 8601 duration string (signed when negative).
30
30
  */
31
31
  days(value: number): string;
32
32
  /**
33
33
  * Build a duration string from component parts.
34
34
  * @param parts Day/hour/minute/second parts.
35
- * @return ISO 8601 duration string.
35
+ * @return ISO 8601 duration string (signed when total is negative).
36
36
  */
37
37
  from(parts: {
38
38
  days?: number;
@@ -5,7 +5,9 @@ import { EMPTY_STRING } from "./constants.js";
5
5
  * @return ISO 8601 duration string.
6
6
  */
7
7
  export function durationFromSeconds(totalSeconds) {
8
- const clamped = Math.max(0, Math.floor(totalSeconds));
8
+ const negative = totalSeconds < 0;
9
+ const absSeconds = Math.abs(totalSeconds);
10
+ const clamped = Math.floor(absSeconds);
9
11
  const days = Math.floor(clamped / 86400);
10
12
  let remaining = clamped % 86400;
11
13
  const hours = Math.floor(remaining / 3600);
@@ -22,13 +24,14 @@ export function durationFromSeconds(totalSeconds) {
22
24
  timeParts.push(`${seconds}S`);
23
25
  }
24
26
  const timePart = timeParts.length > 0 ? `T${timeParts.join("")}` : "";
25
- return `P${datePart}${timePart}`;
27
+ const sign = negative && clamped > 0 ? "-" : "";
28
+ return `${sign}P${datePart}${timePart}`;
26
29
  }
27
30
  export const Duration = {
28
31
  /**
29
32
  * Convert seconds to a duration string.
30
33
  * @param value Total seconds.
31
- * @return ISO 8601 duration string.
34
+ * @return ISO 8601 duration string (signed when negative).
32
35
  */
33
36
  seconds(value) {
34
37
  return durationFromSeconds(value);
@@ -36,7 +39,7 @@ export const Duration = {
36
39
  /**
37
40
  * Convert minutes to a duration string.
38
41
  * @param value Total minutes.
39
- * @return ISO 8601 duration string.
42
+ * @return ISO 8601 duration string (signed when negative).
40
43
  */
41
44
  minutes(value) {
42
45
  return durationFromSeconds(value * 60);
@@ -44,7 +47,7 @@ export const Duration = {
44
47
  /**
45
48
  * Convert hours to a duration string.
46
49
  * @param value Total hours.
47
- * @return ISO 8601 duration string.
50
+ * @return ISO 8601 duration string (signed when negative).
48
51
  */
49
52
  hours(value) {
50
53
  return durationFromSeconds(value * 3600);
@@ -52,7 +55,7 @@ export const Duration = {
52
55
  /**
53
56
  * Convert days to a duration string.
54
57
  * @param value Total days.
55
- * @return ISO 8601 duration string.
58
+ * @return ISO 8601 duration string (signed when negative).
56
59
  */
57
60
  days(value) {
58
61
  return durationFromSeconds(value * 86400);
@@ -60,7 +63,7 @@ export const Duration = {
60
63
  /**
61
64
  * Build a duration string from component parts.
62
65
  * @param parts Day/hour/minute/second parts.
63
- * @return ISO 8601 duration string.
66
+ * @return ISO 8601 duration string (signed when total is negative).
64
67
  */
65
68
  from(parts) {
66
69
  const seconds = (parts.days ?? 0) * 86400 +
@@ -43,7 +43,7 @@ export class EventObject extends Base {
43
43
  data.timeZone = timeZone;
44
44
  if (rawDuration !== undefined) {
45
45
  data.duration = isNumberValue(rawDuration)
46
- ? durationFromSeconds(rawDuration)
46
+ ? durationFromSeconds(Math.max(0, rawDuration))
47
47
  : rawDuration;
48
48
  }
49
49
  if (rawCreated) {
package/dist/jscal.d.ts CHANGED
@@ -6,9 +6,9 @@ import { TaskObject } from "./jscal/task.js";
6
6
  import { GroupObject } from "./jscal/group.js";
7
7
  import { createId, createUid } from "./jscal/ids.js";
8
8
  import { isEvent, isGroup, isTask } from "./jscal/guards.js";
9
- import { buildAlert, buildLink, buildLocation, buildNDay, buildParticipants, buildRecurrenceRule, buildRelation, buildRelatedTo, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, buildVirtualLocations, buildLocations, buildLinks, buildParticipant, buildAlerts } from "./jscal/builders.js";
9
+ import { buildAlert, buildAbsoluteTrigger, buildLink, buildLocation, buildNDay, buildOffsetTrigger, buildParticipants, buildRecurrenceRule, buildRelation, buildRelatedTo, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, buildVirtualLocations, buildLocations, buildLinks, buildParticipant, buildAlerts } from "./jscal/builders.js";
10
10
  export type { CreateOptions, UpdateOptions } from "./jscal/types.js";
11
- export type { AlertInput, LinkInput, LocationInput, NDayInput, ParticipantInput, RecurrenceRuleInput, RelationInput, TimeZoneInput, TimeZoneRuleInput, VirtualLocationInput, } from "./jscal/builders.js";
11
+ export type { AlertInput, AbsoluteTriggerInput, LinkInput, LocationInput, NDayInput, OffsetTriggerInput, ParticipantInput, RecurrenceRuleInput, RelationInput, TimeZoneInput, TimeZoneRuleInput, VirtualLocationInput, } from "./jscal/builders.js";
12
12
  export { createId, createUid, isEvent, isGroup, isTask };
13
13
  export declare const JsCal: {
14
14
  Event: typeof EventObject;
@@ -35,6 +35,8 @@ export declare const JsCal: {
35
35
  Location: typeof buildLocation;
36
36
  VirtualLocation: typeof buildVirtualLocation;
37
37
  Alert: typeof buildAlert;
38
+ OffsetTrigger: typeof buildOffsetTrigger;
39
+ AbsoluteTrigger: typeof buildAbsoluteTrigger;
38
40
  Relation: typeof buildRelation;
39
41
  Link: typeof buildLink;
40
42
  TimeZone: typeof buildTimeZone;
package/dist/jscal.js CHANGED
@@ -10,7 +10,7 @@ import { Duration } from "./jscal/duration.js";
10
10
  import { createId, createUid } from "./jscal/ids.js";
11
11
  import { normalizeItems, normalizeToObjects } from "./jscal/normalize.js";
12
12
  import { isEvent, isGroup, isTask } from "./jscal/guards.js";
13
- import { buildAlert, buildLink, buildLocation, buildNDay, buildParticipants, buildRecurrenceRule, buildRelation, buildRelatedTo, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, buildVirtualLocations, buildLocations, buildLinks, buildParticipant, buildAlerts, } from "./jscal/builders.js";
13
+ import { buildAlert, buildAbsoluteTrigger, buildLink, buildLocation, buildNDay, buildOffsetTrigger, buildParticipants, buildRecurrenceRule, buildRelation, buildRelatedTo, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, buildVirtualLocations, buildLocations, buildLinks, buildParticipant, buildAlerts, } from "./jscal/builders.js";
14
14
  export { createId, createUid, isEvent, isGroup, isTask };
15
15
  export const JsCal = {
16
16
  Event: EventObject,
@@ -26,6 +26,8 @@ export const JsCal = {
26
26
  Location: buildLocation,
27
27
  VirtualLocation: buildVirtualLocation,
28
28
  Alert: buildAlert,
29
+ OffsetTrigger: buildOffsetTrigger,
30
+ AbsoluteTrigger: buildAbsoluteTrigger,
29
31
  Relation: buildRelation,
30
32
  Link: buildLink,
31
33
  TimeZone: buildTimeZone,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftguild/jscalendar",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "RFC 8984 (JSCalendar) data model helpers for TypeScript",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",