mhc 1.1.1 → 1.2.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 (48) hide show
  1. checksums.yaml +5 -5
  2. data/bin/mhc +66 -3
  3. data/emacs/Cask +1 -1
  4. data/emacs/mhc-date.el +1 -1
  5. data/emacs/mhc-day.el +1 -1
  6. data/emacs/mhc-db.el +57 -38
  7. data/emacs/mhc-draft.el +36 -22
  8. data/emacs/mhc-face.el +1 -1
  9. data/emacs/mhc-header.el +20 -1
  10. data/emacs/mhc-minibuf.el +12 -7
  11. data/emacs/mhc-parse.el +1 -1
  12. data/emacs/mhc-process.el +26 -9
  13. data/emacs/mhc-ps.el +1 -1
  14. data/emacs/mhc-schedule.el +5 -2
  15. data/emacs/mhc-summary.el +31 -12
  16. data/emacs/mhc-vars.el +15 -2
  17. data/emacs/mhc.el +50 -24
  18. data/lib/mhc.rb +3 -1
  19. data/lib/mhc/builder.rb +5 -1
  20. data/lib/mhc/calendar.rb +5 -1
  21. data/lib/mhc/command/cache.rb +5 -4
  22. data/lib/mhc/converter.rb +3 -2
  23. data/lib/mhc/datastore.rb +52 -13
  24. data/lib/mhc/date_enumerator.rb +2 -2
  25. data/lib/mhc/event.rb +42 -21
  26. data/lib/mhc/formatter.rb +17 -312
  27. data/lib/mhc/formatter/base.rb +125 -0
  28. data/lib/mhc/formatter/emacs.rb +47 -0
  29. data/lib/mhc/formatter/howm.rb +35 -0
  30. data/lib/mhc/formatter/icalendar.rb +17 -0
  31. data/lib/mhc/formatter/json.rb +27 -0
  32. data/lib/mhc/formatter/mail.rb +20 -0
  33. data/lib/mhc/formatter/org_table.rb +24 -0
  34. data/lib/mhc/formatter/symbolic_expression.rb +42 -0
  35. data/lib/mhc/formatter/text.rb +29 -0
  36. data/lib/mhc/occurrence.rb +27 -5
  37. data/lib/mhc/occurrence_enumerator.rb +1 -1
  38. data/lib/mhc/property_value.rb +6 -0
  39. data/lib/mhc/property_value/date.rb +23 -14
  40. data/lib/mhc/property_value/date_time.rb +19 -0
  41. data/lib/mhc/property_value/integer.rb +5 -1
  42. data/lib/mhc/property_value/list.rb +7 -6
  43. data/lib/mhc/property_value/period.rb +3 -1
  44. data/lib/mhc/property_value/range.rb +1 -1
  45. data/lib/mhc/property_value/time.rb +8 -1
  46. data/lib/mhc/version.rb +1 -1
  47. data/spec/mhc_spec.rb +83 -0
  48. metadata +13 -3
@@ -0,0 +1,125 @@
1
+ module Mhc
2
+ class Formatter
3
+ # prepare
4
+ # format_header
5
+ # format_day_header
6
+ # format_item_header
7
+ # format_item
8
+ # format_item_hooter
9
+ # format_day_hooter
10
+ # format_footer
11
+ # teardown
12
+ class Base
13
+ def initialize(date_range:, options:nil)
14
+ @date_range = date_range
15
+ @options = options
16
+ @occurrences, @events, @items = [], [], {}
17
+ @event_hash = {}
18
+ end
19
+
20
+ def <<(occurrence)
21
+ event = occurrence.event
22
+ @occurrences << occurrence
23
+ @events << event unless @event_hash[event]
24
+ @event_hash[event] = true
25
+
26
+ @items[occurrence.date] ||= []
27
+ @items[occurrence.date] << occurrence
28
+ end
29
+
30
+ def to_s
31
+ context = {:items => @items}.merge(@options)
32
+ prepare(context)
33
+ string = format_header(context) + format_body(context) + format_footer(context)
34
+ teardown(context)
35
+ return string
36
+ end
37
+
38
+ ################################################################
39
+ private
40
+
41
+ def prepare(context); end
42
+ def teardown(context); end
43
+
44
+ def pad_empty_dates
45
+ @date_range.each do |date|
46
+ @items[date] ||= []
47
+ end
48
+ end
49
+
50
+ def expand_multiple_days_occurrences
51
+ @occurrences.each do |oc|
52
+ next if oc.oneday?
53
+ ((oc.first + 1) .. oc.last).each do |date|
54
+ @items[date] ||= []
55
+ @items[date] << oc
56
+ end
57
+ end
58
+ end
59
+
60
+ def format_header(context); ""; end
61
+ def format_footer(context); ""; end
62
+ def format_day_header(context, date, is_holiday); ""; end
63
+ def format_day_footer(context, date); ""; end
64
+
65
+ def format_body(context)
66
+ context[:number] = 0
67
+ @items.keys.sort.map{|date| format_day(context, date, @items[date]) }.join
68
+ end
69
+
70
+ def format_day(context, date, items)
71
+ string = format_day_header(context, date, items.any?{|e| e.holiday?})
72
+
73
+ items = sort_items_in_day(items)
74
+ items.each_with_index do |occurrence, count|
75
+ context[:number] += 1
76
+ context[:number_in_day] = count + 1
77
+ string += format_item(context, date, occurrence)
78
+ end
79
+
80
+ return string + format_day_footer(context, date)
81
+ end
82
+
83
+ def format_item(context, date, item)
84
+ raise "Implement in subclasses."
85
+ end
86
+
87
+ def format_item_header(context, date, item)
88
+ raise "Implement in subclasses."
89
+ end
90
+
91
+ ################################################################
92
+ ## helpers
93
+ def append(item, separator = " ")
94
+ return "" if item.to_s.empty?
95
+ return separator + item.to_s
96
+ end
97
+
98
+ def prepend(item, separator = " ")
99
+ return "" if item.to_s.empty?
100
+ return item.to_s + separator
101
+ end
102
+
103
+ def enclose(item, bracket = "[]")
104
+ return "" if item.to_s.empty?
105
+ return bracket[0] + item.to_s + bracket[1]
106
+ end
107
+
108
+ # sort occurrences in a day
109
+ # make sure all-day occurrences are prior to others
110
+ def sort_items_in_day(items)
111
+ items.sort do |a,b|
112
+ sign_a = a.allday? ? 0 : 1
113
+ sign_b = b.allday? ? 0 : 1
114
+
115
+ if sign_a != sign_b
116
+ sign_a - sign_b
117
+ else
118
+ a <=> b
119
+ end
120
+ end
121
+ end
122
+
123
+ end # class Base
124
+ end # module Formatter
125
+ end # module Mhc
@@ -0,0 +1,47 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Emacs < SymbolicExpression
4
+ private
5
+
6
+ def prepare(context)
7
+ expand_multiple_days_occurrences
8
+ end
9
+
10
+ def format_header(context); "("; end
11
+ def format_footer(context); ")\n"; end
12
+
13
+ def format_day_header(context, date, is_holiday)
14
+ # (DAYS_FROM_EPOC . [year month day wday holiday-p (
15
+ format("(%d . [%d %d %d %d #{is_holiday ? 't' : 'nil'} (",
16
+ date.absolute_from_epoch, date.year, date.month, date.day, date.wday)
17
+ end
18
+
19
+ def format_item(context, date, item)
20
+ subject = item.subject.to_s
21
+ subject = "(no subject)" if subject == ""
22
+
23
+ # [ RECORD CONDITION SUBJECT LOCATION (TIMEB . TIMEE) ALARM
24
+ # CATEGORIES PRIORITY REGION RECURRENCE-TAG]
25
+ format("[(%s . [%s nil nil]) nil %s %s (%s . %s) %s (%s) nil nil %s]",
26
+ elisp_string(item.path.to_s),
27
+ elisp_string(item.uid.to_s),
28
+ elisp_string(subject),
29
+ elisp_string(item.location),
30
+ (item.time_range.first ? (item.time_range.first.to_i / 60) : "nil"),
31
+ (item.time_range.last ? (item.time_range.last.to_i / 60) : "nil"),
32
+ elisp_string(item.alarm.to_s),
33
+ item.categories.map{|c| elisp_string(c.to_s.downcase)}.join(" "),
34
+ elisp_string(item.recurrence_tag))
35
+ end
36
+
37
+ def format_day_footer(context, date)
38
+ ")]) "
39
+ end
40
+
41
+ def elisp_string(string)
42
+ '"' + string.to_s.toutf8.gsub(/[\"\\]/, '\\\\\&').gsub("\n", "\\n") + '"'
43
+ end
44
+
45
+ end # class Emacs
46
+ end # class Formatter
47
+ end # module Mhc
@@ -0,0 +1,35 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Howm < Base
4
+ private
5
+
6
+ def format_header(context)
7
+ format("= mhc %s--%s\n", *context[:items].keys.minmax)
8
+ end
9
+
10
+ def format_item(context, date, item)
11
+ string = format("[%04d-%02d-%02d %5s]%1s %s\n",
12
+ date.year, date.month, date.mday,
13
+ item.time_range.first.to_s,
14
+ mark_todo(item.categories.to_mhc_string),
15
+ item.subject)
16
+ if item.description.to_s != ""
17
+ string += item.description.to_s.gsub(/^/, " ") + "\n"
18
+ end
19
+ return string
20
+ end
21
+
22
+ def mark_todo(category)
23
+ case category
24
+ when /done/i
25
+ "."
26
+ when /todo/i
27
+ "+"
28
+ else
29
+ "@"
30
+ end
31
+ end
32
+
33
+ end # class Howm
34
+ end # class Formatter
35
+ end # module Mhc
@@ -0,0 +1,17 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Icalendar < Base
4
+ private
5
+
6
+ def format_body(context)
7
+ ical = RiCal.Calendar
8
+ ical.prodid = Mhc::PRODID
9
+ @events.each do |event|
10
+ ical.events << event.to_icalendar
11
+ end
12
+ return ical.to_s
13
+ end
14
+
15
+ end # class Icalendar
16
+ end # class Formatter
17
+ end # module Mhc
@@ -0,0 +1,27 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Json < Base
4
+ require "json"
5
+
6
+ def format_body(context)
7
+ events = []
8
+ @occurrences.each do |oc|
9
+ class_name = []
10
+ class_name += oc.categories.map{|c| "mhc-category-#{c.to_s.downcase}"}
11
+ class_name << (oc.allday? ? "mhc-allday" : "mhc-time-range")
12
+
13
+ events << {
14
+ id: oc.record_id,
15
+ allDay: oc.allday?,
16
+ title: oc.subject,
17
+ start: oc.dtstart.iso8601,
18
+ end: oc.dtend.iso8601,
19
+ className: class_name
20
+ }
21
+ end
22
+ return events.to_json
23
+ end
24
+
25
+ end # class Json
26
+ end # class Formatter
27
+ end # module Mhc
@@ -0,0 +1,20 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Mail < Text
4
+ private
5
+
6
+ def format_header(context)
7
+ mail_address = context[:mailto].to_s
8
+ subject = format("MHC schedule report (%s--%s)", *context[:items].keys.minmax)
9
+ header = "To: #{mail_address}\n"
10
+ header += "From: #{append(mail_address, "secretary-of-")}\n"
11
+ header += "Subject: #{subject}\n"
12
+ header += "Content-Type: Text/Plain; charset=utf-8\n"
13
+ header += "Content-Transfer-Encoding: 8bit\n"
14
+ header += "\n"
15
+ header += format("* mhc %s--%s\n", *context[:items].keys.minmax)
16
+ end
17
+
18
+ end # class Mail
19
+ end # class Formatter
20
+ end # module Mhc
@@ -0,0 +1,24 @@
1
+ module Mhc
2
+ class Formatter
3
+ class OrgTable < Base
4
+ private
5
+
6
+ def format_header(context)
7
+ format("* mhc %s--%s\n", *context[:items].keys.minmax)
8
+ end
9
+
10
+ def format_item(context, date, item)
11
+ # | No | Mission | Recurrence | Subject | Path | Date |
12
+ format(" | %4d | %s | %s | %s | %s | [%04d-%02d-%02d%s] |\n",
13
+ context[:number],
14
+ item.mission_tag.to_s.toutf8,
15
+ item.recurrence_tag.to_s.toutf8,
16
+ item.subject.to_s.toutf8,
17
+ item.path.to_s,
18
+ date.year, date.month, date.mday,
19
+ append(item.time_range.to_s))
20
+ end
21
+
22
+ end # class OrgTable
23
+ end # class Formatter
24
+ end # module Mhc
@@ -0,0 +1,42 @@
1
+ module Mhc
2
+ class Formatter
3
+ class SymbolicExpression < Base
4
+ private
5
+
6
+ def format_header(context); "("; end
7
+ def format_footer(context); "(periods #{@periods}))\n"; end
8
+
9
+ def format_day_header(context, date, is_holiday)
10
+ date.strftime("((%2m %2d %Y) . (")
11
+ end
12
+
13
+ def format_item(context, date, item)
14
+ unless item.oneday?
15
+ format_multiple_days_item(context, date, item)
16
+ return ""
17
+ end
18
+ format_item_line(item)
19
+ end
20
+
21
+ def format_multiple_days_item(context, date, item)
22
+ @periods ||= ""
23
+ @periods += item.first.strftime("((%2m %2d %Y) ") +
24
+ item.last.strftime(" (%2m %2d %Y) ") +
25
+ format_item_line(item) + ') '
26
+ end
27
+
28
+ def format_day_footer(context, date); ")) "; end
29
+
30
+ def format_item_line(item)
31
+ '"' +
32
+ format("%s%s%s",
33
+ prepend(item.time_range.first.to_s).toutf8,
34
+ item.subject.to_s.toutf8,
35
+ append(enclose(item.location)).toutf8).gsub(/[\"\\]/, '\\\\\&') +
36
+ '" '
37
+
38
+ end
39
+
40
+ end # class SymbolicExpression
41
+ end # class Formatter
42
+ end # module Mhc
@@ -0,0 +1,29 @@
1
+ module Mhc
2
+ class Formatter
3
+ class Text < Base
4
+ def prepare(context)
5
+ expand_multiple_days_occurrences
6
+ end
7
+
8
+ def format_item(context, date, item)
9
+ subject = item.subject.to_s
10
+ subject = "(no subject)" if subject == ""
11
+ format("%s%-11s %s%s\n",
12
+ format_item_header(context, date, item),
13
+ item.time_range.to_mhc_string.toutf8,
14
+ subject.toutf8,
15
+ append(enclose(item.location)).toutf8
16
+ )
17
+ end
18
+
19
+ def format_item_header(context, date, item)
20
+ if context[:number_in_day] == 1
21
+ date.strftime("%Y/%m/%d %a ")
22
+ else
23
+ " " * 15
24
+ end
25
+ end
26
+
27
+ end # class Text
28
+ end # module Formatter
29
+ end # module Mhc
@@ -16,7 +16,6 @@ module Mhc
16
16
  :record_id,
17
17
  :uid,
18
18
  :subject,
19
- :time_range,
20
19
  :recurrence_tag,
21
20
  :mission_tag,
22
21
  :allday?,
@@ -37,14 +36,32 @@ module Mhc
37
36
  end
38
37
 
39
38
  def date
40
- @start_date
39
+ if @start_date.respond_to?(:hour)
40
+ Mhc::PropertyValue::Date.new(@start_date.year, @start_date.month, @start_date.day)
41
+ else
42
+ @start_date
43
+ end
44
+ end
45
+
46
+ # FIXME: TimeRange class should be implemented
47
+ def time_range
48
+ range = Mhc::PropertyValue::Range.new(Mhc::PropertyValue::Time)
49
+ if dtstart.respond_to?(:hour)
50
+ range.parse("#{dtstart.hour}:#{dtstart.min}-#{dtend.hour}:#{dtend.min}")
51
+ else
52
+ return range # allday
53
+ end
41
54
  end
42
55
 
43
56
  def dtstart
44
57
  if allday?
45
58
  @start_date
46
59
  else
47
- time_range.first.to_datetime(@start_date)
60
+ if @start_date.respond_to?(:hour)
61
+ @start_date
62
+ else
63
+ @event.time_range.first.to_datetime(@start_date)
64
+ end
48
65
  end
49
66
  end
50
67
 
@@ -52,7 +69,11 @@ module Mhc
52
69
  if allday?
53
70
  @end_date + 1
54
71
  else
55
- time_range.last.to_datetime(@end_date)
72
+ if @end_date.respond_to?(:hour)
73
+ @end_date
74
+ else
75
+ @event.time_range.last.to_datetime(@end_date)
76
+ end
56
77
  end
57
78
  end
58
79
 
@@ -77,7 +98,8 @@ module Mhc
77
98
  return "#{dtstart.to_mhc_string}" if oneday?
78
99
  return "#{@start_date.to_mhc_string}-#{@end_date.to_mhc_string}"
79
100
  else
80
- return dtstart.strftime("%Y%m%d %H:%m-") + ((@start_date == @end_date) ? dtend.strftime("%H:%m") : dtend.strftime("%Y%m%dT%H:%m"))
101
+ time = dtstart.strftime("%Y%m%d %H:%m-") + ((@start_date.to_date == @end_date.to_date) ? dtend.strftime("%H:%m") : dtend.strftime("%Y%m%dT%H:%m"))
102
+ return time + " " + subject.to_mhc_string
81
103
  end
82
104
  end
83
105
 
@@ -71,7 +71,7 @@ module Mhc
71
71
  first_date = date_or_range
72
72
  last_date = date_or_range
73
73
  end
74
- if last_date < @date_range.first or @date_range.last < first_date
74
+ if last_date.to_date < @date_range.first or @date_range.last < first_date.to_date
75
75
  next
76
76
  end
77
77
  next if @exceptions.include?(first_date)