Skip to content

Commit 8958060

Browse files
committed
Recurring natural input
1 parent 70fb14b commit 8958060

File tree

9 files changed

+194
-10
lines changed

9 files changed

+194
-10
lines changed

app/controllers/events_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ def create
1616
private
1717

1818
def event_params
19-
params.expect(event: [:title, :starts_at, :ends_at, :recurring_type, :recurring_day, :recurring_interval, :recurring_until])
19+
params.expect(event: [:title, :starts_at, :ends_at, :recurring_rule, :recurring_until])
2020
end
2121
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
import { RecurringParser } from "src/recurring_parser"
3+
4+
export default class extends Controller {
5+
static targets = ["input", "feedback"]
6+
7+
parse(event) {
8+
const result = this.#parser.parse(event.currentTarget.value)
9+
10+
if (result.valid) {
11+
this.feedbackTarget.textContent = "✓"
12+
13+
this.inputTarget.value = JSON.stringify(result.rule)
14+
} else {
15+
this.feedbackTarget.textContent = "Don't know wht you mean…"
16+
17+
this.inputTarget.value = ""
18+
}
19+
}
20+
21+
// private
22+
23+
get #parser() {
24+
return new RecurringParser({ backend: "iceCube" })
25+
}
26+
}

app/javascript/src/date_helpers.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export const DAYS = {
2+
mon: "monday",
3+
tue: "tuesday",
4+
wed: "wednesday",
5+
thu: "thursday",
6+
fri: "friday",
7+
sat: "saturday",
8+
sun: "sunday"
9+
}
10+
11+
export const MONTHS = {
12+
jan: "january",
13+
feb: "february",
14+
mar: "march",
15+
apr: "april",
16+
may: "may",
17+
jun: "june",
18+
jul: "july",
19+
aug: "august",
20+
sep: "september",
21+
oct: "october",
22+
nov: "november",
23+
dec: "december"
24+
}
25+
26+
export const dayPattern = `(${Object.keys(DAYS).join("|")}|${Object.values(DAYS).join("|")})`
27+
export const monthPattern = `(${Object.keys(MONTHS).join("|")}|${Object.values(MONTHS).join("|")})`
28+
29+
export function dayToNumber(day) {
30+
const normalizedDay = DAYS[day.toLowerCase()] || day.toLowerCase()
31+
32+
return Object.values(DAYS).indexOf(normalizedDay)
33+
}
34+
35+
export function monthToNumber(month) {
36+
const normalizedMonth = MONTHS[month.toLowerCase()] || month.toLowerCase()
37+
38+
return Object.values(MONTHS).indexOf(normalizedMonth) + 1
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { iceCube } from "src/recurring_parser/backends/ice_cube"
2+
import { patterns } from "src/recurring_parser/patterns"
3+
4+
export class RecurringParser {
5+
static backends = { iceCube }
6+
7+
constructor(options = {}) {
8+
this.backend = RecurringParser.backends[options.backend || "iceCube"]
9+
}
10+
11+
parse(input) {
12+
if (!input?.trim()) return { valid: false }
13+
14+
let result = { valid: false }
15+
16+
Object.values(patterns).forEach(pattern => {
17+
const matches = input.match(pattern.regex)
18+
19+
if (!matches) return
20+
21+
result = {
22+
valid: true,
23+
rule: pattern.parse(matches, this.backend)
24+
}
25+
})
26+
27+
return result
28+
}
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export const iceCube = {
2+
daily: () => ({
3+
rule_type: "IceCube::DailyRule",
4+
validations: {},
5+
interval: 1
6+
}),
7+
8+
weekly: (day) => ({
9+
rule_type: "IceCube::WeeklyRule",
10+
validations: { day: [day] },
11+
interval: 1
12+
}),
13+
14+
biweekly: (day) => ({
15+
rule_type: "IceCube::WeeklyRule",
16+
validations: { day: [day] },
17+
interval: 2
18+
}),
19+
20+
monthly_day: (dayNum) => ({
21+
rule_type: "IceCube::MonthlyRule",
22+
validations: { day_of_month: [dayNum] },
23+
interval: 1
24+
}),
25+
26+
yearly_on: (month, day) => ({
27+
rule_type: "IceCube::YearlyRule",
28+
validations: {
29+
month_of_year: [month],
30+
day_of_month: [day]
31+
},
32+
33+
interval: 1
34+
})
35+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
dayPattern,
3+
monthPattern,
4+
dayToNumber,
5+
monthToNumber
6+
} from "src/date_helpers"
7+
8+
export const patterns = {
9+
daily: {
10+
regex: /^every\s+day|daily$/i,
11+
parse: (_, backend) => backend.daily()
12+
},
13+
14+
weekly: {
15+
regex: new RegExp(`^(?:every\\s+week|weekly)(?:\\s+on\\s+${dayPattern})?$`, "i"),
16+
parse: (matches, backend) => {
17+
const currentDay = new Date().getDay()
18+
const day = matches[1] ? dayToNumber(matches[1]) : currentDay
19+
20+
return backend.weekly(day)
21+
}
22+
},
23+
24+
biweekly: {
25+
regex: new RegExp(`^(?:every\\s+2\\s+weeks?|bi-?weekly)(?:\\s+on\\s+${dayPattern})?$`, "i"),
26+
parse: (matches, backend) => {
27+
const currentDay = new Date().getDay()
28+
const day = matches[1] ? dayToNumber(matches[1]) : currentDay
29+
30+
return backend.biweekly(day)
31+
}
32+
},
33+
34+
monthly: {
35+
regex: /^(?:every\s+month|monthly)(?:\s+on\s+(the\s+)?(\d{1,2})(?:st|nd|rd|th)?)?$/i,
36+
parse: (matches, backend) => {
37+
const currentDay = new Date().getDate()
38+
const dayNum = matches[2] ? parseInt(matches[2]) : currentDay
39+
40+
return backend.monthly_day(dayNum)
41+
}
42+
},
43+
44+
yearly: {
45+
regex: new RegExp(`^(?:every\\s+year|yearly)(?:\\s+on\\s+${monthPattern}(?:\\s+(\\d{1,2})(?:st|nd|rd|th)?)?)?$`, "i"),
46+
parse: (matches, backend) => {
47+
const currentDate = new Date()
48+
const month = matches[1] ? monthToNumber(matches[1]) : currentDate.getMonth() + 1
49+
const day = matches[2] ? parseInt(matches[2]) : currentDate.getDate()
50+
51+
return backend.yearly_on(month, day)
52+
}
53+
}
54+
}

app/models/event.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
class Event < ApplicationRecord
22
include Recurrence
3-
include Recurrence::Builder
3+
# include Recurrence::Builder
44
end

app/views/events/_form.html.erb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@
1414
<%= form.datetime_field :ends_at, required: true %>
1515
</div>
1616

17-
<div>
18-
<%= form.label :recurring_type, "Repeats" %>
19-
<%= form.select :recurring_type, [
20-
["Daily", "daily"],
21-
["Weekly", "weekly"],
22-
["Every 2 weeks", "biweekly"],
23-
["Monthly", "monthly"]
24-
], { include_blank: "No recurrence" } %>
17+
<div data-controller="recurring">
18+
<%= form.label :natural_recurring %>
19+
<%= form.text_field :natural_recurring, data: {action: "recurring#parse"} %>
20+
<%= form.hidden_field :recurring_rule, data: {recurring_target: "input"} %>
21+
22+
<small data-recurring-target="feedback"></small>
2523
</div>
2624

2725
<div>

config/importmap.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44
pin "@hotwired/turbo-rails", to: "turbo.min.js"
55
pin "@hotwired/stimulus", to: "stimulus.min.js"
66
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
7+
8+
pin_all_from "app/javascript/src", under: "src", to: "src"
9+
710
pin_all_from "app/javascript/controllers", under: "controllers"

0 commit comments

Comments
 (0)