fat_core 0.0.1

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,7 @@
1
+ module Enumerable
2
+ # Emit item in groups of n
3
+ def groups_of(n)
4
+ k = -1
5
+ group_by {|item| k += 1; k.div(n)}
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ class Hash
2
+ # Return all keys in hash that have a value == to the given value or have an
3
+ # Enumerable value that includes the given value.
4
+ def keys_with_value(val)
5
+ result = []
6
+ each_pair do |k, v|
7
+ if self[k] == val || (v.respond_to?(:include?) && v.include?(val))
8
+ result << k
9
+ end
10
+ end
11
+ result
12
+ end
13
+
14
+ # Remove from the hash all keys that have values == to given value or that
15
+ # include the given value if the hash has an Enumerable for a value
16
+ def delete_with_value(v)
17
+ keys_with_value(v).each do |k|
18
+ delete(k)
19
+ end
20
+ end
21
+
22
+ def remap_keys(key_map = {})
23
+ new_hash = {}
24
+ each_pair do |key, val|
25
+ if key_map.has_key?(key)
26
+ new_hash[key_map[key]] = val
27
+ else
28
+ new_hash[key] = val
29
+ end
30
+ end
31
+ new_hash
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Kernel
2
+ def time_it(message = '', &block)
3
+ start = Time.now
4
+ result = yield block
5
+ run_time = Time.now - start
6
+ puts "Ran #{message} in #{run_time.secs_to_hms}"
7
+ result
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require 'erubis'
2
+ require 'erubis/enhancer'
3
+ require 'erubis/helper'
4
+
5
+ class LaTeXEruby < Erubis::Eruby
6
+ include Erubis::EscapeEnhancer
7
+
8
+ def escaped_expr(code)
9
+ code.nil? ? '' : "(#{code}).tex_quote"
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class NilClass
2
+ def entitle
3
+ nil
4
+ end
5
+
6
+ def tex_quote
7
+ ''
8
+ end
9
+ end
@@ -0,0 +1,87 @@
1
+ class Numeric
2
+ def signum
3
+ if self > 0
4
+ 1
5
+ elsif self < 0
6
+ -1
7
+ else
8
+ 0
9
+ end
10
+ end
11
+
12
+ def commas(places = nil)
13
+ # By default, use zero places for whole numbers; four places for
14
+ # numbers containing a fractional part to 4 places.
15
+ if places.nil?
16
+ if self.modulo(1).round(4) > 0.0
17
+ places = 4
18
+ else
19
+ places = 0
20
+ end
21
+ end
22
+ group(places, ',')
23
+ end
24
+
25
+ def group(places = 0, delim = ',')
26
+ # Return number as a string with embedded commas
27
+ # for nice printing; round to places places after
28
+ # the decimal
29
+
30
+ # Only convert to string numbers with exponent unless they are
31
+ # less than 1 (to ensure that really small numbers round to 0.0)
32
+ if self.abs > 1.0 && self.to_s =~ /e/
33
+ return self.to_s
34
+ end
35
+
36
+ str = self.to_f.round(places).to_s
37
+
38
+ # Break the number into parts
39
+ str =~ /^(-)?(\d*)((\.)?(\d*))?$/
40
+ neg = $1 || ''
41
+ whole = $2
42
+ frac = $5
43
+
44
+ # Pad out the fractional part with zeroes to the right
45
+ n_zeroes = [places - frac.length, 0].max
46
+ frac += "0" * n_zeroes if n_zeroes > 0
47
+
48
+ # Place the commas in the whole part only
49
+ whole = whole.reverse
50
+ whole.gsub!(/([0-9]{3})/, "\\1#{delim}")
51
+ whole.gsub!(/#{Regexp.escape(delim)}$/, '')
52
+ whole.reverse!
53
+ if frac.nil? || places <= 0
54
+ return neg + whole
55
+ else
56
+ return neg + whole + '.' + frac
57
+ end
58
+ end
59
+
60
+ # Return an integer type, but only if the fractional part of self
61
+ # is zero
62
+ def int_if_whole
63
+ self.floor == self ? self.floor : self
64
+ end
65
+
66
+ def secs_to_hms
67
+ frac = self % 1
68
+ mins, secs = self.divmod(60)
69
+ hrs, mins = mins.divmod(60)
70
+ if frac.round(5) > 0.0
71
+ "%02d:%02d:%02d.%d" % [hrs, mins, secs, frac.round(5) * 100]
72
+ else
73
+ "%02d:%02d:%02d" % [hrs, mins, secs]
74
+ end
75
+ end
76
+
77
+ # Allow erb documents can directly interpolate numbers
78
+ def tex_quote
79
+ to_s
80
+ end
81
+ end
82
+
83
+ class BigDecimal
84
+ def inspect
85
+ to_s
86
+ end
87
+ end
@@ -0,0 +1,410 @@
1
+ # -*- coding: utf-8 -*-
2
+ class Period
3
+ include Enumerable
4
+ include Comparable
5
+
6
+ attr_accessor :first, :last
7
+
8
+ def initialize(first, last)
9
+ case first
10
+ when String
11
+ begin
12
+ first = Date.parse(first)
13
+ rescue ArgumentError => ex
14
+ if ex.message =~ /invalid date/
15
+ raise ArgumentError, "you gave an invalid date '#{first}'"
16
+ else
17
+ raise
18
+ end
19
+ end
20
+ when Date
21
+ first = first
22
+ else
23
+ raise ArgumentError, "use Date or String to initialize Period"
24
+ end
25
+
26
+ case last
27
+ when String
28
+ begin
29
+ last = Date.parse(last)
30
+ rescue ArgumentError => ex
31
+ if ex.message =~ /invalid date/
32
+ raise ArgumentError, "you gave an invalid date '#{last}'"
33
+ else
34
+ raise
35
+ end
36
+ end
37
+ when Date
38
+ last = last
39
+ else
40
+ raise ArgumentError, "use Date or String to initialize Period"
41
+ end
42
+
43
+ @first = first
44
+ @last = last
45
+ if @first > @last
46
+ raise ArgumentError, "Period's first date is later than its last date"
47
+ end
48
+ end
49
+
50
+ # Possibly useful class method to take an array of periods and join all the
51
+ # contiguous ones, then return an array of the disjoint periods not
52
+ # contiguous to one another. An array of periods with no gaps should return
53
+ # an array of only one period spanning all the given periods.
54
+
55
+ # Return an array of periods that represent the concatenation of all
56
+ # adjacent periods in the given periods.
57
+ # def self.meld_periods(*periods)
58
+ # melded_periods = []
59
+ # while (this_period = periods.pop)
60
+ # melded_periods.each do |mp|
61
+ # if mp.overlaps?(this_period)
62
+ # melded_periods.delete(mp)
63
+ # melded_periods << mp.union(this_period)
64
+ # break
65
+ # elsif mp.contiguous?(this_period)
66
+ # melded_periods.delete(mp)
67
+ # melded_periods << mp.join(this_period)
68
+ # break
69
+ # end
70
+ # end
71
+ # end
72
+ # melded_periods
73
+ # end
74
+
75
+ # TO_DATE = Period.new(Date::BOT, Date.current)
76
+ # FOREVER = Period.new(Date::BOT, Date::EOT)
77
+
78
+ def each
79
+ d = first
80
+ while d <= last
81
+ yield d
82
+ d = d + 1.day
83
+ end
84
+ end
85
+
86
+ def self.chunk_syms
87
+ [:day, :week, :biweek, :semimonth, :month, :bimonth,
88
+ :quarter, :year, :irregular]
89
+ end
90
+
91
+ def self.chunk_sym_to_days(sym)
92
+ case sym
93
+ when :day
94
+ 1
95
+ when :week
96
+ 7
97
+ when :biweek
98
+ 14
99
+ when :semimonth
100
+ 15
101
+ when :month
102
+ 30
103
+ when :bimonth
104
+ 60
105
+ when :quarter
106
+ 90
107
+ when :year
108
+ 365
109
+ when :irregular
110
+ 30
111
+ else
112
+ raise ArgumentError, "unknown chunk sym '#{sym}'"
113
+ end
114
+ end
115
+
116
+ # The largest number of days possible in each chunk
117
+ def self.chunk_sym_to_max_days(sym)
118
+ case sym
119
+ when :semimonth
120
+ 16
121
+ when :month
122
+ 31
123
+ when :bimonth
124
+ 62
125
+ when :quarter
126
+ 92
127
+ when :year
128
+ 366
129
+ else
130
+ chunk_sym_to_days(sym)
131
+ end
132
+ end
133
+
134
+ # This is only used for inferring statement frequency based on the number
135
+ # of days between statements, so it will not consider all possible chunks,
136
+ # only :year, :quarter, :month, and :week. And distinguishing between
137
+ # :semimonth and :biweek is impossible anyway. Since statement dates can
138
+ # bounce around quite a bit in my experience, this is really fuzzy. For
139
+ # example, one of my banks does monthly statements "around" the 10th of
140
+ # the month, but the 10th can get pushed off by holidays, weekends, etc.,
141
+ # so a "quarter" here is much broader than the calendar definition. Ditto
142
+ # for the others, but since statements are most likely monthly, we default
143
+ # to :month.
144
+ def self.days_to_chunk_sym(days)
145
+ case days
146
+ when 356..376
147
+ :year
148
+ when 86..96
149
+ :quarter
150
+ when 26..33
151
+ :month
152
+ when 7
153
+ :week
154
+ when 1
155
+ :day
156
+ else
157
+ :irregular
158
+ end
159
+ end
160
+
161
+ def size
162
+ to_range.size
163
+ end
164
+
165
+ def length
166
+ size
167
+ end
168
+
169
+ def to_range
170
+ (first..last)
171
+ end
172
+
173
+ def ==(other)
174
+ first == other.first && last == other.last
175
+ end
176
+
177
+ def <=>(other)
178
+ [first, size] <=> [other.first, other.size]
179
+ end
180
+
181
+ def to_s
182
+ if first.beginning_of_year? && last.end_of_year? && first.year == last.year
183
+ "#{first.year}"
184
+ elsif first.beginning_of_quarter? &&
185
+ last.end_of_quarter? &&
186
+ first.year == last.year &&
187
+ first.quarter == last.quarter
188
+ "#{first.year}-#{first.quarter}Q"
189
+ elsif first.beginning_of_month? &&
190
+ last.end_of_month? &&
191
+ first.year == last.year &&
192
+ first.month == last.month
193
+ "#{first.year}-%02d" % first.month
194
+ else
195
+ "#{first.iso} to #{last.iso}"
196
+ end
197
+ end
198
+
199
+ # Allow erb documents can directly interpolate ranges
200
+ def tex_quote
201
+ "#{first.iso}--#{last.iso}"
202
+ end
203
+
204
+ # Days in period
205
+ def size
206
+ (last - first + 1).to_i
207
+ end
208
+
209
+ def length
210
+ size
211
+ end
212
+
213
+ def subset_of?(other)
214
+ to_range.subset_of?(other.to_range)
215
+ end
216
+
217
+ def proper_subset_of?(other)
218
+ to_range.proper_subset_of?(other.to_range)
219
+ end
220
+
221
+ def superset_of?(other)
222
+ to_range.superset_of?(other.to_range)
223
+ end
224
+
225
+ def proper_superset_of?(other)
226
+ to_range.proper_superset_of?(other.to_range)
227
+ end
228
+
229
+ def overlaps?(other)
230
+ self.to_range.overlaps?(other.to_range)
231
+ end
232
+
233
+ def intersection(other)
234
+ self.to_range.intersection(other.to_range)
235
+ end
236
+ alias_method :&, :intersection
237
+
238
+ def union(other)
239
+ self.to_range.union(other.to_range)
240
+ end
241
+ alias_method :+, :union
242
+
243
+ def difference(other)
244
+ self.to_range.difference(other.to_range)
245
+ end
246
+ alias_method :-, :difference
247
+
248
+ # returns the chunk sym represented by the period
249
+ def chunk_sym
250
+ if first.beginning_of_year? && last.end_of_year? &&
251
+ (365..366) === last - first + 1
252
+ :year
253
+ elsif first.beginning_of_quarter? && last.end_of_quarter? &&
254
+ (90..92) === last - first + 1
255
+ :quarter
256
+ elsif first.beginning_of_bimonth? && last.end_of_bimonth? &&
257
+ (58..62) === last - first + 1
258
+ :bimonth
259
+ elsif first.beginning_of_month? && last.end_of_month? &&
260
+ (28..31) === last - first + 1
261
+ :month
262
+ elsif first.beginning_of_semimonth? && last.end_of_semimonth &&
263
+ (13..16) === last - first + 1
264
+ :semimonth
265
+ elsif first.beginning_of_biweek? && last.end_of_biweek? &&
266
+ last - first + 1 == 14
267
+ :biweek
268
+ elsif first.beginning_of_week? && last.end_of_week? &&
269
+ last - first + 1 == 7
270
+ :week
271
+ elsif first == last
272
+ :day
273
+ else
274
+ :irregular
275
+ end
276
+ end
277
+
278
+ # Name for a period not necessarily ending on calendar boundaries. For
279
+ # example, in reporting reconciliation, we want the period from Feb 11,
280
+ # 2014, to March 10, 2014, be called the 'Month ending March 10, 2014,'
281
+ # event though the period is not a calendar month. Using the stricter
282
+ # Period#chunk_sym, would not allow such looseness.
283
+ def chunk_name
284
+ case Period.days_to_chunk_sym(length)
285
+ when :year
286
+ 'Year'
287
+ when :quarter
288
+ 'Quarter'
289
+ when :bimonth
290
+ 'Bi-month'
291
+ when :month
292
+ 'Month'
293
+ when :semimonth
294
+ 'Semi-month'
295
+ when :biweek
296
+ 'Bi-week'
297
+ when :week
298
+ 'Week'
299
+ when :day
300
+ 'Day'
301
+ else
302
+ 'Period'
303
+ end
304
+ end
305
+
306
+ def contains?(date)
307
+ self.to_range.cover?(date)
308
+ end
309
+
310
+ def overlaps?(other)
311
+ self.to_range.overlaps?(other.to_range)
312
+ end
313
+
314
+ # Return whether any of the Periods that are within self overlap one
315
+ # another
316
+ def has_overlaps_within?(periods)
317
+ self.to_range.has_overlaps_within?(periods.map{ |p| p.to_range})
318
+ end
319
+
320
+ def spanned_by?(periods)
321
+ to_range.spanned_by?(periods.map { |p| p.to_range })
322
+ end
323
+
324
+ def gaps(periods)
325
+ to_range.gaps(periods.map { |p| p.to_range }).
326
+ map { |r| Period.new(r.first, r.last)}
327
+ end
328
+
329
+ # Return an array of Periods wholly-contained within self in chunks of
330
+ # size, defaulting to monthly chunks. Partial chunks at the beginning and
331
+ # end of self are not included unless partial_first or partial_last,
332
+ # respectively, are set true. The last chunk can be made to extend beyond
333
+ # the end of self to make it a whole chunk if round_up_last is set true,
334
+ # in which case, partial_last is ignored.
335
+ def chunks(size: :month, partial_first: false, partial_last: false, round_up_last: false)
336
+ size = size.to_sym
337
+ result = []
338
+ chunk_start = first.dup
339
+ while chunk_start <= last
340
+ case size
341
+ when :year
342
+ unless partial_first
343
+ until chunk_start.beginning_of_year?
344
+ chunk_start += 1.day
345
+ end
346
+ end
347
+ chunk_end = chunk_start.end_of_year
348
+ when :quarter
349
+ unless partial_first
350
+ until chunk_start.beginning_of_quarter?
351
+ chunk_start += 1.day
352
+ end
353
+ end
354
+ chunk_end = chunk_start.end_of_quarter
355
+ when :bimonth
356
+ unless partial_first
357
+ until chunk_start.beginning_of_bimonth?
358
+ chunk_start += 1.day
359
+ end
360
+ end
361
+ chunk_end = (chunk_start.end_of_month + 1.day).end_of_month
362
+ when :month
363
+ unless partial_first
364
+ until chunk_start.beginning_of_month?
365
+ chunk_start += 1.day
366
+ end
367
+ end
368
+ chunk_end = chunk_start.end_of_month
369
+ when :semimonth
370
+ unless partial_first
371
+ until chunk_start.beginning_of_semimonth?
372
+ chunk_start += 1.day
373
+ end
374
+ end
375
+ chunk_end = chunk_start.end_of_semimonth
376
+ when :biweek
377
+ unless partial_first
378
+ until chunk_start.beginning_of_biweek?
379
+ chunk_start += 1.day
380
+ end
381
+ end
382
+ chunk_end = chunk_start.end_of_biweek
383
+ when :week
384
+ unless partial_first
385
+ until chunk_start.beginning_of_week?
386
+ chunk_start += 1.day
387
+ end
388
+ end
389
+ chunk_end = chunk_start.end_of_week
390
+ when :day
391
+ chunk_end = chunk_start
392
+ else
393
+ chunk_end = last
394
+ end
395
+ if chunk_end <= last
396
+ result << Period.new(chunk_start, chunk_end)
397
+ else
398
+ if round_up_last
399
+ result << Period.new(chunk_start, chunk_end)
400
+ elsif partial_last
401
+ result << Period.new(chunk_start, last)
402
+ else
403
+ break
404
+ end
405
+ end
406
+ chunk_start = result.last.last + 1.day
407
+ end
408
+ result
409
+ end
410
+ end