Skip to content

Commit

Permalink
Merge pull request #2 from remotehour/develop
Browse files Browse the repository at this point in the history
implemented
  • Loading branch information
acro5piano authored Sep 27, 2020
2 parents e5d34e7 + 025db09 commit d944b98
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 53 deletions.
79 changes: 79 additions & 0 deletions README.md
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
```
15 changes: 14 additions & 1 deletion package.json
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"
}
}
25 changes: 25 additions & 0 deletions rollup.config.js
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,
},
],
}
103 changes: 103 additions & 0 deletions src/CalendarEvent.test.ts
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',
)
})
136 changes: 136 additions & 0 deletions src/CalendarEvent.ts
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}`
}
}
Loading

0 comments on commit d944b98

Please sign in to comment.