mhc 1.0.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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/COPYRIGHT +28 -0
  6. data/Gemfile +8 -0
  7. data/README.org +209 -0
  8. data/Rakefile +13 -0
  9. data/bin/mhc +312 -0
  10. data/emacs/Cask +25 -0
  11. data/emacs/Makefile +58 -0
  12. data/emacs/mhc-calendar.el +1723 -0
  13. data/emacs/mhc-calfw.el +135 -0
  14. data/emacs/mhc-compat.el +90 -0
  15. data/emacs/mhc-date.el +642 -0
  16. data/emacs/mhc-day.el +149 -0
  17. data/emacs/mhc-db.el +158 -0
  18. data/emacs/mhc-draft.el +211 -0
  19. data/emacs/mhc-e21.el +167 -0
  20. data/emacs/mhc-face.el +236 -0
  21. data/emacs/mhc-file.el +224 -0
  22. data/emacs/mhc-guess.el +648 -0
  23. data/emacs/mhc-header.el +176 -0
  24. data/emacs/mhc-logic.el +563 -0
  25. data/emacs/mhc-message.el +130 -0
  26. data/emacs/mhc-minibuf.el +466 -0
  27. data/emacs/mhc-misc.el +248 -0
  28. data/emacs/mhc-mua.el +260 -0
  29. data/emacs/mhc-parse.el +286 -0
  30. data/emacs/mhc-process.el +35 -0
  31. data/emacs/mhc-ps.el +1174 -0
  32. data/emacs/mhc-record.el +201 -0
  33. data/emacs/mhc-schedule.el +202 -0
  34. data/emacs/mhc-summary.el +763 -0
  35. data/emacs/mhc-sync.el +158 -0
  36. data/emacs/mhc-vars.el +149 -0
  37. data/emacs/mhc.el +1114 -0
  38. data/icons/Anniversary.xbm +6 -0
  39. data/icons/Anniversary.xpm +27 -0
  40. data/icons/Birthday.xbm +6 -0
  41. data/icons/Birthday.xpm +25 -0
  42. data/icons/Business.xbm +6 -0
  43. data/icons/Business.xpm +24 -0
  44. data/icons/CheckBox.xbm +6 -0
  45. data/icons/CheckBox.xpm +24 -0
  46. data/icons/CheckedBox.xbm +6 -0
  47. data/icons/CheckedBox.xpm +25 -0
  48. data/icons/Conflict.xbm +6 -0
  49. data/icons/Conflict.xpm +22 -0
  50. data/icons/Date.xbm +6 -0
  51. data/icons/Date.xpm +29 -0
  52. data/icons/Holiday.xbm +6 -0
  53. data/icons/Holiday.xpm +25 -0
  54. data/icons/Link.xbm +6 -0
  55. data/icons/Link.xpm +25 -0
  56. data/icons/Other.xbm +6 -0
  57. data/icons/Other.xpm +28 -0
  58. data/icons/Party.xbm +6 -0
  59. data/icons/Party.xpm +23 -0
  60. data/icons/Private.xbm +6 -0
  61. data/icons/Private.xpm +26 -0
  62. data/icons/Recurrence.xbm +6 -0
  63. data/icons/Recurrence.xpm +98 -0
  64. data/icons/Vacation.xbm +6 -0
  65. data/icons/Vacation.xpm +26 -0
  66. data/lib/mhc.rb +45 -0
  67. data/lib/mhc/builder.rb +64 -0
  68. data/lib/mhc/caldav.rb +304 -0
  69. data/lib/mhc/calendar.rb +106 -0
  70. data/lib/mhc/command.rb +13 -0
  71. data/lib/mhc/command/cache.rb +14 -0
  72. data/lib/mhc/command/completions.rb +108 -0
  73. data/lib/mhc/command/init.rb +133 -0
  74. data/lib/mhc/command/scan.rb +33 -0
  75. data/lib/mhc/command/sync.rb +22 -0
  76. data/lib/mhc/config.rb +229 -0
  77. data/lib/mhc/converter.rb +330 -0
  78. data/lib/mhc/datastore.rb +164 -0
  79. data/lib/mhc/date_enumerator.rb +274 -0
  80. data/lib/mhc/date_frame.rb +124 -0
  81. data/lib/mhc/date_helper.rb +49 -0
  82. data/lib/mhc/etag.rb +68 -0
  83. data/lib/mhc/event.rb +396 -0
  84. data/lib/mhc/formatter.rb +312 -0
  85. data/lib/mhc/logger.rb +94 -0
  86. data/lib/mhc/modifier.rb +149 -0
  87. data/lib/mhc/occurrence.rb +94 -0
  88. data/lib/mhc/occurrence_enumerator.rb +113 -0
  89. data/lib/mhc/property_value.rb +33 -0
  90. data/lib/mhc/property_value/date.rb +190 -0
  91. data/lib/mhc/property_value/integer.rb +15 -0
  92. data/lib/mhc/property_value/list.rb +41 -0
  93. data/lib/mhc/property_value/period.rb +49 -0
  94. data/lib/mhc/property_value/range.rb +100 -0
  95. data/lib/mhc/property_value/recurrence_condition.rb +272 -0
  96. data/lib/mhc/property_value/text.rb +11 -0
  97. data/lib/mhc/property_value/time.rb +45 -0
  98. data/lib/mhc/query.rb +210 -0
  99. data/lib/mhc/sync.rb +46 -0
  100. data/lib/mhc/sync/driver.rb +108 -0
  101. data/lib/mhc/sync/status.rb +70 -0
  102. data/lib/mhc/sync/status_manager.rb +142 -0
  103. data/lib/mhc/sync/strategy.rb +233 -0
  104. data/lib/mhc/sync/syncinfo.rb +98 -0
  105. data/lib/mhc/templates/config.yml.erb +142 -0
  106. data/lib/mhc/version.rb +4 -0
  107. data/lib/mhc/webdav.rb +319 -0
  108. data/mhc.gemspec +24 -0
  109. data/samples/DOT.mhc-config.yml +116 -0
  110. data/samples/japanese-holidays.mhcc +153 -0
  111. data/samples/mhc-completions.zsh +11 -0
  112. data/spec/mhc_spec.rb +682 -0
  113. data/spec/spec_helper.rb +9 -0
  114. data/xpm/close.xpm +18 -0
  115. data/xpm/delete.xpm +19 -0
  116. data/xpm/exit.xpm +18 -0
  117. data/xpm/month.xpm +18 -0
  118. data/xpm/next.xpm +18 -0
  119. data/xpm/next2.xpm +18 -0
  120. data/xpm/next_year.xpm +18 -0
  121. data/xpm/open.xpm +19 -0
  122. data/xpm/prev.xpm +18 -0
  123. data/xpm/prev2.xpm +18 -0
  124. data/xpm/prev_year.xpm +18 -0
  125. data/xpm/save.xpm +19 -0
  126. data/xpm/today.xpm +18 -0
  127. metadata +214 -0
@@ -0,0 +1,330 @@
1
+ module Mhc
2
+ class Converter
3
+ class Emacs
4
+ # return cfw:event structure
5
+ #
6
+ # (defstruct cfw:event
7
+ # title ; event title [string]
8
+ # start-date ; start date of the event [cfw:date]
9
+ # start-time ; start time of the event (optional)
10
+ # end-date ; end date of the event [cfw:date] (optional)
11
+ # end-time ; end of the event (optional)
12
+ # description ; event description [string] (optional)
13
+ # location ; location [strting] (optional)
14
+ # source ; [internal] source of the event
15
+ # )
16
+ def to_calfw(ev)
17
+ hash = {
18
+ :title => ev.subject.to_s,
19
+ :start_date => "",
20
+ :start_time => "",
21
+ :end_date => "",
22
+ :end_time => "",
23
+ :description => "",
24
+ :location => "",
25
+ :source => ""
26
+ }
27
+ to_emacs_plist(hash)
28
+ end
29
+
30
+ def to_emacs(obj)
31
+ case obj
32
+ when Array
33
+ to_emacs_list(obj)
34
+ when Hash
35
+ to_emacs_plist(obj)
36
+ else
37
+ to_emacs_string(obj)
38
+ end
39
+ end
40
+
41
+ def to_emacs_symbol(obj)
42
+ ":" + obj.to_s.downcase.gsub('_', '-')
43
+ end
44
+
45
+ def to_emacs_string(str)
46
+ # 1. quote " and \
47
+ # 2. surround by "
48
+ '"' + str.to_s.toutf8.gsub(/[\"\\]/, '\\\\\&') + '"'
49
+ end
50
+
51
+ def to_emacs_plist(hash)
52
+ wrap(hash.map{|key,val| "#{to_emacs_symbol(key)} #{to_emacs(val)}"}.join(" "))
53
+ end
54
+
55
+ def to_emacs_list(array)
56
+ wrap(array.map{|val| to_emacs(val)}.join(" "))
57
+ end
58
+
59
+ private
60
+ def wrap(obj)
61
+ "(" + obj.to_s + ")"
62
+ end
63
+ end # class Emacs
64
+
65
+ class Icalendar
66
+
67
+ def to_ics(event)
68
+ return to_icalendar(event).to_s
69
+ end
70
+
71
+ def to_ics_string(event)
72
+ ical = RiCal.Calendar
73
+ ical.prodid = Mhc::PRODID
74
+ ical.events << to_icalendar(event)
75
+ return ical.to_s
76
+ end
77
+
78
+ def to_icalendar(event)
79
+ icalendar = RiCal.Event do |iev|
80
+ iev.rrule = event.recurrence_condition.to_ics(dtstart(event), event.duration.last) if event.recurring?
81
+ iev.exdates = [exdates(event)] if exdates(event)
82
+ iev.rdates = [rdates(event)] if rdates(event)
83
+ iev.created = created(event).utc.strftime("%Y%m%dT%H%M%SZ")
84
+ iev.categories = event.categories.to_a unless event.categories.empty?
85
+ iev.location = event.location.to_s unless event.location.to_s.empty?
86
+ iev.last_modified = last_modified(event).utc.strftime("%Y%m%dT%H%M%SZ")
87
+ iev.uid = event.uid.to_s
88
+ iev.dtstart = dtstart(event)
89
+ iev.dtend = dtend(event)
90
+ iev.summary = event.subject.to_s
91
+ iev.description = event.description.to_s
92
+ iev.sequence = (event.sequence.to_i || 0)
93
+ iev.dtstamp = ::Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
94
+ iev.add_x_property("X-SC-Recurrence-Tag", event.recurrence_tag.to_s) if event.recurrence_tag.to_s != ""
95
+ iev.add_x_property("X-SC-Mission-Tag", event.mission_tag.to_s) if event.mission_tag.to_s != ""
96
+ end
97
+ return icalendar
98
+ end
99
+
100
+ ################################################################
101
+ private
102
+
103
+ # DTSTART (RFC5445:iCalendar) has these two meanings:
104
+ # 1) first ocurrence date of recurrence events
105
+ # 2) start date of a single-shot event
106
+ #
107
+ # In MHC, DTSTART should be calculated as:
108
+ #
109
+ # if a MHC article has a Cond: field,
110
+ # + DTSTART is calculated from Duration: and Cond: field.
111
+ # + Additional Day: field is recognized as RDATE.
112
+ # else
113
+ # + DTSTART is calculated from a first entry of Days: field.
114
+ # + Remains in Day: field is recognized as RDATE.
115
+ # end
116
+ #
117
+ def dtstart(event)
118
+ if event.recurring?
119
+ Mhc::OccurrenceEnumerator.new(event, empty_dates, empty_dates, event.recurrence_condition, event.duration).first.dtstart
120
+ else
121
+ Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).first.dtstart
122
+ end
123
+ end
124
+
125
+ def dtend(event)
126
+ if event.recurring?
127
+ Mhc::OccurrenceEnumerator.new(event, empty_dates, empty_dates, event.recurrence_condition, event.duration).first.dtend
128
+ else
129
+ Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).first.dtend
130
+ end
131
+ end
132
+
133
+ def rdates(event)
134
+ return nil if event.dates.empty?
135
+ ocs = Mhc::OccurrenceEnumerator.new(event, event.dates, empty_dates, empty_condition, empty_duration).map {|oc| oc.dtstart}
136
+ if event.recurring?
137
+ ocs
138
+ else
139
+ ocs = ocs[1..-1]
140
+ return nil if ocs.empty?
141
+ return ocs
142
+ end
143
+ end
144
+
145
+ def exdates(event)
146
+ return nil if event.exceptions.empty?
147
+ ocs = Mhc::OccurrenceEnumerator.new(event, event.exceptions, empty_dates, empty_condition, empty_duration).map {|oc| oc.dtstart }
148
+ return ocs
149
+ end
150
+
151
+ def empty_duration
152
+ Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date)
153
+ end
154
+
155
+ def empty_dates
156
+ Mhc::PropertyValue::List.new(Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Date.new))
157
+ end
158
+
159
+ def empty_condition
160
+ Mhc::PropertyValue::RecurrenceCondition.new
161
+ end
162
+
163
+ def created(event)
164
+ if event.path
165
+ File.ctime(event.path)
166
+ else
167
+ ::Time.utc(2014, 1, 1)
168
+ end
169
+ end
170
+
171
+ def last_modified(event)
172
+ if event.path
173
+ File.mtime(event.path)
174
+ else
175
+ ::Time.utc(2014, 1, 1)
176
+ end
177
+ end
178
+ end # class Icalendar
179
+
180
+ class IcalendarImporter
181
+ def self.parse_ics(ics)
182
+ # * 3.8.1. Descriptive Component Properties
183
+ # ** CATEGORIES 3.8.1.2. Categories
184
+ # ** DESCRIPTION 3.8.1.5. Description
185
+ # ** LOCATION 3.8.1.7. Location
186
+ # ** SUMMARY 3.8.1.12. Summary
187
+ # * 3.8.2. Date and Time Component Properties
188
+ # ** DTEND 3.8.2.2. Date-Time End
189
+ # ** DTSTART 3.8.2.4. Date-Time Start
190
+ # ** DURATION 3.8.2.5. Duration
191
+ # * 3.8.4. Relationship Component Properties
192
+ # ** RECURRENCE-ID 3.8.4.4. Recurrence ID
193
+ # * 3.8.5. Recurrence Component Properties
194
+ # ** EXDATE 3.8.5.1. Exception Date-Times
195
+ # ** RDATE 3.8.5.2. Recurrence Date-Times
196
+ # ** RRULE 3.8.5.3. Recurrence Rule
197
+ # * 3.8.7. Change Management Component Properties
198
+ # ** SEQUENCE 3.8.7.4. Sequence Number
199
+ # * 3.8.8. Miscellaneous Component Properties
200
+ # ** X-FIELD 3.8.8.2. Non-Standard Properties
201
+
202
+ # DTSTART:
203
+ # Date part => X-SC-Duration: .first
204
+ # Time part => X-SC-Time: .first
205
+ # DTEND
206
+ # Date part =>
207
+ # DTEND - DTSTART = 1day
208
+ # DTEND - DTSTART > 1days
209
+ # Day:
210
+ # RRULE:
211
+ # X-SC-Cond:
212
+ # UNTIL:
213
+ # X-SC-Duration: .last
214
+ # RDATES:
215
+ # X-SC-Day:
216
+ # EXDATES:
217
+ # X-SC-Day: !YYYYMMDD
218
+ #
219
+ ical = RiCal.parse_string(ics).first
220
+ return nil unless ical
221
+
222
+ iev = ical.events.first
223
+ allday = !iev.dtstart.respond_to?(:hour)
224
+ recurring = !iev.rrule.empty?
225
+
226
+ # X-SC-Day: (from DTSTART, DTEND)
227
+ # for recurring event, DTSTSRT is a start part of X-SC-Duration:
228
+ dates = []
229
+ unless recurring
230
+ date = tz_convert(iev.dtstart).strftime("%Y%m%d")
231
+ if allday && (iev.dtend - iev.dtstart).to_i > 1
232
+ date += "-" + (iev.dtend - 1).to_time.strftime("%Y%m%d")
233
+ end
234
+ dates << date
235
+ end
236
+
237
+ # X-SC-Day: (from RDATE, EXDATE)
238
+ dates += iev.rdate.flatten.map{|d| d.to_time.strftime("%Y%m%d")}
239
+ exdates = iev.exdate.flatten.map{|d| d.to_time.strftime("!%Y%m%d")}
240
+
241
+ # X-SC-Time:
242
+ unless allday
243
+ time = tz_convert(iev.dtstart).strftime("%H:%M")
244
+ if iev.dtend
245
+ time += "-" + tz_convert(iev.dtend).strftime("%H:%M")
246
+ end
247
+ end
248
+
249
+ ev = Mhc::Event.parse "X-SC-Subject: #{iev.summary}\n" +
250
+ "X-SC-Location: #{iev.location}\n" +
251
+ "X-SC-Day: #{(dates + exdates).join(' ')}\n" +
252
+ "X-SC-Time: #{time}\n" +
253
+ "X-SC-Category: #{iev.categories.to_a.join(' ')}\n" +
254
+ "X-SC-Mission-Tag: #{iev.x_sc_mission_tag.first}\n" +
255
+ "X-SC-Recurrence-Tag: #{iev.x_sc_recurrence_tag.first}\n" +
256
+ "X-SC-Cond: \n" +
257
+ "X-SC-Duration: \n" +
258
+ "X-SC-Alarm: \n" +
259
+ "X-SC-Record-Id: #{iev.uid}\n" +
260
+ "X-SC-Sequence: #{iev.sequence.to_i}\n\n" + iev.description.to_s +
261
+ if $MHC_DEBUG_FOR_DEVELOPER # FIXME: should introduce good logger and debug scheme
262
+ ical.to_s.force_encoding("ASCII-8BIT").gsub(/\r\n/, "\n")
263
+ else
264
+ ""
265
+ end
266
+
267
+ # X-SC-Cond:
268
+ ev.recurrence_condition.set_from_ics(iev.rrule.first, tz_convert(iev.dtstart))
269
+
270
+ # X-SC-Duration: is only for recurring articles
271
+ if recurring
272
+ duration_string = tz_convert(iev.dtstart).strftime("%Y%m%d") + "-"
273
+ if iev.rrule.first.to_s.match(/until=([^;]+)/i)
274
+ duration_string += parse_ical_datetime($1).strftime("%Y%m%d")
275
+ end
276
+ ev.duration = duration_string
277
+ end
278
+
279
+ return ev
280
+ end
281
+
282
+ private
283
+
284
+ def self.tz_convert(datetime, src_tzid: nil, dst_tzid: nil)
285
+ return datetime unless datetime.respond_to?(:hour)
286
+
287
+ dst_tzid ||= Mhc.default_tzid
288
+ src_tzid ||= if datetime.respond_to?(:tzid) and datetime.tzid
289
+ datetime.tzid
290
+ else
291
+ Mhc.default_tzid
292
+ end
293
+ dst_tz = TZInfo::Timezone.get(dst_tzid)
294
+ src_tz = TZInfo::Timezone.get(src_tzid)
295
+
296
+ utc = Time.utc(datetime.year, datetime.month, datetime.day,
297
+ datetime.hour, datetime.min, datetime.sec)
298
+
299
+ time1 = src_tz.local_to_utc(utc)
300
+ time1.tzid = src_tzid if time1.respond_to?(:tzid)
301
+
302
+ time = dst_tz.utc_to_local(time1)
303
+ time.tzid = dst_tzid if time.respond_to?(:tzid)
304
+
305
+ return time
306
+ end
307
+
308
+ def self.parse_ical_datetime(datetime_string, dst_tzid = nil)
309
+ src_tzid = case datetime_string
310
+ when /TZID=([^;]+)/
311
+ $1
312
+ when /\d{8}T\d{6}Z/
313
+ "UTC"
314
+ else
315
+ Mhc.default_tzid
316
+ end
317
+
318
+ dst_tzid ||= Mhc.default_tzid
319
+
320
+ if /^(\d{4})(\d\d)(\d\d)(?:T(\d\d)(\d\d)(\d\d)Z?)?$/ =~ datetime_string
321
+ time = Time.utc($1, $2, $3, $4, $5, $6)
322
+ return tz_convert(time, src_tzid: src_tzid, dst_tzid: dst_tzid)
323
+ else
324
+ raise ArgumentError
325
+ end
326
+ end
327
+
328
+ end # IcalendarImporter
329
+ end # Converter
330
+ end # Mhc
@@ -0,0 +1,164 @@
1
+ require "fileutils"
2
+
3
+ module Mhc
4
+ class DataStore
5
+ def initialize(basedir)
6
+ unless basedir and File.directory?(File.expand_path(basedir.to_s))
7
+ raise Mhc::ConfigurationError, "datastore directory '#{basedir}' not found"
8
+ end
9
+ @basedir = Pathname.new(File.expand_path(basedir))
10
+ @cache = Cache.new(File.expand_path("status/cache/events.pstore", @basedir))
11
+ end
12
+
13
+ def entries(date_range = nil)
14
+ if date_range
15
+ int_range = date_range.min.absolute_from_epoch .. date_range.max.absolute_from_epoch
16
+ end
17
+
18
+ Enumerator.new do |yielder|
19
+ ["inbox", "spool", "presets"].each do |slot|
20
+ dir = File.expand_path(slot, @basedir)
21
+ next unless File.directory?(dir)
22
+
23
+ Dir.chdir(dir) do
24
+ Dir.foreach(".") do |ent|
25
+ parse_mhcc(ent).each {|ev| yielder << ev} if /\.mhcc$/ =~ ent
26
+ next unless /\.mhc$/ =~ ent
27
+ uid = $`
28
+ cache_entry = @cache.lookup(uid, ent)
29
+ if !date_range || cache_entry.involved?(int_range)
30
+ yielder << Event.parse_file(File.expand_path(ent))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ @cache.save
36
+ end
37
+ end
38
+
39
+ def logger
40
+ @logger ||= Mhc::Logger.new(@logfile)
41
+ end
42
+
43
+ def find_by_uid(uid)
44
+ path = find_path(uid)
45
+ return nil unless path
46
+ return Event.parse_file(path)
47
+ end
48
+
49
+ def create(event)
50
+ if find_by_uid(event.uid)
51
+ raise "Already exist uid:#{uid} in #{@basedir}"
52
+ end
53
+ File.open(path, "w") do |f|
54
+ f.write(event.dump)
55
+ end
56
+ end
57
+
58
+ def update(event)
59
+ unless path = uid_to_path(event.uid)
60
+ raise "Not found uid:#{uid} in #{@basedir}"
61
+ end
62
+ File.open(path, "w") do |f|
63
+ f.write(event.dump)
64
+ end
65
+ end
66
+
67
+ def delete(uid_or_event)
68
+ uid = if uid_or_event.respond_to?(:uid)
69
+ uid_or_event.uid
70
+ else
71
+ uid_or_event
72
+ end
73
+ if path = find_path(uid)
74
+ File.delete(path)
75
+ else
76
+ raise "Not found uid:#{uid} in #{@basedir}"
77
+ end
78
+ end
79
+
80
+ ################################################################
81
+ private
82
+
83
+ def parse_mhcc(filename)
84
+ string = File.open(filename).read.scrub.gsub(/^\s*#.*$/, "").strip
85
+ string.split(/\n\n\n*/).map do |header|
86
+ Event.parse(header)
87
+ end
88
+ end
89
+
90
+ def find_path(uid)
91
+ glob = @basedir + ('**/' + uid + '.mhc')
92
+ return Dir.glob(glob).first
93
+ end
94
+
95
+ def uid_to_path(uid)
96
+ return @basedir + ('spool/' + uid + '.mhc')
97
+ end
98
+
99
+ end # class DataStore
100
+ end # module Mhc
101
+
102
+ module Mhc
103
+ class DataStore
104
+ class Cache
105
+ require 'pstore'
106
+
107
+ def initialize(cache_filename)
108
+ @pstore = PStore.new(cache_filename)
109
+ load
110
+ end
111
+
112
+ def lookup(uid, filename)
113
+ unless c = get(uid) and File.mtime(filename).to_i <= c.mtime
114
+ c = CacheEntry.new(filename)
115
+ put(uid, c)
116
+ end
117
+ return c
118
+ end
119
+
120
+ def save
121
+ return self unless @dirty
122
+ @pstore.transaction do
123
+ @pstore["root"] = @db
124
+ end
125
+ @dirty = false
126
+ end
127
+
128
+ private
129
+
130
+ def get(uid)
131
+ @db[uid]
132
+ end
133
+
134
+ def put(uid, value)
135
+ @db[uid] = value
136
+ @dirty = true
137
+ end
138
+
139
+ def load
140
+ @pstore.transaction do
141
+ @db = @pstore["root"] || {}
142
+ end
143
+ @dirty = false
144
+ end
145
+
146
+ end # class Cache
147
+
148
+ class CacheEntry
149
+ attr_reader :mtime, :range
150
+
151
+ def initialize(filename)
152
+ @mtime, event = File.mtime(filename).to_i, Event.parse_file(filename)
153
+ @range = event.range.min.absolute_from_epoch ..
154
+ event.range.max.absolute_from_epoch
155
+ end
156
+
157
+ def involved?(range)
158
+ range.min <= @range.max && @range.min <= range.max
159
+ end
160
+
161
+ end # class CacheEntry
162
+
163
+ end # class DataStore
164
+ end # module Mhc