mo2tex 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.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Mo2tex
6
+
7
+ module CourseHelper
8
+
9
+ def normalize_title(t)
10
+ return t.gsub(/_/, ' ')
11
+ end
12
+
13
+ end
14
+
15
+ # class Course
16
+
17
+ # include CourseHelper
18
+
19
+ # attr :title, :hours, :type, :students, :start
20
+
21
+ # def initialize(title, desc)
22
+ # @title = normalize_title(title)
23
+ # @hours = desc['ore']
24
+ # @type = desc['tipologia']
25
+ # @students = desc['studenti']
26
+ # @start = DateTime.parse(desc['inizio'].to_s)
27
+ # end
28
+
29
+ # def generate(cal)
30
+ # result = latex_title
31
+ # evs = cal.lookup(self.title)
32
+ # raise UnknownCourse, "#{self.title}" unless evs
33
+ # tot_nlessons = self.total_number_of_lessons(evs)
34
+ # nless = 0
35
+ # evs.each do
36
+ # |ev|
37
+ # ev.repeat do
38
+ # |sev, title|
39
+ # nless += 1
40
+ # result += (sev.to_latex + " & Lezione n.#{nless}/#{tot_nlessons}\\\\\n")
41
+ # break if nless > tot_nlessons
42
+ # end
43
+ # end
44
+ # return result
45
+ # end
46
+
47
+ # def total_course_dur_in_seconds
48
+ # return self.academic_hours * 3600
49
+ # end
50
+
51
+ # def total_number_of_lessons(evs)
52
+ # tot = 0
53
+ # nless = 0
54
+ # evs.each do
55
+ # |ev|
56
+ # ev.repeat do
57
+ # |sev, title|
58
+ # tot += sev.dur
59
+ # nless += 1
60
+ # break if tot > self.total_course_dur_in_seconds
61
+ # end
62
+ # end
63
+ # return nless
64
+ # end
65
+
66
+ # class <<self
67
+
68
+ # def create(title, desc)
69
+ # result = nil
70
+ # case desc['tipologia']
71
+ # when 'collettiva'
72
+ # result = CollectiveCourse.new(title, desc)
73
+ # when 'individuale'
74
+ # result = IndividualCourse.new(title, desc)
75
+ # else
76
+ # raise UnknownCourseType, "Unknown type #{desc['tipologia']}"
77
+ # end
78
+ # return result
79
+ # end
80
+
81
+ # end
82
+
83
+ # def num_students
84
+ # return self.students.size
85
+ # end
86
+
87
+ # def total_hours
88
+ # raise PureVirtualMethodCalled, 'Course#total_hours'
89
+ # end
90
+
91
+ # def academic_hours
92
+ # return self.total_hours
93
+ # end
94
+
95
+ # private
96
+
97
+ # def latex_title
98
+ # return "\\multicolumn{3}{\\bfseries #{self.title}}\\\\\n"
99
+ # end
100
+
101
+ # end
102
+
103
+ # class CollectiveCourse < Course
104
+
105
+ # def total_hours
106
+ # return self.hours
107
+ # end
108
+
109
+ # end
110
+
111
+ # class IndividualCourse < Course
112
+
113
+ # def total_hours
114
+ # return self.hours * self.num_students
115
+ # end
116
+
117
+ # def academic_hours
118
+ # return (self.total_hours * 0.75)
119
+ # end
120
+
121
+ # end
122
+
123
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mo2tex
4
+
5
+ module DateTimeHelper
6
+
7
+ def datetime_create(day, t)
8
+ raise DayIsNotDateTime, "#{day}" unless day.class == DateTime
9
+ raise TimeIsNotString, "#{t}" unless t.class == String
10
+ (hours, minutes) = t.split(':').map { |n| n.to_i }
11
+ result = DateTime.new(day.year, day.month, day.day, hours, minutes, 0)
12
+ return result
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def self.extend(base)
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ def first_midnight
22
+ return DateTime.new(0, 1, 1, 0, 0, 0)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mo2tex
4
+
5
+ class SlotBase
6
+
7
+ include DateTimeHelper
8
+
9
+ attr_reader :day
10
+ attr_accessor :dtstart, :dtend
11
+
12
+ def initialize(day, ts, te)
13
+ @day = day
14
+ self.dtstart = ts
15
+ self.dtend = te
16
+ end
17
+
18
+ def dtstart=(ts)
19
+ @dtstart = common_equal(ts)
20
+ end
21
+
22
+ def dtend=(te)
23
+ @dtend = common_equal(te)
24
+ end
25
+
26
+ def color
27
+ raise PureVirtualMethodCalled, "#{self.class}#color"
28
+ end
29
+
30
+ def title
31
+ raise PureVirtualMethodCalled, "#{self.class}#title"
32
+ end
33
+
34
+ private
35
+
36
+ def common_equal(t)
37
+ result = t if t.is_a?(DateTime)
38
+ result = datetime_create(self.day, t) if t.is_a?(String)
39
+ return result
40
+ end
41
+
42
+ end
43
+
44
+ class FreeSlot < SlotBase
45
+
46
+ def title
47
+ return "FREE"
48
+ end
49
+
50
+ def color
51
+ return "\"red\""
52
+ end
53
+
54
+ end
55
+
56
+ class LunchBreak < SlotBase
57
+
58
+ def title
59
+ return "Pranzo"
60
+ end
61
+
62
+ def color
63
+ return "\"grey50\""
64
+ end
65
+
66
+ end
67
+
68
+ class DaySchedule
69
+
70
+ include DateTimeHelper
71
+
72
+ attr_reader :day, :tstart, :tend, :lbtstart, :lbtend, :dtstart, :dtend, :lbdtstart, :lbdtend, :lnumber, :wday
73
+ attr_reader :slots
74
+ attr_accessor :dirty
75
+
76
+ def initialize(day, ln, wd, conf)
77
+ raise DayIsNotDateTime, "#{day}" unless day.is_a?(DateTime)
78
+ raise ArgumentError, "#{conf}" unless conf.is_a?(Hash)
79
+ @day = day # should be a DateTime with the d/m/y
80
+ @wday = wd
81
+ @tstart = conf[self.wday]['orario'][0]
82
+ @tend = conf[self.wday]['orario'][1]
83
+ @lbtstart = conf[self.wday]['pausa'][0]
84
+ @lbtend = conf[self.wday]['pausa'][1]
85
+ @dtstart = datetime_create(self.day, self.tstart)
86
+ @dtend = datetime_create(self.day, self.tend)
87
+ @lbdtstart = datetime_create(self.day, self.lbtstart)
88
+ @lbdtend = datetime_create(self.day, self.lbtend)
89
+ @lnumber = ln
90
+ @slots = []
91
+ self.dirty = false
92
+ calculate_free_slots
93
+ end
94
+
95
+ def dirty?
96
+ return self.dirty
97
+ end
98
+
99
+ def <<(event)
100
+ return unless this_slot?(event)
101
+ self.slots << event
102
+ self.dirty = true
103
+ end
104
+
105
+ def calculate_free_slots
106
+ if self.slots.empty?
107
+ self.slots << LunchBreak.new(self.day, self.lbdtstart, self.lbdtend)
108
+ else
109
+ bsorted = self.slots.sort { |a, b| a.dtstart <=> b.dtstart }
110
+ tstart = self.dtstart
111
+ bsorted.each do
112
+ |ev|
113
+ self.slots << FreeSlot.new(self.day, tstart, ev.dtstart) if (ev.dtstart.round_to_ymdhm > tstart.round_to_ymdhm)
114
+ tstart = ev.dtend
115
+ end
116
+ self.slots << FreeSlot.new(self.day, tstart, self.dtend) if (tstart.round_to_ymdhm < self.dtend.round_to_ymdhm)
117
+ end
118
+ self.slots.sort! { |a, b| a.dtstart <=> b.dtstart }
119
+ self.dirty = false
120
+ return self.slots
121
+ end
122
+
123
+ def each_slot(&block)
124
+ self.calculate_free_slots if self.dirty?
125
+ self.slots.sort! { |a, b| a.dtstart <=> b.dtstart }
126
+ self.slots.each { |slot| yield(slot) } if block_given?
127
+ end
128
+
129
+ def this_slot?(ev)
130
+ return (ev.dtstart.day_only >= self.dtstart.day_only && ev.dtend.day_only <= self.dtend.day_only)
131
+ end
132
+
133
+ end
134
+
135
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'icalendar'
4
+
5
+ module Mo2tex
6
+
7
+ class Event
8
+
9
+ attr_reader :dtstart, :dtend, :lnumber, :ltot, :title, :desc, :loc, :online
10
+
11
+ def initialize(ls, le, ln, lt, t, desc, loc, online)
12
+ @dtstart = DateTime.parse(ls.to_s)
13
+ @dtend = DateTime.parse(le.to_s)
14
+ @lnumber = ln
15
+ @ltot = lt
16
+ @title = t
17
+ @desc = desc
18
+ @loc = loc
19
+ @online = online
20
+ end
21
+
22
+ def to_s
23
+ return self.title + ' ' + self.dtstart.strftime("%d/%m/%Y %H:%M") + '-' + self.dtend.strftime("%H:%M") + ' ' + self.lnumber.to_s + '/' + self.ltot.to_s
24
+ end
25
+
26
+ def to_ical(ical)
27
+ ical.event do
28
+ |e|
29
+ e.dtstart = self.dtstart
30
+ e.dtend = self.dtend
31
+ e.summary = self.title
32
+ e.description = self.desc
33
+ e.location = self.loc
34
+ e.ip_class = 'PUBLIC'
35
+ if is_online?
36
+ e.location = 'Microsoft Teams Meeting'
37
+ e.description += <<EOR
38
+ \n_____________________________________________________________
39
+ ___________________\nMicrosoft Teams Need help?<https://aka.ms/JoinTeamsMe
40
+ eting?omkt=en-US>\nJoin the meeting now<https://teams.microsoft.com/l/meet
41
+ up-join/19%3ameeting_MWZlZDdkZmMtNjRkNi00M2U3LTgwMzUtMjkwNGQ1YTY3N2I4%40th
42
+ read.v2/0?context=%7b%22Tid%22%3a%2276c57fd2-8d47-4cb5-a7a2-83c2cd0c3565%2
43
+ 2%2c%22Oid%22%3a%22a29bd7f0-b99d-4031-8cf5-fabe578a7d05%22%7d>\nMeeting ID
44
+ : 358 935 834 932\nPasscode: 9wNLge\n________________________________\nFor
45
+ organizers: Meeting options<https://teams.microsoft.com/meetingOptions/?o
46
+ rganizerId=a29bd7f0-b99d-4031-8cf5-fabe578a7d05&tenantId=76c57fd2-8d47-4cb
47
+ 5-a7a2-83c2cd0c3565&threadId=19_meeting_MWZlZDdkZmMtNjRkNi00M2U3LTgwMzUtMj
48
+ kwNGQ1YTY3N2I4@thread.v2&messageId=0&language=en-US>\n____________________
49
+ ____________________________________________________________\n
50
+ EOR
51
+ end
52
+ end
53
+ return ical
54
+ end
55
+
56
+ def color
57
+ return "\"green\""
58
+ end
59
+
60
+ private
61
+
62
+ def is_online?
63
+ result = false
64
+ self.online.each do
65
+ |d|
66
+ dt = DateTime.parse(d.to_s)
67
+ if self.dtstart.year == dt.year && self.dtstart.month == dt.month && self.dtstart.day == dt.day
68
+ result = true
69
+ break
70
+ end
71
+ end
72
+ return result
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module Mo2tex
5
+
6
+ class UnknownCourse < Error; end
7
+ class UnknownCourseType < Error; end
8
+ class UnknownRepetition < Error; end
9
+ class PureVirtuaMethodCalled < Error; end
10
+ class DateOutOfRange < Error; end
11
+ class UnexistentWeekDay < Error; end
12
+ class UnknownCourseFrequency < Error; end
13
+ class WrongWeekDay < Error; end
14
+ class DayIsNotDateTime < Error; end
15
+ class TimeIsNotString < Error; end
16
+ class NotAPicContainer < Error; end
17
+ class GenerationError < Error; end
18
+ class NotEnoughLessons < GenerationError; end
19
+ class EventsNotGenerated < GenerationError; end
20
+
21
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Mo2tex
6
+
7
+ class ExcludeBase
8
+
9
+ attr_reader :start_period, :end_period
10
+
11
+ def initialize(sp, ep)
12
+ @start_period = DateTime.parse(sp.to_s)
13
+ @end_period = DateTime.parse(ep.to_s)
14
+ end
15
+
16
+ def skip?(d)
17
+ dt = DateTime.parse(d.to_s)
18
+ return true unless within_period?(dt)
19
+ return does_it_skip?(dt)
20
+ end
21
+
22
+ protected
23
+
24
+ def does_it_skip?(d)
25
+ raise PureVirtualMethodCalled, 'ExcludeBase#skip'
26
+ end
27
+
28
+ def within_period?(d)
29
+ return (d >= self.start_period && d <= self.end_period)
30
+ end
31
+
32
+ end
33
+
34
+ class ExcludeDate < ExcludeBase
35
+
36
+ attr_reader :date
37
+
38
+ def initialize(d, sd, ed)
39
+ super(sd, ed)
40
+ @date = DateTime.parse(d.to_s)
41
+ raise DateOutOfRange, self.date.to_s unless within_period?(self.date)
42
+ end
43
+
44
+ def does_it_skip?(d)
45
+ return d == self.date
46
+ end
47
+
48
+ end
49
+
50
+ class ExcludeDates < ExcludeBase
51
+
52
+ attr_reader :start_date, :end_date
53
+
54
+ def initialize(sd, ed, sp, ep)
55
+ super(sp, ep)
56
+ @start_date = DateTime.parse(sd.to_s)
57
+ @end_date = DateTime.parse(ed.to_s)
58
+ raise DateOutOfRange, self.start_date.to_s unless within_period?(self.start_date)
59
+ raise DateOutOfRange, self.end_date.to_s unless within_period?(self.end_date)
60
+ end
61
+
62
+ def does_it_skip?(d)
63
+ return (d >= self.start_date && d <= self.end_date)
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Mo2tex
6
+
7
+ class ExclusionManager
8
+
9
+ attr_reader :excluded, :start_period, :end_period
10
+
11
+ def initialize(exl_array, sp, ep)
12
+ @start_period = DateTime.parse(sp.to_s)
13
+ @end_period = DateTime.parse(ep.to_s)
14
+ @excluded = parse(exl_array)
15
+ end
16
+
17
+ def skip?(d)
18
+ result = false
19
+ self.excluded.each { |ed| result |= ed.skip?(d) }
20
+ return result
21
+ end
22
+
23
+ def out_of_range?(d)
24
+ dt = DateTime.parse(d.to_s)
25
+ return (dt < self.start_period || dt > self.end_period)
26
+ end
27
+
28
+ def empty?
29
+ return self.excluded.empty?
30
+ end
31
+
32
+ def <<(dts)
33
+ arg = dts.is_a?(Array) ? dts : [ dts ]
34
+ result = parse(arg)
35
+ self.excluded.concat(result)
36
+ end
37
+
38
+ private
39
+
40
+ def parse(excl_array)
41
+ result = []
42
+ excl_array.each do
43
+ |el|
44
+ case el
45
+ when String
46
+ (sd, ed) = el.split(/-/).map { |n| n.to_i }
47
+ result << ExcludeDates.new(sd, ed, self.start_period, self.end_period)
48
+ when Integer
49
+ result << ExcludeDate.new(el, self.start_period, self.end_period)
50
+ else
51
+ raise ArgumentError, "#{el} not recognized"
52
+ end
53
+ end
54
+ return result
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ class DateTime
6
+
7
+ def hm_to_s
8
+ result = sprintf("%02d:%02d", self.hour, self.minute)
9
+ return result
10
+ end
11
+
12
+ ITWDAYS = [ 'domenica', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato', ]
13
+
14
+ def to_it_wday
15
+ result = ITWDAYS[self.wday]
16
+ raise UnexistentWeekDay, wd.to_s unless result
17
+ return result
18
+ end
19
+
20
+ def to_it_roff_wday
21
+ return to_it_wday.sub(/ì/, "\\(`i")
22
+ end
23
+
24
+ def day_only
25
+ return self.to_date.to_datetime
26
+ end
27
+
28
+ def round_to_ymdhm
29
+ result = DateTime.new(self.year, self.month, self.day, self.hour, self.min, 0, 0)
30
+ return result
31
+ end
32
+
33
+ class << self
34
+
35
+ FIRST_MIDNIGHT = DateTime.new(0, 1, 1, 0, 0, 0, 0)
36
+
37
+ def first_midnight
38
+ return FIRST_MIDNIGHT
39
+ end
40
+
41
+ def dayfrac_to_dt(frac)
42
+ return first_midnight + frac.to_r
43
+ end
44
+
45
+ def dayfrac_to_hm(frac)
46
+ dt = dayfrac_to_dt(frac)
47
+ return [dt.hour, dt.minute]
48
+ end
49
+
50
+ def dayfrac_to_hms(frac)
51
+ return dayfrac_to_dt(frac).hm_to_s
52
+ end
53
+
54
+ def dayfrac_to_dur(frac)
55
+ return ((frac.to_r*24)*10).round/10.0
56
+ end
57
+
58
+ def from_it_wday(wday)
59
+ result = ITWDAYS.index(wday)
60
+ raise UnexistentWeekDay, wday unless result
61
+ return result
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Mo2tex
6
+
7
+ DEFAULT_LTDIR = File.expand_path(File.join(['..']*3, 'config', 'templates', 'latex'), __FILE__)
8
+
9
+ class Latex
10
+
11
+ attr_reader :school, :header, :trailer, :body, :total_hours_done
12
+
13
+ def initialize(s)
14
+ @total_hours_done = 0
15
+ @school = s
16
+ @header = read_header
17
+ @trailer = read_trailer
18
+ @body = read_body
19
+ end
20
+
21
+ def generate
22
+ result = self.header
23
+ result += self.body.result(binding)
24
+ result += self.trailer
25
+ end
26
+
27
+ def table_body
28
+ result = ''
29
+ head = proc { |title| "\\hline\n\\multicolumn{3}{|l|}{\\bfseries #{title}}\\\\\n\\hline\n" }
30
+ tail = proc { |sctot| "\\multicolumn{2}{l}{Totale ore corso:} & \\hfill#{sctot.round.to_i}\\\\\n & & \\\\\n" }
31
+ @total_hours_done, result = self.school.generate(head, tail) do
32
+ |title, nol, ev|
33
+ sprintf("%s & %s--%s & Lezione n.%02d/%02d\\\\\n", ev.dtstart.strftime("%d/%m/%Y"),
34
+ ev.dtstart.strftime("%H:%M"),
35
+ ev.dtend.strftime("%H:%M"),
36
+ ev.lnumber, ev.ltot)
37
+ end
38
+ @total_hours_done = @total_hours_done.round.to_i
39
+ return result
40
+ end
41
+
42
+ private
43
+
44
+ def read_header
45
+ return read('header.tex')
46
+ end
47
+
48
+ def read_trailer
49
+ return read('trailer.tex')
50
+ end
51
+
52
+ def read_body
53
+ template = read('body.tex.erb')
54
+ return ERB.new(template)
55
+ end
56
+
57
+ def read(what)
58
+ result = nil
59
+ tdir ||= DEFAULT_LTDIR
60
+ File.open(File.join(tdir, what)) { |fh| result = fh.readlines.join }
61
+ return result
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Mo2tex
6
+
7
+ module CommonParts
8
+
9
+ def m2common(args, io = STDOUT, &block)
10
+ raise ArgumentError, "Usage: #{File.basename($0)} <config file>" unless args.size > 0
11
+ school = School.new(args[0])
12
+ result = yield(school)
13
+ io.puts(result)
14
+ return result
15
+ end
16
+
17
+ end
18
+
19
+ class << self
20
+
21
+ include CommonParts
22
+
23
+ #
24
+ # +m2latex+: produces a LaTeX file from a configuration
25
+ #
26
+ def m2latex(args, io = STDOUT)
27
+ text = m2common(args, io) do
28
+ |school|
29
+ latex = Latex.new(school)
30
+ return latex.generate
31
+ end
32
+ return text
33
+ end
34
+
35
+ #
36
+ # +m2ical+: produces an iCal file from a configuration
37
+ #
38
+ def m2ical(args, io = STDOUT)
39
+ ical = m2common(args, io) do
40
+ |school|
41
+ ical = school.cal.create_icalendar(school)
42
+ return ical.to_ical
43
+ end
44
+ return ical
45
+ end
46
+
47
+ #
48
+ # +m2pic+: produces a pic file from a configuration
49
+ #
50
+ def m2pic(args, io = STDOUT)
51
+ text = m2common(args, io) do
52
+ |school|
53
+ pic = Pic::Writer.new(school)
54
+ return pic.generate
55
+ end
56
+ return text
57
+ end
58
+ end
59
+
60
+ end