Skip to content

Commit

Permalink
feat(shared/helpers): add getNextOccurrence of ISO R/interval (#432)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi authored Jul 16, 2024
1 parent 885ac35 commit 76a557a
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 6 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
"class-variance-authority": "^0.7.0",
"constants-browserify": "^1.0.0",
"crypto-browserify": "^3.12.0",
"date-fns": "^3.6.0",
"events": "^3.3.0",
"framer-motion": "^11.2.10",
"http-message-signatures": "^1.0.4",
"httpbis-digest-headers": "^1.0.0",
"iso8601-duration": "^2.1.2",
"loglevel": "^1.9.1",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 78 additions & 1 deletion src/shared/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { isOkState, objectEquals, removeQueryParams } from './helpers'
import { addDays, addMonths, addSeconds } from 'date-fns'
import {
isOkState,
objectEquals,
removeQueryParams,
getNextOccurrence
} from './helpers'

describe('objectEquals', () => {
it('should return true if objects are equal', () => {
Expand Down Expand Up @@ -45,3 +51,74 @@ describe('isOkState', () => {
).toBe(false)
})
})

describe('getNextOccurrence', () => {
const now = new Date()
const nowISO = now.toISOString()
const dateJan = new Date('2024-01-03T00:00:00.000Z')
const dateJanEnd = new Date('2024-01-30T00:00:00.000Z')
const dateFeb = new Date('2023-02-03T00:00:00.000Z')
const dateFebLeap = new Date('2024-02-29T00:00:00.000Z')
const dateApr = new Date('2024-04-03T00:00:00.000Z')

it('should return the next occurrence with /P1M', () => {
expect(
getNextOccurrence(`R/${dateJan.toISOString()}/P1M`, dateJan)
).toEqual(addMonths(dateJan, 1))
expect(
getNextOccurrence(`R/${dateJan.toISOString()}/P1M`, addDays(dateJan, 2))
).toEqual(addMonths(dateJan, 1))
expect(
getNextOccurrence(`R/${dateJanEnd.toISOString()}/P1M`, dateJanEnd)
).toEqual(new Date('2024-03-01T00:00:00.000Z'))
expect(
getNextOccurrence(`R/${dateFeb.toISOString()}/P1M`, dateFeb)
).toEqual(addMonths(dateFeb, 1))
expect(
getNextOccurrence(`R/${dateFebLeap.toISOString()}/P1M`, dateFebLeap)
).toEqual(addMonths(dateFebLeap, 1))
expect(
getNextOccurrence(`R/${dateApr.toISOString()}/P1M`, dateApr)
).toEqual(addMonths(dateApr, 1))
})

it('should return next occurrence with /P1W', () => {
expect(
getNextOccurrence(`R/${dateJan.toISOString()}/P1W`, dateJan)
).toEqual(addDays(dateJan, 7))
expect(
getNextOccurrence(`R/${dateFeb.toISOString()}/P1W`, dateFeb)
).toEqual(addDays(dateFeb, 7))
expect(
getNextOccurrence(`R/${dateFebLeap.toISOString()}/P1W`, dateFebLeap)
).toEqual(addDays(dateFebLeap, 7))
expect(
getNextOccurrence(`R/${dateApr.toISOString()}/P1W`, dateApr)
).toEqual(addDays(dateApr, 7))
})

it('should throw if no more occurrences are possible', () => {
const interval = `R1/${dateJan.toISOString()}/P1M`
const errorMsg = /No next occurrence is possible/

expect(() =>
getNextOccurrence(interval, addMonths(dateJan, 0))
).not.toThrow(errorMsg)
expect(() => getNextOccurrence(interval, addDays(dateJan, 10))).not.toThrow(
errorMsg
)

expect(() => getNextOccurrence(interval, addMonths(dateJan, 1))).toThrow(
errorMsg
)
expect(() => getNextOccurrence(interval, addMonths(dateJan, 2))).toThrow(
errorMsg
)
})

it('should return the next occurrence with /PT', () => {
expect(getNextOccurrence(`R/${nowISO}/PT30S`, now)).toEqual(
addSeconds(now, 30)
)
})
})
38 changes: 35 additions & 3 deletions src/shared/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { SuccessResponse } from '@/shared/messages'
import { WalletAddress } from '@interledger/open-payments/dist/types'
import type { SuccessResponse } from '@/shared/messages'
import type { WalletAddress } from '@interledger/open-payments/dist/types'
import { cx, CxOptions } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
import { addSeconds } from 'date-fns/addSeconds'
import { isAfter } from 'date-fns/isAfter'
import { isBefore } from 'date-fns/isBefore'
import { parse, toSeconds } from 'iso8601-duration'
import type { Browser } from 'webextension-polyfill'
import type { Storage } from './types'
import type { Storage, RepeatingInterval } from './types'

export const cn = (...inputs: CxOptions) => {
return twMerge(cx(inputs))
Expand Down Expand Up @@ -226,3 +230,31 @@ export const removeQueryParams = (urlString: string) => {
export const isOkState = (state: Storage['state']) => {
return Object.values(state).every((value) => value === false)
}

const REPEATING_INTERVAL_REGEX =
/^R(\d*)\/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\/(P.+)$/

export const getNextOccurrence = (
interval: RepeatingInterval,
base = new Date()
): Date => {
const match = interval.match(REPEATING_INTERVAL_REGEX)
if (!match) {
throw new Error(`Invalid interval: ${interval}`)
}
const count = match[1] ? parseInt(match[1], 10) : null
const startDate = new Date(match[2])
const pattern = parse(match[3])
const seconds = toSeconds(pattern, base)

if (count && isAfter(base, addSeconds(startDate, count * seconds))) {
throw new Error('No next occurrence is possible beyond given time')
}

let date = new Date(startDate)
while (!isBefore(base, date)) {
date = addSeconds(date, seconds)
}

return date
}
6 changes: 4 additions & 2 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { WalletAddress } from '@interledger/open-payments/dist/types'
/** Bigint amount, before transformation with assetScale */
export type AmountValue = string

/** https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */
export type RepeatingInterval = string

/** Wallet amount */
export interface WalletAmount {
value: string
/** https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */
interval?: string
interval?: RepeatingInterval
}

/** Amount interface - used in the `exceptionList` */
Expand Down

0 comments on commit 76a557a

Please sign in to comment.