added recurrence (#35)

This commit is contained in:
Doh
2025-01-10 22:54:41 -05:00
committed by GitHub
parent 889391fcfe
commit 6c5853adea
14 changed files with 780 additions and 357 deletions

View File

@@ -1,7 +1,9 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { DateTime } from "luxon"
import { Habit, CoinTransaction } from '@/lib/types'
import { datetime, RRule } from 'rrule'
import { Freq, Habit, CoinTransaction } from '@/lib/types'
import { INITIAL_RECURRENCE_RULE, RECURRENCE_RULE_MAP } from "./constants"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -14,8 +16,8 @@ export function getTodayInTimezone(timezone: string): string {
}
// get datetime object of now
export function getNow({ timezone = 'utc' }: { timezone?: string }) {
return DateTime.now().setZone(timezone);
export function getNow({ timezone = 'utc', keepLocalTime }: { timezone?: string, keepLocalTime?: boolean }) {
return DateTime.now().setZone(timezone, { keepLocalTime });
}
// get current time in epoch milliseconds
@@ -97,18 +99,6 @@ export function getCompletedHabitsForDate({
})
}
export function isHabitCompletedToday({
habit,
timezone
}: {
habit: Habit,
timezone: string
}): boolean {
const today = getTodayInTimezone(timezone)
const completionsToday = getCompletionsForDate({ habit, date: today, timezone })
return completionsToday >= (habit.targetCompletions || 1)
}
export function getHabitProgress({
habit,
timezone
@@ -135,7 +125,7 @@ export function calculateCoinsEarnedToday(transactions: CoinTransaction[], timez
export function calculateTotalEarned(transactions: CoinTransaction[]): number {
return transactions
.filter(transaction =>
.filter(transaction =>
transaction.amount > 0 || transaction.type === 'HABIT_UNDO'
)
.reduce((sum, transaction) => sum + transaction.amount, 0);
@@ -144,7 +134,7 @@ export function calculateTotalEarned(transactions: CoinTransaction[]): number {
export function calculateTotalSpent(transactions: CoinTransaction[]): number {
return Math.abs(
transactions
.filter(transaction =>
.filter(transaction =>
transaction.amount < 0 &&
transaction.type !== 'HABIT_UNDO'
)
@@ -173,3 +163,61 @@ export function calculateTransactionsToday(transactions: CoinTransaction[], time
t2d({ timestamp: today, timezone }))
).length;
}
export function getRRuleUTC(recurrenceRule: string) {
return RRule.fromString(recurrenceRule); // this returns UTC
}
export function parseNaturalLanguageRRule(ruleText: string) {
ruleText = ruleText.trim()
if (RECURRENCE_RULE_MAP[ruleText]) {
return RRule.fromString(RECURRENCE_RULE_MAP[ruleText])
}
return RRule.fromText(ruleText)
}
export function parseRRule(ruleText: string) {
ruleText = ruleText.trim()
if (RECURRENCE_RULE_MAP[ruleText]) {
return RRule.fromString(RECURRENCE_RULE_MAP[ruleText])
}
return RRule.fromString(ruleText)
}
export function serializeRRule(rrule: RRule) {
return rrule.toString()
}
export function isHabitDueToday(habit: Habit, timezone: string): boolean {
const startOfDay = DateTime.now().setZone(timezone).startOf('day')
const endOfDay = DateTime.now().setZone(timezone).endOf('day')
const ruleText = habit.frequency
const rrule = parseRRule(ruleText)
rrule.origOptions.tzid = timezone // set the target timezone, rrule will do calculation in this timezone
rrule.options.tzid = rrule.origOptions.tzid
rrule.origOptions.dtstart = datetime(startOfDay.year, startOfDay.month, startOfDay.day, startOfDay.hour, startOfDay.minute, startOfDay.second) // set the start time to 00:00:00 of timezone's today
rrule.options.dtstart = rrule.origOptions.dtstart
rrule.origOptions.count = 1
rrule.options.count = rrule.origOptions.count
const matches = rrule.all() // this is given as local time, we need to convert back to timezone time
if (!matches.length) return false
const t = DateTime.fromJSDate(matches[0]).toUTC().setZone('local', { keepLocalTime: true }).setZone(timezone) // this is the formula to convert local time matches[0] to tz time
return startOfDay <= t && t <= endOfDay
}
export function getHabitFreq(habit: Habit): Freq {
const rrule = parseRRule(habit.frequency)
const freq = rrule.origOptions.freq
switch (freq) {
case RRule.DAILY: return 'daily'
case RRule.WEEKLY: return 'weekly'
case RRule.MONTHLY: return 'monthly'
case RRule.YEARLY: return 'yearly'
default: throw new Error(`Invalid frequency: ${freq}`)
}
}