docfolio 0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 99d2c5789c65b55841fd11b71cfb29448e1ea722
4
+ data.tar.gz: d4221138fa97f72c806a8aaabd04f3881f0e4c76
5
+ SHA512:
6
+ metadata.gz: f6afcea04d4231186c8ac92f138637f75fa447073668603d83fbc9f285771d68e5fb83b7872d3466710a742751018063e52d8c4cfe389d69cd9a84fadb3e8bea
7
+ data.tar.gz: 27134b0e2c0d9151a33716307325fc8076a8994056776bef3c4abd197ce355e0e4d6e769a1d1d0373e7c61ed37bca1f2eb71062525ca1dddef86bb266c8e9a6b
data/lib/docfolio.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative 'docfolio/docfolio.rb'
2
+ Docfolio.new.print_logs
@@ -0,0 +1,81 @@
1
+ # convert the date from 'dd-Oct-yy' to seconds past UNIX epoc
2
+ # accepts dd_Mmm-yy dd-Mmmmmmm-yy dd-MMM-yy and other similar
3
+ module DateFormat
4
+ # Extracts a date in seconds past UNIX epoc from a string date. The result
5
+ # can be used for other date operations. Converts from dd-mmm-yy and similar
6
+ # formats as commonly found in csv files
7
+ class DateExtractor
8
+ def format_date(date)
9
+ day, month, year = components(date)
10
+ begin
11
+ Time.new(year, month, day).to_i
12
+ rescue ArgumentError => e
13
+ print_argument_error_msg(e)
14
+ return nil
15
+ rescue => e
16
+ raise e
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def print_argument_error_msg(e)
23
+ puts "\n#{e.to_s.upcase}"
24
+ puts "date : #{date.inspect}"
25
+ puts "day : #{day}"
26
+ puts "month : #{month}"
27
+ puts "year : #{year}"
28
+ puts e.backtrace
29
+ end
30
+
31
+ # splits date into is component day month and time
32
+ def components(date)
33
+ date = date.split('-')
34
+ day = date[0].to_i
35
+ month = convert_month_to_number(date[1])
36
+ year = date[2].to_i
37
+ if year < 100 # no century
38
+ year > Time.now.year % 1000 ? century = 1900 : century = 2000
39
+ year += century
40
+ end
41
+ [day, month, year]
42
+ end
43
+
44
+ MONTHS = {
45
+ 'jan' => 1,
46
+ 'feb' => 2,
47
+ 'mar' => 3,
48
+ 'apr' => 4,
49
+ 'may' => 5,
50
+ 'jun' => 6,
51
+ 'jul' => 7,
52
+ 'aug' => 8,
53
+ 'sep' => 9,
54
+ 'oct' => 10,
55
+ 'nov' => 11,
56
+ 'dec' => 12,
57
+ 'january' => 1,
58
+ 'february' => 2,
59
+ 'march' => 3,
60
+ 'april' => 4,
61
+ 'june' => 6,
62
+ 'july' => 7,
63
+ 'august' => 8,
64
+ 'september' => 9,
65
+ 'october' => 10,
66
+ 'november' => 11,
67
+ 'december' => 12,
68
+ 'sept' => 9
69
+ }
70
+
71
+ def convert_month_to_number(month)
72
+ return month.to_i if month.to_i > 0 # already a number
73
+ month = month.downcase
74
+ MONTHS[month]
75
+ end
76
+ end
77
+
78
+ def format_date(date)
79
+ DateExtractor.new.format_date(date)
80
+ end
81
+ end
@@ -0,0 +1,74 @@
1
+ require_relative 'learning_diary.rb'
2
+ # require 'stringio'
3
+ require_relative 'views/collater_console_view.rb'
4
+
5
+ # collection class for Learning Diarys / logs
6
+ class Logs
7
+ include Enumerable
8
+
9
+ # iterates through every file in the data directory and parses with
10
+ # LearningDiary
11
+ def initialize(loading_logs = true)
12
+ @logs = []
13
+ if loading_logs
14
+ log_files_dir.each do |log_file_name|
15
+ @logs << LearningDiary.new(log_file_name)
16
+ end
17
+ end
18
+ end
19
+
20
+ # Adds either a file to logs or a directory to logs
21
+ # @param [String] file_or_directory A string containing the name of a
22
+ # file or directory
23
+ def add(file_or_directory)
24
+ log_directory(file_or_directory) if File.directory?(file_or_directory)
25
+ log_file(file_or_directory) if File.file?(file_or_directory)
26
+ end
27
+
28
+ def each(&block)
29
+ @logs.each { |p| block.call(p) }
30
+ end
31
+
32
+ private
33
+
34
+ # Adds a file to logs
35
+ # @param [String] file A string containing the name of a file
36
+ def log_file(file)
37
+ @logs << LearningDiary.new(file)
38
+ end
39
+
40
+ # Adds a directory to logs
41
+ # @param [String] dir A string containing the name of a directory
42
+ def log_directory(dir)
43
+ Dir[dir + '/**/*.txt'].each { |file| log_file(file) }
44
+ end
45
+
46
+ # @return [Array] An array of strings. Each string is a
47
+ # relative file path for every .txt file under the data directory
48
+ def log_files_dir
49
+ Dir['./docfolio/data/**/*.txt']
50
+ end
51
+ end
52
+
53
+ # controller class for the diaries/logs collection
54
+ class Docfolio
55
+ def initialize
56
+ @logs = Logs.new
57
+ @view = CollaterConsoleView.new
58
+ end
59
+
60
+ # Creates a portfolio
61
+ # @param [String] portfolio_file_or_directory A portfolio file or a
62
+ # name of a directory containing portfolio files including files
63
+ # within subdirectories.
64
+ def self.create(portfolio_file_or_directory)
65
+ logs = Logs.new(false)
66
+ logs.add(portfolio_file_or_directory)
67
+ CollaterConsoleView.new.print_logs(logs)
68
+ true
69
+ end
70
+
71
+ def print_logs
72
+ @view.print_logs(@logs)
73
+ end
74
+ end
@@ -0,0 +1,87 @@
1
+ require_relative 'date_format.rb'
2
+ require_relative 'paragraph.rb'
3
+ require_relative 'views/diary_console_view.rb'
4
+
5
+ # collection class for paragraphs
6
+ # synonym 'Topic' 'Learning Diary'
7
+ class LearningDiary
8
+ include Enumerable
9
+
10
+ attr_reader :paragraphs
11
+
12
+ # @param [String] file name of a text file containing text in docfile DSL
13
+ def initialize(file)
14
+ # preparation
15
+ # @todo extract to an initialize_vars function, as in the paragraph class
16
+ Paragraph.reset
17
+ @console_view = DiaryConsoleView.new
18
+ @paragraphs = []
19
+ @standard_credits_array = []
20
+ @impact_credits_array = []
21
+
22
+ # read the whole txt file in one go
23
+ f = File.read(file, encoding: 'UTF-8')
24
+
25
+ # iterates through each paragraph
26
+ f.split(/\n/).each do |p|
27
+ next if p == '' # ignore if paragraph empty
28
+ @paragraphs << Paragraph.new(p) #
29
+ end
30
+ calc_standard_credits
31
+ calc_impact_credits
32
+ end
33
+
34
+ def each(&block)
35
+ @paragraphs.each { |p| block.call(p) }
36
+ end
37
+
38
+ def [](index)
39
+ @paragraphs[index]
40
+ end
41
+
42
+ def standard_credits_total
43
+ @standard_credits_array.reduce(0) { |a, e| a + e[2] }
44
+ end
45
+
46
+ def standard_credits
47
+ @standard_credits_array
48
+ end
49
+
50
+ def impact_credits_total
51
+ @impact_credits_array.reduce(0) { |a, e| a + e[2] }
52
+ end
53
+
54
+ def credits_total
55
+ impact_credits_total + standard_credits_total
56
+ end
57
+
58
+ def impact_credits
59
+ @impact_credits_array
60
+ end
61
+
62
+ private
63
+
64
+ def calc_standard_credits
65
+ @paragraphs.each do |p|
66
+ next unless p.creditable?
67
+ start = p.start_time
68
+ finish = p.end_time
69
+ duration = p.period
70
+ @standard_credits_array << [start, finish, duration] unless duration == 0
71
+ end
72
+ @standard_credits_array.uniq!
73
+ end
74
+
75
+ def calc_impact_credits
76
+
77
+ @paragraphs.each do |p|
78
+ next unless p.impact_creditable?
79
+ # p p
80
+ start = p.start_time
81
+ finish = p.end_time
82
+ duration = p.period
83
+ @impact_credits_array << [start, finish, duration] unless duration == 0
84
+ end
85
+ @impact_credits_array.uniq!
86
+ end
87
+ end
@@ -0,0 +1,270 @@
1
+ require_relative 'tags.rb'
2
+
3
+ # @param [Array] time_array updated start and end times in hours and minutes
4
+ # @param [Array] times_and_dates current start and end times and date
5
+ # @return [Array] returns the new times_and_dates array for use going forwards
6
+ module MyTime
7
+ # processes new times and dates with current times and dates
8
+ class TimeProcesser
9
+ include Tags
10
+
11
+ # Takes class start end times and dates as Time objects and amends the
12
+ # times, advancing the date if the start time has crossed midnight.
13
+ # @param [Array] current_times_and_dates An array containing the
14
+ # Paragraph class instance variables for the start time, end time
15
+ # and date (day)
16
+ # @param [Array] new_times An array containing the from hour, from min,
17
+ # to hour, to min
18
+ # @return [Array] The updated Paragraph class instance variables for the
19
+ # start time, end time and date.
20
+ def process_times(new_times, current_times_and_dates)
21
+ @start_time, @end_time, @date = current_times_and_dates
22
+ f_hour, f_min, t_hour, t_min = new_times
23
+ if (has f_hour) && to_st_tme(f_hour, f_min)
24
+ t_hour = f_hour
25
+ t_min = f_min
26
+ end
27
+ to_end_time(t_hour, t_min) if has t_hour
28
+ [@start_time, @end_time, @date]
29
+ end
30
+
31
+ private
32
+
33
+
34
+ # @param [Number] f_hour From hours
35
+ # @param [Number] f_min From minutes
36
+ # @return [Boolean] if has a start time and no end time return true else
37
+ # set start time and return false (the nil for the last assignment of
38
+ # start_t)
39
+ def to_st_tme(f_hour, f_min)
40
+ # treat the time as an end time if there is a start time and no end time
41
+ has(@start_time) && (!has @end_time) ? true : start_t(f_hour, f_min)
42
+ end
43
+
44
+ def to_end_time(t_hour, t_min)
45
+ # extract_time_object simply adds hours and mins to date
46
+ @end_time = extract_time_object(t_hour, t_min, @date)
47
+ # if end_time before start_time, assume it is the following day
48
+ @end_time += a_day if @end_time < @start_time
49
+ end
50
+
51
+ # Adds hours and mins to date (Time). Then adds a day if the start time is
52
+ # before the end time. Finally, makes end time nil
53
+ # @todo why advance the start time by a day if before the end time?
54
+ # @todo why make end_time nil?
55
+ def start_t(hour, min)
56
+ # extract_time_object simply adds hours and mins to date
57
+ @start_time = extract_time_object(hour, min, @date)
58
+
59
+ # advance start time by one day if the start time is before the end_time
60
+ @start_time += a_day if (has @end_time) && @start_time < @end_time
61
+ @end_time = nil
62
+ end
63
+
64
+ # Improves readability of boolean condition statements. Is used from Time
65
+ # objects and integer hour component, but it works for any object
66
+ # @param [Object] time_date_component Any object
67
+ # @return [Boolean] true if the parameter is not nil
68
+ def has(time_date_component)
69
+ !time_date_component.nil?
70
+ end
71
+
72
+ # returns one day in seconds and adds a day to the @date
73
+ def a_day
74
+ @date.nil? ? fail('needs date') : @date += 86_400
75
+ 86_400
76
+ end
77
+ end
78
+
79
+ # Takes class start end times and dates as Time objects and amends the
80
+ # times, advancing the date if the start time has crossed midnight.
81
+ # @param [Array] times_and_dates An array containing the Paragraph class
82
+ # instance variables for the start time, end time and date (day)
83
+ # @param [Array] time_array An array containing the from hour, from min,
84
+ # to hour, to min
85
+ def process_times(time_array, times_and_dates)
86
+ TimeProcesser.new.process_times(time_array, times_and_dates)
87
+ end
88
+ end
89
+
90
+ # Used by LearningDiary
91
+ # Initialized with plain text, will parse and hold tagged content
92
+ # Can keep track of time, section and id information from previous paragraphs
93
+ # using class instance variables
94
+ class Paragraph
95
+ include Enumerable
96
+ include MyTime
97
+
98
+ private
99
+
100
+ include Tags
101
+
102
+ public
103
+
104
+ attr_reader :id, :tags
105
+
106
+ # instance start time and instance end time
107
+ attr_accessor :start_time, :end_time
108
+
109
+ # initialize class date, start and class end time class instance variables
110
+ @cst = @cet = @date = nil
111
+ @section = @id = 0 # :TITLE
112
+
113
+ class << self
114
+ # class starttime and class end time
115
+ attr_accessor :st, :et, :date, :section, :id
116
+ end
117
+
118
+ # @param [String] p a single paragraph from a text file.
119
+ def initialize(p)
120
+ # preparation
121
+ initialize_vars
122
+
123
+ # Extract the date and time from a paragraph if it contains date and time
124
+ # info. Removes the date and time from the paragraph puts whats left into
125
+ # rest_of_str. Puts the to hour, to min, from hour and from min into the
126
+ # time array. Puts the date into Paragraph.date as a Time object.
127
+ #
128
+ # Paragraph.date is a class instance variable that holds the date to apply
129
+ # to this and subsequent paragraphs. It is initialized to nil when the
130
+ # program starts and reset to nil when reset is called (which it is called
131
+ # by the LearningDiary when initializing to parse a new file, called by
132
+ # the Collater when iterating through each text file)
133
+ #
134
+ # The extract_date function is from the Tag module
135
+ rest_of_str, time_array, Paragraph.date = extract_date(p, Paragraph.date)
136
+
137
+ # if a date or time has been found (and extracted)
138
+ if rest_of_str != p
139
+ # transer class start and end times to those of this paragraph, reset
140
+ # section to :NOTE
141
+ note_time
142
+
143
+ # Takes the current class instance times and dates and newly extracted
144
+ # paragraph dates from this paragraph, follows a set of rules to
145
+ # determine what the class instant times and dates should become
146
+ assign_class_dates process_times(time_array, class_dates)
147
+
148
+ # tranfser class start and end times to those of this paragraph, reset
149
+ # section to :NOTE
150
+ note_time
151
+ end
152
+
153
+ # if a new date or time has not been found then return
154
+ # @todo should this be in an else statement?
155
+ return if rest_of_str == ''
156
+
157
+ tags_extracted?(rest_of_str) ? note_time : tag_section(rest_of_str)
158
+ end
159
+
160
+ # returns true if any tags are of type tag
161
+ # @param [Array] tag An array of tags
162
+ def tag?(tag)
163
+ @tags.each { |t| return true if t[0] == tag }
164
+ false
165
+ end
166
+
167
+ # resets the class variables so that a new file can be parsed
168
+ # is called by LearningDiary when preparing to parse a new txt file
169
+ def self.reset
170
+ Paragraph.date = Paragraph.st = Paragraph.et = nil
171
+ Paragraph.section = Paragraph.id = 0 # :TITLE
172
+ end
173
+
174
+ # @todo should this be private?
175
+ def initialize_vars
176
+ @date_specified = @end_time_specified = @start_time_specified = false
177
+ @start_time = @end_time = nil
178
+ @tags = []
179
+ @id = Paragraph.id
180
+ Paragraph.id += 1
181
+ end
182
+
183
+ # each on paragraph iterates through the tags
184
+ def each(&block)
185
+ @tags.each { |t| block.call(t) }
186
+ end
187
+
188
+ def [](index)
189
+ tags[index]
190
+ end
191
+
192
+ # true is the paragraph contains a tag that can earn credit
193
+ def creditable?
194
+ @tags.each { |t| return true if CREDITABLE.include?(t[0]) }
195
+ false
196
+ end
197
+
198
+ # true is the paragraph contains a tag that can earn impact credit
199
+ def impact_creditable?
200
+ @tags.each { |t| return true if t[0] == :I }
201
+ false
202
+ end
203
+
204
+ def period
205
+ return 0 if @end_time.nil? || @start_time.nil?
206
+ (@end_time - @start_time).to_i / 60
207
+ end
208
+
209
+ def latest_time
210
+ return @end_time unless @end_time.nil?
211
+ return @start_time unless @start_time.nil?
212
+ nil
213
+ end
214
+
215
+ private
216
+
217
+ TAG = 0
218
+ CONTENT = 1
219
+
220
+ def assign_class_dates(array)
221
+ Paragraph.st, Paragraph.et, Paragraph.date = array
222
+ end
223
+
224
+ # @return [Array] An array containing the class instant variables for the
225
+ # start time, end time and the date.
226
+ def class_dates
227
+ [Paragraph.st, Paragraph.et, Paragraph.date]
228
+ end
229
+
230
+ def tags_extracted?(str)
231
+ (@tags.count) < (@tags += extract_tags(str)).count
232
+ end
233
+
234
+ def tag_section(str)
235
+ tag_it(SECTIONS[Paragraph.section], str)
236
+ end
237
+
238
+ def content(tag, str = '')
239
+ @tags.each { |t| str << t[CONTENT] + ' ' if t[TAG] == tag }
240
+ str
241
+ end
242
+
243
+ def next_section
244
+ Paragraph.section += 1 unless Paragraph.section == (SECTIONS.count - 1)
245
+ end
246
+
247
+ def tag_it(tag, p)
248
+ @tags << [tag, p]
249
+ next_section if Paragraph.section == 0 # :TITLE
250
+ end
251
+
252
+ def method_missing(n, *args, &block)
253
+ if args[0].nil? # tag getter
254
+ ALL_TAGS.include?(n) ? content(n) : super(n, *args, &block)
255
+ else # section setter
256
+ SECTIONS.include?(n) ? tag_it(n, args[0]) : super(n, *args, &block)
257
+ end
258
+ end
259
+
260
+
261
+ # @todo this function does two things, split it up
262
+ # Sets the section to a simple :NOTE and transfers the class times to the
263
+ # instance times.
264
+ # new untagged sections should be of :NOTE (2)
265
+ def note_time
266
+ Paragraph.section = 2 #:NOTE
267
+ @start_time = Paragraph.st
268
+ @end_time = Paragraph.et
269
+ end
270
+ end
@@ -0,0 +1,158 @@
1
+ require_relative 'date_format.rb'
2
+ require 'English'
3
+
4
+ # handles extraction of tagged or other significant content
5
+ module Tags
6
+ # interface: extract_date, extract_tags
7
+ TAGS = [:LP, :R, :DEN, :NOTE, :I] # recognized in text
8
+ CREDITABLE = [:LP, :R, :I] # can earn cpd credit
9
+ SECTIONS = [:TITLE, :INTRO, :NOTE] # assumed from position in document
10
+ SPECIAL = [:DATE] # internal tags with no meaning from content or position
11
+ ALL_TAGS = SECTIONS + TAGS + SPECIAL
12
+
13
+ # extracts and formats tags and pertaining text from a plain text paragraph
14
+ class TagFriend
15
+ include DateFormat
16
+
17
+ # @todo move function to one of the date or time handling classes
18
+ # The $LAST_MATCH_INFO global is equivalent to Rexexp.last_match and
19
+ # returns a MatchData object. This can be used as an array, where indices
20
+ # 1 - n are the matched backreferences of the last successful match
21
+ # @param [String] paragraph_text a paragraph from a DSL text file
22
+ # @param [Time] date of this paragraph. May be nil if not known.
23
+ # @return [Array<String, Array, Time>] Array of values to be returned
24
+ # [String return value] 'paragraph_text' the same paragraph that was passed to the function but without the matched date character if there were any.
25
+ # [Array return value] 'time_array' array of 4 integer representing the hours and minutes of the from and to times
26
+ # [Time return value] 'date' the date in (day month year) of this paragraph taken from the matched date_regex if there was one. Will be nil if there was no match and if the date passed to the function was also nil.
27
+ def extract_date(paragraph_text, date)
28
+ time_array = []
29
+
30
+ # if text contains a date match
31
+ if date_regex =~ paragraph_text
32
+ # $' (or $POSTMATCH), contains the characters after the match position
33
+ paragraph_text = $'
34
+
35
+ # strip whitespace if any remaining match or set to empty string
36
+ # if no match. If there is just white space after the match then
37
+ # this is truncated to an empty string
38
+ paragraph_text.nil? ? paragraph_text = '' : paragraph_text.strip!
39
+
40
+ # extracts the 'from' and 'to' times from the last match above. the
41
+ # time_array contains from_hour, from_min, to_hour, to_min, the
42
+ # date parameter is updated if the match found a new date
43
+ time_array, date = date_from_globals($LAST_MATCH_INFO, date)
44
+ end
45
+ [paragraph_text, time_array, date]
46
+ end
47
+
48
+ def extract_tags(paragraph_text)
49
+ tag_regex =~ paragraph_text ? extract_tag(paragraph_text) : []
50
+ end
51
+
52
+ private
53
+
54
+ # @todo move function to one of the date or time handling classes
55
+ # returns a date from the 26 globals returned by date_regex
56
+ # @param [MatchData] glob_a the MatchData object return when the date_regex
57
+ # was matched to the paragraph
58
+ # @param [Time] date the date of the paragraph; may be nil if not known
59
+ # @return [Array] array of 4 integer representing the
60
+ # hours and minutes of the from and to times
61
+ # @return [Time] 'date' the date (day month year) of this paragraph
62
+ def date_from_globals(glob_a, date)
63
+ from_hour = glob([1, 23], glob_a)
64
+ from_min = glob([2, 24], glob_a)
65
+ to_hour = glob([3, 25], glob_a)
66
+ to_min = glob([4, 26], glob_a)
67
+ day = glob([5, 8, 12, 14, 17, 21], glob_a)
68
+ month = glob([6, 9, 11, 15, 18, 20], glob_a)
69
+ year = glob([7, 10, 13, 16, 19, 22], glob_a)
70
+ date = Time.at(format_date("#{day}-#{month}-#{year}")) unless day.nil?
71
+ [[from_hour, from_min, to_hour, to_min], date]
72
+ end
73
+
74
+ # @todo move function to one of the date or time handling classes
75
+ # Returns a regular expression to be used to match dates and times of
76
+ # the paragraph.
77
+ # @return [Regex] a regular expression to use to match dates and times
78
+ # in the paragraph
79
+ def date_regex
80
+ dy = /(?<day>\d{1,2})/
81
+ mt = /(?<month>\w+)/
82
+ yr = /(?<year>\d{2,4})/
83
+ time = /(?<hour>\d{1,2}):(?<min>\d{2})/
84
+ period = /#{time}( ?(?:-|to) ?#{time})?/
85
+ date1 = %r{#{dy}/#{dy}/#{yr}} # d/m/y
86
+ date2 = /#{dy},? #{mt},? #{yr}/ # d Month Year
87
+ date3 = /#{mt},? #{dy},? #{yr}/ # Month d Year
88
+ date = /#{date1}|#{date2}|#{date3}/
89
+ /^(#{period} ?#{date}?|#{date} ?#{period}?)/
90
+ end
91
+
92
+ # @todo would str be better with join?
93
+ # Creates a regex that can be used to match for tags that are recognized
94
+ # as part of the DSL, currently :LP, :R, :DEN, :NOTE and :I
95
+ def tag_regex
96
+ str = ''
97
+ TAGS.each do |t|
98
+ str << "#{t}|"
99
+ end
100
+ str.gsub!(/\|$/, '')
101
+ /\b(#{str}): ?/
102
+ end
103
+
104
+ # Extracts a particular parameter from the MatchData object return when the
105
+ # paragraph was matched with the date regex. Treats the MatchData
106
+ # as an array, iterating through each index represented in the i_a
107
+ # array to find and return a value if there is one.
108
+ # @param [Array] i_a Array of integers representing positions to test in
109
+ # array glob_a
110
+ # @param [MatchData] glob_a Array of matched backreferences of the last
111
+ # successful regular expression match
112
+ # @return the first element in MatchData that is not nil. Returns
113
+ # nil if there are no elements in MatchData at the indices in i_a that
114
+ # are not nil.
115
+ def glob(i_a, glob_a)
116
+ i_a.each { |n| return glob_a[n] unless glob_a[n].nil? }
117
+ nil
118
+ end
119
+
120
+ def preface_with_note(a)
121
+ str = a[0].strip
122
+ str == '' ? [] : [[:NOTE, str]]
123
+ end
124
+
125
+ def tags_array(a)
126
+ tags = []
127
+ tag_count = (a.count - 1) / 2
128
+ 1.upto(tag_count) do |i|
129
+ tag = a[(i * 2) - 1].to_sym
130
+ content = a[i * 2].strip
131
+ tags << [tag, content]
132
+ end
133
+ tags
134
+ end
135
+
136
+ def extract_tag(paragraph_text)
137
+ a = paragraph_text.split(tag_regex)
138
+ preface_with_note(a) + tags_array(a)
139
+ end
140
+ end
141
+
142
+ # @param [Time] from time to which hours and minutes are added
143
+ # @param [Number] hour hours to add to from
144
+ # @param [Number] min minutes to add to from
145
+ # @return [Time] the result of hours and minutes after from
146
+ def extract_time_object(hour, min, from)
147
+ seconds = (hour.to_i * 3600) + (min.to_i * 60)
148
+ Time.at(from.to_i + seconds)
149
+ end
150
+
151
+ def extract_tags(paragraph_text)
152
+ TagFriend.new.extract_tags(paragraph_text)
153
+ end
154
+
155
+ def extract_date(paragraph_text, date)
156
+ TagFriend.new.extract_date(paragraph_text, date)
157
+ end
158
+ end
@@ -0,0 +1,14 @@
1
+ # Collates information from different logs and can output collated information
2
+ # for use in appraisals.
3
+ class CollaterConsoleView < ConsoleView
4
+ def print_logs(diaries)
5
+ diary_console_view = DiaryConsoleView.new
6
+ diaries.each do |diary|
7
+ str = diary_console_view.output(diary)
8
+ unless str == ''
9
+ puts str
10
+ puts "\n#{'=' * OUTPUT_WIDTH}\n\n"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,228 @@
1
+ require 'colorize'
2
+ require_relative 'view.rb'
3
+
4
+ # A console view class for the Learning Diary
5
+ class DiaryConsoleView < ConsoleView
6
+ def initialize(diary = nil)
7
+ return if diary.nil?
8
+ @paragraphs = diary.paragraphs
9
+ @credits = diary.standard_credits
10
+ @credits_total = diary.standard_credits_total
11
+ @impact = diary.impact_credits
12
+ @impact_total = diary.impact_credits_total
13
+ @total = diary.credits_total
14
+ end
15
+
16
+ def output(diary = nil)
17
+ initialize(diary)
18
+ output_str
19
+ end
20
+
21
+ private
22
+
23
+ def output_str
24
+ str = ''
25
+ str += section_str('TITLE', content(:TITLE))
26
+ str += section_str('INTRODUCTION', content(:INTRO))
27
+ str += learning_str
28
+ str += credits_str
29
+ str
30
+ end
31
+
32
+ def learning_str(str = '')
33
+ str += section_str('LEARNING POINTS', content(:LP), true)
34
+ str += section_str('REFLECTION', reflection, true)
35
+ str += section_str('IMPACT', impact)
36
+ str += section_str('FURTHER STUDY', content(:DEN), true)
37
+ str += section_str('OTHER NOTES', content(:NOTE))
38
+ str
39
+ end
40
+
41
+ def credits_str(str = '')
42
+ str += section_str('CREDITS BREAKDOWN', credits_breakdown(@credits))
43
+ str += mins2hour(@credits_total) + "\n" unless @credits_total == 0
44
+ str += section_str('IMPACT BREAKDOWN', credits_breakdown(@impact))
45
+ str += mins2hour(@impact_total) + "\n" unless @impact_total == 0
46
+ str += section_str('TOTAL CREDITS CLAIMED', credits_total(@total))
47
+ str
48
+ end
49
+
50
+ def credits_total(total)
51
+ return '' if total == 0
52
+ total_line + mins2hour(total)
53
+ end
54
+
55
+ def content(tag)
56
+ arr = []
57
+ @paragraphs.each do |p|
58
+ if block_given? # yields the whole paragraph for further processing
59
+ arr = yield p if p.tag?(tag)
60
+ else # strip tag texts to an array of one text element per tag
61
+ text = p.send(tag).strip
62
+ arr << text unless text == ''
63
+ end
64
+ end
65
+ arr
66
+ end
67
+
68
+ def credits_breakdown(credits_array)
69
+ return '' if credits_array.empty?
70
+ str = ''
71
+ credit_count = credits_array.count - 1
72
+ 0.upto(credit_count) do |i|
73
+ prev_credit, credit = elements(credits_array, i)
74
+ str << take_credit(prev_credit, credit, i)
75
+ end
76
+ str + sub_total_line
77
+ end
78
+
79
+ def reflection
80
+ arr = []
81
+ content(:R) do |p|
82
+ arr += reflections(p)
83
+ end
84
+ arr
85
+ end
86
+
87
+ def impact
88
+ arr = []
89
+ old_cr = []
90
+ date = ''
91
+ content(:I) { |p| old_cr = credit_line(p); break }
92
+ statement_a = []
93
+ content(:I) do |p|
94
+ date = strftime(p.start_time, '%e-%b-%y').underline
95
+ sta = impact_statement(p)
96
+ cr = credit_line(p)
97
+ if cr != old_cr # group claims of the same time/credit
98
+ arr << statement_line(date, statement_a, old_cr)
99
+ statement_a = []
100
+ statement_a << add_impact_statement(sta)
101
+ old_cr = cr
102
+ next
103
+ end
104
+ statement_a << add_impact_statement(sta)
105
+ end
106
+ line = statement_line(date, statement_a, old_cr)
107
+ arr << line unless line.nil?
108
+ arr
109
+ end
110
+
111
+ # @param [Array] e element - statement array from #impact_statement(paragraph) in
112
+ # the form [has_info?(Boolean), text(String)]
113
+ def add_impact_statement(e)
114
+ if e[0] == false
115
+ impact_err(e[1])
116
+ else
117
+ "\n\n#{e[1]}\n\n"
118
+ end
119
+ end
120
+
121
+ def statement_line(date, statement_a, old_cr)
122
+ return if statement_a.empty?
123
+ statement = statement_a.join
124
+ "\n#{date}#{statement}#{old_cr}".gsub(/\n\n\n\n/, "\n\n")
125
+ end
126
+
127
+ # @[param] p paragraph
128
+ def credit_line(p)
129
+ st = p.start_time
130
+ fin = p.end_time
131
+ amount = ((fin - st) / 60).to_i
132
+ cr = "Impact Credit Claimed: #{amount} mins"
133
+ cr += ' based on an original period of study and reflection from'
134
+ cr + " #{print_date_and_time(st)} to #{end_time(st, fin)}\n"
135
+ end
136
+
137
+ def impact_statement(paragraph)
138
+ arr = []
139
+ ref = rm_colour(reflections(paragraph).join(' '))
140
+ paragraph.each { |tag| arr << tag[1] if tag[0] == :I }
141
+ sta = arr.join(' ')
142
+ ref == '' ? [false, sta] : [true, "#{ref}\n\n#{sta}"]
143
+ end
144
+
145
+ def impact_err(sta)
146
+ ret = "#{sta}" + " Warning - this impact statement does"\
147
+ " not pertain to any learning points or reflection.".colorize(:red)
148
+ "\n\n#{ret}\n\n"
149
+ end
150
+
151
+ # @param [Array] paragraph A paragraph containing one or more reflections.
152
+ # The paragraph is an array of tags of the form [tag symbol, string]
153
+ # @return an array of reflections
154
+ # The reflection text is returned with preceeding learning points formatted
155
+ # in a different style. A learning point is attached to the reflection
156
+ # if it preceeds the reflection, is in the same paragraph and there are
157
+ # no other intervening reflections in the same paragraph i.e. Within the
158
+ # same paragraph, a reflection will take the preceeding LPs and then leave
159
+ # subsequent LPs which will be picked up by a subsequent reflection if
160
+ # there is one. Such successive reflections, within the same paragraph, can
161
+ # be placed in order and in context when reported.
162
+ def reflections(paragraph)
163
+ arr = []
164
+ lp = ''
165
+ paragraph.each do |tag|
166
+ if tag[0] == :R
167
+ arr << (lp + tag[1].colorize(:green))
168
+ lp = ''
169
+ end
170
+ lp += (tag[1] + ' ').colorize(:light_black) if tag[0] == :LP
171
+ end
172
+ arr
173
+ end
174
+
175
+ def on_same_day?(time1, time2)
176
+ one_day = 60 * 60 * 24
177
+ (time1.to_i / one_day).to_i == (time2.to_i / one_day).to_i
178
+ end
179
+
180
+ # @return [Array] The previous and current elements in the array
181
+ def elements(a, i)
182
+ i > 0 ? [a[i - 1], a[i]] : [[], a[i]]
183
+ end
184
+
185
+ def strftime(t, format)
186
+ t.strftime(format)
187
+ end
188
+
189
+ def print_just_time(t)
190
+ strftime(t, '%H:%M')
191
+ end
192
+
193
+ def print_date_and_time(t)
194
+ strftime(t, '%e-%b-%y %H:%M')
195
+ end
196
+
197
+ def print_time_and_date(t)
198
+ strftime(t, '%H:%M (%-e-%b)')
199
+ end
200
+
201
+ def start_time(cr, prev_cr, i)
202
+ if i == 0 # first line always has the full date and time
203
+ print_date_and_time(cr)
204
+ else
205
+ on_same_day?(cr, prev_cr) ? print_just_time(cr) : print_date_and_time(cr)
206
+ end
207
+ end
208
+
209
+ def end_time(st, fin)
210
+ on_same_day?(st, fin) ? print_just_time(fin) : print_time_and_date(fin)
211
+ end
212
+
213
+ def credit_period(prev_credit, credit, i)
214
+ str = start_time(credit[0], prev_credit[0], i).rjust(16)
215
+ str << ' - '
216
+ str << end_time(credit[0], credit[1]).ljust(16)
217
+ end
218
+
219
+ def credit_amount(amount)
220
+ str = "#{amount}".rjust(4)
221
+ str << " mins\n"
222
+ end
223
+
224
+ def take_credit(prev_credit, credit, i)
225
+ str = credit_period(prev_credit, credit, i)
226
+ str << credit_amount(credit[2])
227
+ end
228
+ end
@@ -0,0 +1,91 @@
1
+ # parent view class
2
+ class View
3
+ protected
4
+
5
+ def rm_colour(str)
6
+ str.gsub(/\e.*?m/, '')
7
+ end
8
+ end
9
+
10
+ class ConsoleView < View
11
+ protected
12
+
13
+ OUTPUT_WIDTH = 80
14
+
15
+ def print_array_section(title, body, list)
16
+ return '' if body.empty?
17
+ if list
18
+ to_console("#{title}") + "\n\n" +
19
+ to_console("• #{body.join("\n\n• ")}\n\n", 2, 4)
20
+ else
21
+ to_console("#{title}") +
22
+ to_console("#{body.join}\n\n")
23
+ end
24
+ end
25
+
26
+ def section_str(title, body = nil, list = false)
27
+ return '' if body.nil?
28
+ case body
29
+ when Array
30
+ return '' if body.empty?
31
+ print_array_section(title, body, list)
32
+ when String
33
+ return '' if body.strip == ''
34
+ "#{title}\n\n#{body}"
35
+ end
36
+ end
37
+
38
+ def line(char)
39
+ ' ' * 27 + char * 18 + "\n"
40
+ end
41
+
42
+ def sub_total_line
43
+ line('─')
44
+ end
45
+
46
+ def total_line
47
+ line('═')
48
+ end
49
+
50
+ def mins2hour(mins)
51
+ hours = (mins.to_r / 60).to_i
52
+ mins -= hours * 60
53
+ hours == 1 ? h_str = 'hour' : h_str = 'hours'
54
+ mins == 1 ? m_str = 'min' : m_str = 'mins'
55
+ "#{hours}".rjust(30) + " #{h_str}".ljust(6) +
56
+ "#{mins.to_i}".rjust(3) + " #{m_str}\n"
57
+ end
58
+
59
+ # prints the word to the console
60
+ def word2console(word, x_pos, margin)
61
+ str = ''
62
+ length = 1 + rm_colour(word).length
63
+ if x_pos + length >= OUTPUT_WIDTH
64
+ str += "\n" + (' ' * margin)
65
+ x_pos = margin + length
66
+ else
67
+ x_pos += length
68
+ end
69
+ str != '' ? debug = true : debug = false
70
+ str += word
71
+ [x_pos, str]
72
+ end
73
+
74
+ def to_console(str, first_line_margin = 0, margin = 0)
75
+ return_str = ''
76
+ str.each_line do |line|
77
+ if line == "\n"
78
+ return_str += "\n\n"
79
+ next
80
+ end
81
+ return_str += ' ' * first_line_margin
82
+ x_pos = first_line_margin
83
+ words = line.split(' ')
84
+ words.each do |word|
85
+ x_pos, this_str = word2console(word, x_pos, margin)
86
+ return_str += this_str + ' '
87
+ end
88
+ end
89
+ return_str
90
+ end
91
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: docfolio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Bulmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A domain specific language to aid recording of a personal learning portfolio
14
+ for UK General Practitioners.
15
+ email: n.bulmer@live.co.uk
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/docfolio.rb
21
+ - lib/docfolio/date_format.rb
22
+ - lib/docfolio/docfolio.rb
23
+ - lib/docfolio/learning_diary.rb
24
+ - lib/docfolio/paragraph.rb
25
+ - lib/docfolio/tags.rb
26
+ - lib/docfolio/views/collater_console_view.rb
27
+ - lib/docfolio/views/diary_console_view.rb
28
+ - lib/docfolio/views/view.rb
29
+ homepage: https://github.com/nickbulmer/docfolio
30
+ licenses:
31
+ - GNU
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.4.8
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: A DSL for GP CPD
53
+ test_files: []
54
+ has_rdoc: