rem2ics 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +328 -0
- data/docs/rem2ics.md +83 -0
- data/exe/rem2ics +8 -0
- data/lib/rem2ics/cli.rb +201 -0
- data/lib/rem2ics/converter.rb +162 -0
- data/lib/rem2ics/event.rb +240 -0
- data/lib/rem2ics/recurrence.rb +344 -0
- data/lib/rem2ics/version.rb +5 -0
- data/lib/rem2ics.rb +32 -0
- metadata +144 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ice_cube"
|
|
4
|
+
|
|
5
|
+
module Rem2ics
|
|
6
|
+
# How a reminder repeats, expressed the way iCalendar expresses it -- when
|
|
7
|
+
# iCalendar can express it at all.
|
|
8
|
+
#
|
|
9
|
+
# A reminder's trigger and an RRULE overlap, but neither contains the other.
|
|
10
|
+
# `REM 15` and `FREQ=MONTHLY;BYMONTHDAY=15` are the same thing. `REM Mon 13
|
|
11
|
+
# SKIP OMIT Sat Sun` is not any RRULE: it means "the Monday of the week the
|
|
12
|
+
# 13th falls in, unless that lands on an omitted day, in which case do not
|
|
13
|
+
# trigger at all", and BYDAY has no way to say the second half.
|
|
14
|
+
#
|
|
15
|
+
# So the rule is a guess, and the guess is checked. `Remind::Reminder` can
|
|
16
|
+
# produce the dates the reminder actually fires on -- they come from
|
|
17
|
+
# ComputeTrigger, which is what `remind` itself runs -- so the candidate
|
|
18
|
+
# RRULE is expanded with ice_cube and compared against them. If they agree
|
|
19
|
+
# for the whole horizon, the event carries the rule, and every calendar that
|
|
20
|
+
# reads it recurs forever, correctly. If they disagree anywhere, the event
|
|
21
|
+
# carries Remind's dates instead: fewer of them, but none of them wrong.
|
|
22
|
+
#
|
|
23
|
+
# That check is the reason this converter is built on bindings rather than
|
|
24
|
+
# on a parser of its own. Without Remind there is nothing to check against,
|
|
25
|
+
# and every previous converter has had to guess and hope.
|
|
26
|
+
class Recurrence
|
|
27
|
+
# How many occurrences the guess has to get right. Ten years of a monthly
|
|
28
|
+
# reminder, or two of a weekly one -- far enough out for the leap years,
|
|
29
|
+
# the month lengths and the moving holidays that break a rule to have
|
|
30
|
+
# shown up.
|
|
31
|
+
DEFAULT_HORIZON = 120
|
|
32
|
+
|
|
33
|
+
# A rule that agreed with Remind for the whole horizon.
|
|
34
|
+
Rule = Struct.new(:text, :dates) do
|
|
35
|
+
def rule?
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Remind's dates, for a reminder no rule describes.
|
|
41
|
+
Dates = Struct.new(:dates) do
|
|
42
|
+
def rule?
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def text
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
attr_reader :reminder, :horizon
|
|
52
|
+
|
|
53
|
+
def initialize(reminder, horizon: DEFAULT_HORIZON)
|
|
54
|
+
@reminder = reminder
|
|
55
|
+
@horizon = horizon
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call
|
|
59
|
+
if single?
|
|
60
|
+
Dates.new(remind_dates)
|
|
61
|
+
elsif agrees?
|
|
62
|
+
Rule.new(candidate, remind_dates)
|
|
63
|
+
else
|
|
64
|
+
Dates.new(remind_dates)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A reminder Remind only ever triggers once needs no rule to describe it,
|
|
69
|
+
# whatever its trigger looks like. Asking Remind is more reliable than
|
|
70
|
+
# reading the trigger: `REM 25 Dec 2027` and `REM 1 Jan 2027 *14 UNTIL 14
|
|
71
|
+
# Jan 2027` both happen once, and only one of them looks like it.
|
|
72
|
+
def single?
|
|
73
|
+
remind_dates.length <= 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The dates Remind says the reminder fires on, bounded by the horizon.
|
|
77
|
+
def remind_dates
|
|
78
|
+
@remind_dates ||= reminder.occurrences(limit: horizon).to_a
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The RRULE this reminder looks like it means, before anyone checks.
|
|
82
|
+
#
|
|
83
|
+
# The shape is decided by which parts of the date the trigger left out,
|
|
84
|
+
# because that is how Remind's trigger language works: `REM 15` says
|
|
85
|
+
# nothing about the month, so it happens every month.
|
|
86
|
+
def candidate
|
|
87
|
+
@candidate ||= bounded(frequency)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def frequency
|
|
93
|
+
if reminder.repeat
|
|
94
|
+
"FREQ=DAILY;INTERVAL=#{reminder.repeat}"
|
|
95
|
+
elsif weekday_and_day?
|
|
96
|
+
"FREQ=MONTHLY;BYDAY=#{weekdays};#{week_containing(reminder.day)}"
|
|
97
|
+
elsif weekday_only?
|
|
98
|
+
"FREQ=WEEKLY;BYDAY=#{weekdays}#{in_month}"
|
|
99
|
+
elsif last_day_of_month?
|
|
100
|
+
"FREQ=MONTHLY;BYMONTHDAY=-#{reminder.back}"
|
|
101
|
+
elsif yearly?
|
|
102
|
+
"FREQ=YEARLY;BYMONTH=#{reminder.month};BYMONTHDAY=#{reminder.day}"
|
|
103
|
+
elsif monthly?
|
|
104
|
+
"FREQ=MONTHLY;BYMONTHDAY=#{reminder.day}"
|
|
105
|
+
elsif within_month?
|
|
106
|
+
"FREQ=DAILY;BYMONTH=#{reminder.month}"
|
|
107
|
+
else
|
|
108
|
+
"FREQ=DAILY"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def weekdays
|
|
113
|
+
reminder.weekdays.join(",")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def weekday_only?
|
|
117
|
+
reminder.weekdays.any? && reminder.day.nil?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def weekday_and_day?
|
|
121
|
+
reminder.weekdays.any? && !reminder.day.nil?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# `REM Mon Jun` is every Monday, but only in June.
|
|
125
|
+
def in_month
|
|
126
|
+
if reminder.month
|
|
127
|
+
";BYMONTH=#{reminder.month}"
|
|
128
|
+
else
|
|
129
|
+
""
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `REM Wed 15` is the Wednesday on or after the 15th, which is not the
|
|
134
|
+
# third Wednesday of the month -- in a month whose 1st is a Wednesday
|
|
135
|
+
# they are a week apart. What it is, exactly, is the Wednesday inside
|
|
136
|
+
# the seven days starting on the 15th, and BYMONTHDAY can say that.
|
|
137
|
+
def week_containing(day)
|
|
138
|
+
"BYMONTHDAY=#{(day..(day + 6)).to_a.join(",")}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# `REM 1 -1` is the 1st, counted back one day: the last day of the month
|
|
142
|
+
# before. BYMONTHDAY counts back from the end of the month the same way.
|
|
143
|
+
def last_day_of_month?
|
|
144
|
+
reminder.day == 1 && reminder.back.to_i > 0
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def yearly?
|
|
148
|
+
reminder.day && reminder.month
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def monthly?
|
|
152
|
+
reminder.day && reminder.month.nil?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# `REM Feb` is every day in February.
|
|
156
|
+
def within_month?
|
|
157
|
+
reminder.day.nil? && reminder.month
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Where the series stops, if it does.
|
|
161
|
+
#
|
|
162
|
+
# An UNTIL clause says so outright. So does a year in a trigger that
|
|
163
|
+
# leaves the day or the month out: `REM Sat Sun 2021` is every weekend
|
|
164
|
+
# *in 2021*, and a rule without an UNTIL would go on for ever. A year in
|
|
165
|
+
# a fully-specified trigger is a start date rather than a bound, and a
|
|
166
|
+
# `*n` repeat starts from its date rather than being confined to it, so
|
|
167
|
+
# neither of those closes anything.
|
|
168
|
+
#
|
|
169
|
+
# Failing all that, a walk that ran out before the horizon says the
|
|
170
|
+
# reminder stops on its own, and where.
|
|
171
|
+
def bounded(rule)
|
|
172
|
+
last = reminder.until_date || year_end || ran_out
|
|
173
|
+
|
|
174
|
+
if last
|
|
175
|
+
"#{rule};UNTIL=#{last.strftime("%Y%m%d")}"
|
|
176
|
+
else
|
|
177
|
+
rule
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def year_end
|
|
182
|
+
if reminder.year && !reminder.repeat && partial?
|
|
183
|
+
Date.new(reminder.year, 12, 31)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def partial?
|
|
188
|
+
reminder.day.nil? || reminder.month.nil?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def ran_out
|
|
192
|
+
if remind_dates.length < horizon
|
|
193
|
+
remind_dates.last
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# --- the check --------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def agrees?
|
|
200
|
+
expanded == remind_dates
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The candidate rule's own dates, seeded at the first date Remind gives,
|
|
204
|
+
# since DTSTART is where a calendar starts expanding it.
|
|
205
|
+
def expanded
|
|
206
|
+
if parsed_rule
|
|
207
|
+
schedule(parsed_rule).first(remind_dates.length).map(&:to_date)
|
|
208
|
+
else
|
|
209
|
+
[]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# A rule nobody can expand is a rule this converter will not emit. The
|
|
214
|
+
# rescue is around the parse alone: an error anywhere else is a bug
|
|
215
|
+
# here, and swallowing it would turn every bug into a silent fallback.
|
|
216
|
+
def parsed_rule
|
|
217
|
+
IceCube::Rule.from_ical(candidate)
|
|
218
|
+
rescue ArgumentError, RangeError
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def schedule(rule)
|
|
223
|
+
start = remind_dates.first
|
|
224
|
+
|
|
225
|
+
IceCube::Schedule.new(Time.new(start.year, start.month, start.day)) do |built|
|
|
226
|
+
built.add_recurrence_rule(rule)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
__END__
|
|
233
|
+
|
|
234
|
+
require "remind"
|
|
235
|
+
|
|
236
|
+
describe "Rem2ics::Recurrence" do
|
|
237
|
+
session = Remind::Session.new
|
|
238
|
+
session.today = Date.new(2026, 8, 19)
|
|
239
|
+
|
|
240
|
+
recurrence = proc do |line, horizon = 12|
|
|
241
|
+
Rem2ics::Recurrence.new(Remind::Reminder.parse(line, session: session), horizon: horizon).call
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
describe "reminders an RRULE describes" do
|
|
245
|
+
it "makes a weekly reminder a weekly rule" do
|
|
246
|
+
result = recurrence.("REM Mon MSG gym")
|
|
247
|
+
|
|
248
|
+
result.should.be.rule
|
|
249
|
+
result.text.should == "FREQ=WEEKLY;BYDAY=MO"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "makes a several-weekday reminder one rule" do
|
|
253
|
+
recurrence.("REM Mon Wed Fri MSG gym").text.should == "FREQ=WEEKLY;BYDAY=MO,WE,FR"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "makes a day-of-month reminder a monthly rule" do
|
|
257
|
+
recurrence.("REM 15 MSG rent").text.should == "FREQ=MONTHLY;BYMONTHDAY=15"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "makes a day-and-month reminder a yearly rule" do
|
|
261
|
+
recurrence.("REM 25 Dec MSG christmas").text.should ==
|
|
262
|
+
"FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "makes a repeat a daily rule with an interval" do
|
|
266
|
+
recurrence.("REM 1 Jan 2027 *14 MSG fortnightly").text.should.start_with "FREQ=DAILY;INTERVAL=14"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "bounds a rule by the year the trigger names" do
|
|
270
|
+
# `REM Sat Sun 2031` is every weekend in 2031, and a rule without an
|
|
271
|
+
# UNTIL would run for ever.
|
|
272
|
+
recurrence.("REM Sat Sun 2031 MSG weekends", 8).text
|
|
273
|
+
.should == "FREQ=WEEKLY;BYDAY=SA,SU;UNTIL=20311231"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "narrows a weekly rule to the month the trigger names" do
|
|
277
|
+
recurrence.("REM Mon Jun MSG summer mondays").text
|
|
278
|
+
.should == "FREQ=WEEKLY;BYDAY=MO;BYMONTH=6"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "counts back from the end of the month when the trigger does" do
|
|
282
|
+
recurrence.("REM 1 -1 MSG last day").text.should == "FREQ=MONTHLY;BYMONTHDAY=-1"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it "carries an UNTIL through" do
|
|
286
|
+
recurrence.("REM 1 Jan 2027 *7 UNTIL 1 Mar 2027 MSG weekly").text
|
|
287
|
+
.should.include "UNTIL=20270301"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "agrees with Remind, which is the only reason it is a rule at all" do
|
|
291
|
+
result = recurrence.("REM 15 MSG rent")
|
|
292
|
+
|
|
293
|
+
result.dates.first(3).map(&:to_s).should == %w[2026-09-15 2026-10-15 2026-11-15]
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
describe "reminders no RRULE describes" do
|
|
298
|
+
it "falls back to Remind's dates when the reminder skips omitted days" do
|
|
299
|
+
# 1 March 2031 is a Saturday; this reminder does not fire that year.
|
|
300
|
+
result = recurrence.("REM 1 Mar SKIP OMIT Sat Sun MSG payday", 6)
|
|
301
|
+
|
|
302
|
+
result.rule?.should.be.false
|
|
303
|
+
result.dates.map(&:to_s).should ==
|
|
304
|
+
%w[2027-03-01 2028-03-01 2029-03-01 2030-03-01 2032-03-01 2033-03-01]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it "falls back when a trigger names several weekdays and a day" do
|
|
308
|
+
# `REM Wed Thu 15` fires on the *first* of those weekdays on or after
|
|
309
|
+
# the 15th -- one date a month, not two. BYDAY=WE,TH says both, so the
|
|
310
|
+
# check rejects it and the dates go in instead.
|
|
311
|
+
result = recurrence.("REM Wed Thu 15 MSG deadline", 4)
|
|
312
|
+
|
|
313
|
+
result.rule?.should.be.false
|
|
314
|
+
result.dates.map(&:to_s).should == %w[2026-08-19 2026-09-16 2026-10-15 2026-11-18]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it "keeps a rule when one weekday and a day do line up" do
|
|
318
|
+
result = recurrence.("REM Mon 15 MSG deadline", 4)
|
|
319
|
+
|
|
320
|
+
result.should.be.rule
|
|
321
|
+
result.text.should == "FREQ=MONTHLY;BYDAY=MO;BYMONTHDAY=15,16,17,18,19,20,21"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "keeps the dates rather than a rule that would invent an occurrence" do
|
|
325
|
+
result = recurrence.("REM 1 Mar SKIP OMIT Sat Sun MSG payday", 6)
|
|
326
|
+
|
|
327
|
+
result.dates.map(&:to_s).should.not.include "2031-03-01"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
describe "reminders that happen once" do
|
|
332
|
+
it "needs no rule for a reminder Remind triggers once" do
|
|
333
|
+
result = recurrence.("REM 25 Dec 2027 MSG christmas")
|
|
334
|
+
|
|
335
|
+
result.rule?.should.be.false
|
|
336
|
+
result.dates.map(&:to_s).should == %w[2027-12-25]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "asks Remind rather than reading the trigger" do
|
|
340
|
+
# Fully specified, and still repeating.
|
|
341
|
+
recurrence.("REM 1 Jan 2027 *14 MSG fortnightly").should.be.rule
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
data/lib/rem2ics.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rem2ics/cli"
|
|
4
|
+
require_relative "rem2ics/converter"
|
|
5
|
+
require_relative "rem2ics/event"
|
|
6
|
+
require_relative "rem2ics/recurrence"
|
|
7
|
+
require_relative "rem2ics/version"
|
|
8
|
+
|
|
9
|
+
# Reminder files to iCalendar.
|
|
10
|
+
#
|
|
11
|
+
# This is a port of Martin Michel's remmy.pl, and the interesting difference
|
|
12
|
+
# is not that it is Ruby. remmy.pl read the reminder language with regular
|
|
13
|
+
# expressions -- as every converter before it did -- and got some of it right.
|
|
14
|
+
# There is a lot of that language: eighteen worked examples in Remind's manual
|
|
15
|
+
# before the exceptions start, weekday-and-day triggers that are not what they
|
|
16
|
+
# look like, SKIP and OMIT and SCANFROM and BEFORE and AFTER.
|
|
17
|
+
#
|
|
18
|
+
# rem2ics does not read it at all. It is built on the remind-rb bindings, so
|
|
19
|
+
# Remind parses the trigger, Remind computes the dates it fires on, and Remind
|
|
20
|
+
# renders the message. What is left for this side is the mapping to
|
|
21
|
+
# iCalendar's vocabulary, and one judgement:
|
|
22
|
+
#
|
|
23
|
+
# A recurring reminder becomes an event with an RRULE -- but only when that
|
|
24
|
+
# RRULE has been expanded and checked, occurrence by occurrence, against the
|
|
25
|
+
# dates Remind gives. Where the two agree the calendar recurs forever and
|
|
26
|
+
# correctly. Where they disagree -- a reminder that skips holidays, one that
|
|
27
|
+
# moves off weekends -- the event carries Remind's own dates instead.
|
|
28
|
+
#
|
|
29
|
+
# So the output is never wrong about when something happens; at worst it is
|
|
30
|
+
# finite where the reminder is not.
|
|
31
|
+
module Rem2ics
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rem2ics
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan Kidd
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: icalendar
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.12'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.12'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ice_cube
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.17'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.17'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: remind-rb
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '6.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '6.2'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.88'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.88'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: scampi
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
description: |
|
|
97
|
+
Rem2ics turns the reminder files used by Remind into iCalendar, the format
|
|
98
|
+
Outlook, Apple Calendar and Google Calendar import.
|
|
99
|
+
|
|
100
|
+
It does not parse the reminder language. Remind does: rem2ics is built on
|
|
101
|
+
the remind-rb bindings, so the trigger is parsed by ParseRem, the dates it
|
|
102
|
+
fires on come from ComputeTrigger, and the message is rendered by DoSubst
|
|
103
|
+
with its substitutions expanded. A recurring reminder becomes one event
|
|
104
|
+
with an RRULE -- but only when the RRULE has been expanded and checked
|
|
105
|
+
against the dates Remind gives; when the two disagree, as they do for a
|
|
106
|
+
reminder that skips holidays, the event carries Remind's dates instead.
|
|
107
|
+
email: nathanblenheimkidd@gmail.com
|
|
108
|
+
executables:
|
|
109
|
+
- rem2ics
|
|
110
|
+
extensions: []
|
|
111
|
+
extra_rdoc_files:
|
|
112
|
+
- docs/rem2ics.md
|
|
113
|
+
files:
|
|
114
|
+
- LICENSE
|
|
115
|
+
- docs/rem2ics.md
|
|
116
|
+
- exe/rem2ics
|
|
117
|
+
- lib/rem2ics.rb
|
|
118
|
+
- lib/rem2ics/cli.rb
|
|
119
|
+
- lib/rem2ics/converter.rb
|
|
120
|
+
- lib/rem2ics/event.rb
|
|
121
|
+
- lib/rem2ics/recurrence.rb
|
|
122
|
+
- lib/rem2ics/version.rb
|
|
123
|
+
homepage: https://dianne.skoll.ca/projects/remind/
|
|
124
|
+
licenses:
|
|
125
|
+
- GPL-2.0-only
|
|
126
|
+
metadata: {}
|
|
127
|
+
rdoc_options: []
|
|
128
|
+
require_paths:
|
|
129
|
+
- lib
|
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '3.3'
|
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '0'
|
|
140
|
+
requirements: []
|
|
141
|
+
rubygems_version: 3.7.2
|
|
142
|
+
specification_version: 4
|
|
143
|
+
summary: Convert Remind reminder files to iCalendar (.ics)
|
|
144
|
+
test_files: []
|