-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from remotehour/develop
implemented
- Loading branch information
Showing
10 changed files
with
445 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,82 @@ | ||
# rrule-contrib | ||
|
||
[RRule](https://github.com/jakubroztocil/rrule) related utilities. | ||
|
||
# Install | ||
|
||
``` | ||
npm install --save rrule rrule-contrib | ||
``` | ||
|
||
Or if you use Yarn: | ||
|
||
``` | ||
yarn add rrule rrule-contrib | ||
``` | ||
|
||
# Getting Started | ||
|
||
## `CalendarEvent` | ||
|
||
`CalendarEvent` class is intended to use `RRule` with calendar events. Basically an `event` is represented by `start`, `end` and `RRule`. Let's see an example event. | ||
|
||
```typescript | ||
import { RRule } from 'rrule' | ||
import { CalendarEvent } from 'rrule-contrib' | ||
|
||
const event = new CalendarEvent({ | ||
start: { | ||
hour: 10, | ||
minute: 0, | ||
}, | ||
end: { | ||
hour: 13, | ||
minute: 30, | ||
}, | ||
recurrences: [ | ||
new RRule({ | ||
freq: RRule.DAILY, | ||
byweekday: [RRule.MO, RRule.FR], | ||
}), | ||
new RRule({ | ||
freq: RRule.MONTHLY, | ||
bymonthday: 25, | ||
}), | ||
], | ||
}) | ||
``` | ||
|
||
You can get event occurences with `occurences` method. | ||
|
||
```typescript | ||
event.toText() | ||
// => 10:00 AM to 13:30 AM every day on Monday, Friday and every month on the 25th | ||
|
||
event.occurences({ | ||
between: [new Date('2020-10-01T00:00:00Z'), new Date('2020-12-31T00:00:00Z')], | ||
}) | ||
// [ | ||
// [new Date('2020-10-16T10:00:00Z'), new Date('2020-10-16T13:30:00Z')], | ||
// [new Date('2020-11-20T10:00:00Z'), new Date('2020-11-20T13:30:00Z')], | ||
// [new Date('2020-12-18T10:00:00Z'), new Date('2020-12-18T13:30:00Z')], | ||
// ], | ||
``` | ||
|
||
You can get event text as natural language (currently only in English) text. | ||
|
||
```typescript | ||
event.toText() | ||
// => 10:00 AM to 13:30 AM every day on Monday, Friday and every month on the 25th | ||
``` | ||
|
||
**Time zone support** | ||
|
||
The most powerful feature of `CalendarEvent` is strong time zone support. We, Remotehour, provides our service globally so this feature has been developed. In the above example, you can pass a `tz` option to `toText` method. | ||
|
||
```typescript | ||
event.toText({ tz: 'Asia/Tokyo' }) | ||
// => 7:00 PM to 10:30 PM every day on Monday, Friday and every month on the 25th | ||
|
||
event.toText({ tz: 'Pacific/Pago_Pago' }) | ||
// => 11:00 PM to the next day of 2:30 AM every day on Sunday, Thursday and every month on the 24th | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,36 @@ | ||
{ | ||
"name": "rrule-contrib", | ||
"version": "0.0.1", | ||
"description": "RRule related utilities with timezone NLP support", | ||
"repository": "[email protected]:remotehour/rrule-duration", | ||
"author": "gosho-kazuya <[email protected]>", | ||
"license": "MIT", | ||
"main": "dist/index.js", | ||
"jsnext:main": "dist/index.es.js", | ||
"files": [ | ||
"dist", | ||
"README.md" | ||
], | ||
"scripts": { | ||
"build": "tsc", | ||
"build": "rollup -c", | ||
"test": "ava" | ||
}, | ||
"devDependencies": { | ||
"ava": "^3.12.1", | ||
"husky": "^4.3.0", | ||
"lint-staged": "^10.4.0", | ||
"prettier": "^2.1.2", | ||
"rollup": "^2.28.2", | ||
"rollup-plugin-typescript2": "^0.27.3", | ||
"rrule": "^2.6.6", | ||
"ts-node": "^9.0.0", | ||
"tslib": "^2.0.1", | ||
"typescript": "^4.0.3" | ||
}, | ||
"peerDependencies": { | ||
"rrule": "^2" | ||
}, | ||
"dependencies": { | ||
"dayjs": "^1.8.36" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import typescript from 'rollup-plugin-typescript2' | ||
|
||
export default { | ||
input: './src/index.ts', | ||
plugins: [ | ||
typescript({ | ||
tslib: require('tslib'), | ||
declaration: true, | ||
}), | ||
], | ||
external: (id) => !id.startsWith('.') && !id.startsWith('/') && id !== 'tslib', | ||
output: [ | ||
{ | ||
file: 'dist/index.js', | ||
format: 'cjs', | ||
name: 'typed-graphqlify', | ||
sourcemap: true, | ||
}, | ||
{ | ||
file: 'dist/index.es.js', | ||
format: 'es', | ||
sourcemap: true, | ||
}, | ||
], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { CalendarEvent } from './CalendarEvent' | ||
import { RRule } from 'rrule' | ||
import test from 'ava' | ||
|
||
test('CalendarEvent - init with hour and minute, occurences', (t) => { | ||
const event = new CalendarEvent({ | ||
start: { | ||
hour: 10, | ||
minute: 0, | ||
}, | ||
end: { | ||
hour: 10, | ||
minute: 30, | ||
}, | ||
recurrences: [ | ||
new RRule({ | ||
freq: RRule.MONTHLY, | ||
dtstart: new Date('2020-09-27T09:08:24.000Z'), | ||
bymonthday: [], | ||
byweekday: [RRule.FR.nth(3)], | ||
}), | ||
], | ||
}) | ||
|
||
t.is(event.toText(), '10:00 AM to 10:30 AM every month on the 3rd Friday') | ||
|
||
t.deepEqual( | ||
event.occurences({ | ||
between: [new Date('2020-09-01T00:00:00'), new Date('2020-12-31T00:00:00')], | ||
}), | ||
[ | ||
[new Date('2020-10-16T10:00:00Z'), new Date('2020-10-16T10:30:00Z')], | ||
[new Date('2020-11-20T10:00:00Z'), new Date('2020-11-20T10:30:00Z')], | ||
[new Date('2020-12-18T10:00:00Z'), new Date('2020-12-18T10:30:00Z')], | ||
], | ||
) | ||
}) | ||
|
||
test('CalendarEvent - toText format & time zone', (t) => { | ||
const event = new CalendarEvent({ | ||
start: { | ||
dateTime: '2000-01-01T18:00:00Z', | ||
}, | ||
end: { | ||
dateTime: '2000-01-01T21:00:00Z', | ||
}, | ||
recurrences: [ | ||
new RRule({ | ||
freq: RRule.DAILY, | ||
byweekday: [RRule.MO, RRule.FR], | ||
}), | ||
new RRule({ | ||
freq: RRule.MONTHLY, | ||
bymonthday: 25, | ||
}), | ||
], | ||
}) | ||
|
||
t.is(event.toText(), '6:00 PM to 9:00 PM every day on Monday, Friday and every month on the 25th') | ||
|
||
t.is( | ||
event.toText({ tz: 'Pacific/Pago_Pago', joinDatesWith: ' / ' }), | ||
'7:00 AM to 10:00 AM every day on Monday, Friday / every month on the 25th', | ||
) | ||
|
||
// Plus date | ||
t.is( | ||
event.toText({ tz: 'Asia/Tokyo' }), | ||
'3:00 AM to 6:00 AM every day on Tuesday, Saturday and every month on the 26th', | ||
) | ||
|
||
// Plus date, only end | ||
t.is( | ||
event.toText({ tz: 'Asia/Samarkand' }), | ||
'11:00 PM to the next day of 2:00 AM every day on Monday, Friday and every month on the 25th', | ||
) | ||
}) | ||
|
||
test('CalendarEvent - toText format prev date timezone', (t) => { | ||
const event = new CalendarEvent({ | ||
start: { | ||
dateTime: '2000-01-01T10:00:00Z', | ||
}, | ||
end: { | ||
dateTime: '2000-01-01T13:00:00Z', | ||
}, | ||
recurrences: [ | ||
new RRule({ | ||
freq: RRule.DAILY, | ||
byweekday: [RRule.MO, RRule.FR], | ||
}), | ||
new RRule({ | ||
freq: RRule.MONTHLY, | ||
bymonthday: 25, | ||
}), | ||
], | ||
}) | ||
|
||
t.is( | ||
event.toText({ tz: 'Pacific/Pago_Pago' }), | ||
'11:00 PM to the next day of 2:00 AM every day on Sunday, Thursday and every month on the 24th', | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { RRule, RRuleSet } from 'rrule' | ||
import dayjs from 'dayjs' | ||
import utc from 'dayjs/plugin/utc' | ||
import timezone from 'dayjs/plugin/timezone' | ||
|
||
dayjs.extend(utc) | ||
dayjs.extend(timezone) | ||
|
||
interface CalendarEventDef { | ||
dateTime?: Date | string | ||
hour?: number | ||
minute?: number | ||
tz?: string | ||
} | ||
|
||
interface CalendarEventArgs { | ||
start: CalendarEventDef | ||
end: CalendarEventDef | ||
recurrences: RRule[] | ||
} | ||
|
||
interface ToTextArgs { | ||
tz?: string | ||
joinDatesWith?: string | ||
timeFormat?: string | ||
} | ||
|
||
interface OccurencesArgs { | ||
between?: [Date, Date] | ||
until?: Date | ||
} | ||
|
||
const INITIAL_DATE = new Date(Date.UTC(2000, 0, 15, 0, 0, 0, 0)) | ||
|
||
export class CalendarEvent { | ||
private start: dayjs.Dayjs | ||
private end: dayjs.Dayjs | ||
private recurrences: RRuleSet | ||
|
||
constructor({ start, end, recurrences }: CalendarEventArgs) { | ||
if ((start.hour === undefined || start.minute === undefined) && !start.dateTime) { | ||
throw new Error('invalid "start" value. Either spcify dateTime or hour/minutes') | ||
} | ||
if ((end.hour === undefined || end.minute === undefined) && !end.dateTime) { | ||
throw new Error('invalid "end" value. Either spcify dateTime or hour/minutes') | ||
} | ||
|
||
this.start = dayjs.tz(start.dateTime || INITIAL_DATE, start.tz || 'UTC') | ||
if (Number.isInteger(start.hour) && Number.isInteger(start.minute)) { | ||
this.start = this.start.hour(start.hour!).minute(start.minute!) | ||
} | ||
|
||
this.end = dayjs.tz(end.dateTime || INITIAL_DATE, end.tz || 'UTC') | ||
if (Number.isInteger(end.hour) && Number.isInteger(end.minute)) { | ||
this.end = this.end.hour(end.hour!).minute(end.minute!) | ||
} | ||
|
||
this.recurrences = new RRuleSet() | ||
|
||
recurrences.forEach((r) => this.recurrences.rrule(r)) | ||
} | ||
|
||
occurences(args: OccurencesArgs): [Date, Date][] { | ||
return this.getOccurences(args).map((date) => { | ||
const start = dayjs | ||
.utc(date) | ||
.hour(this.start.hour()) | ||
.minute(this.start.minute()) | ||
.second(0) | ||
.millisecond(0) | ||
.toDate() | ||
const end = dayjs | ||
.utc(date) | ||
.hour(this.end.hour()) | ||
.minute(this.end.minute()) | ||
.second(0) | ||
.millisecond(0) | ||
.toDate() | ||
return [start, end] | ||
}) | ||
} | ||
|
||
getOccurences({ between, until }: OccurencesArgs) { | ||
if (between) { | ||
return this.recurrences.between(between[0], between[1]) | ||
} | ||
|
||
if (until) { | ||
return this.recurrences.between(new Date(), until) | ||
} | ||
|
||
return this.recurrences.between(new Date(), dayjs().add(1, 'month').toDate()) | ||
} | ||
|
||
toText({ tz = 'UTC', joinDatesWith = ' and ', timeFormat = 'h:mm A' }: ToTextArgs = {}) { | ||
const start = dayjs.tz(this.start, tz) | ||
const end = dayjs.tz(this.end, tz) | ||
const occurencesText = this.recurrences | ||
.clone() | ||
.rrules() | ||
.map((rrule) => { | ||
if (rrule instanceof RRuleSet) { | ||
throw new Error('Invalid class: RRuleSet') | ||
} | ||
|
||
const crossTimezonePrev = | ||
this.start.date() > start.date() || (this.start.date() === 1 && start.date() > 26) | ||
if (crossTimezonePrev) { | ||
return new RRule({ | ||
...rrule.options, | ||
byweekday: rrule.options.byweekday?.map((x) => x - 1), | ||
bymonthday: rrule.options.bymonthday?.map((x) => x - 1), | ||
}).toText() | ||
} | ||
|
||
const crossTimezoneNext = | ||
this.start.date() < start.date() || (start.date() === 1 && this.start.date() > 26) | ||
if (crossTimezoneNext) { | ||
return new RRule({ | ||
...rrule.options, | ||
byweekday: rrule.options.byweekday?.map((x) => x + 1), | ||
bymonthday: rrule.options.bymonthday?.map((x) => x + 1), | ||
}).toText() | ||
} | ||
|
||
return rrule.toText() | ||
}) | ||
.join(joinDatesWith) | ||
|
||
const startText = start.format(timeFormat) | ||
const endText = end.format(timeFormat) | ||
const nextDateNotes = start.day() === end.day() ? '' : 'the next day of ' | ||
|
||
return `${startText} to ${nextDateNotes}${endText} ${occurencesText}` | ||
} | ||
} |
Oops, something went wrong.