mainej-activewarehouse 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/activewarehouse/README +99 -0
  2. data/activewarehouse/Rakefile +165 -0
  3. data/activewarehouse/TODO +4 -0
  4. data/activewarehouse/db/migrations/001_create_table_reports.rb +28 -0
  5. data/activewarehouse/doc/references.txt +4 -0
  6. data/activewarehouse/generators/bridge/USAGE +1 -0
  7. data/activewarehouse/generators/bridge/bridge_generator.rb +46 -0
  8. data/activewarehouse/generators/bridge/templates/fixture.yml +5 -0
  9. data/activewarehouse/generators/bridge/templates/migration.rb +27 -0
  10. data/activewarehouse/generators/bridge/templates/model.rb +3 -0
  11. data/activewarehouse/generators/bridge/templates/unit_test.rb +8 -0
  12. data/activewarehouse/generators/cube/USAGE +1 -0
  13. data/activewarehouse/generators/cube/cube_generator.rb +28 -0
  14. data/activewarehouse/generators/cube/templates/model.rb +3 -0
  15. data/activewarehouse/generators/cube/templates/unit_test.rb +8 -0
  16. data/activewarehouse/generators/date_dimension/USAGE +1 -0
  17. data/activewarehouse/generators/date_dimension/date_dimension_generator.rb +16 -0
  18. data/activewarehouse/generators/date_dimension/templates/fixture.yml +5 -0
  19. data/activewarehouse/generators/date_dimension/templates/migration.rb +31 -0
  20. data/activewarehouse/generators/date_dimension/templates/model.rb +3 -0
  21. data/activewarehouse/generators/date_dimension/templates/unit_test.rb +8 -0
  22. data/activewarehouse/generators/dimension/USAGE +1 -0
  23. data/activewarehouse/generators/dimension/dimension_generator.rb +46 -0
  24. data/activewarehouse/generators/dimension/templates/fixture.yml +5 -0
  25. data/activewarehouse/generators/dimension/templates/migration.rb +11 -0
  26. data/activewarehouse/generators/dimension/templates/model.rb +3 -0
  27. data/activewarehouse/generators/dimension/templates/unit_test.rb +8 -0
  28. data/activewarehouse/generators/dimension_view/USAGE +1 -0
  29. data/activewarehouse/generators/dimension_view/dimension_view_generator.rb +62 -0
  30. data/activewarehouse/generators/dimension_view/templates/migration.rb +17 -0
  31. data/activewarehouse/generators/dimension_view/templates/model.rb +3 -0
  32. data/activewarehouse/generators/dimension_view/templates/unit_test.rb +10 -0
  33. data/activewarehouse/generators/fact/USAGE +1 -0
  34. data/activewarehouse/generators/fact/fact_generator.rb +46 -0
  35. data/activewarehouse/generators/fact/templates/fixture.yml +5 -0
  36. data/activewarehouse/generators/fact/templates/migration.rb +13 -0
  37. data/activewarehouse/generators/fact/templates/model.rb +3 -0
  38. data/activewarehouse/generators/fact/templates/unit_test.rb +10 -0
  39. data/activewarehouse/generators/time_dimension/USAGE +1 -0
  40. data/activewarehouse/generators/time_dimension/templates/fixture.yml +5 -0
  41. data/activewarehouse/generators/time_dimension/templates/migration.rb +12 -0
  42. data/activewarehouse/generators/time_dimension/templates/model.rb +3 -0
  43. data/activewarehouse/generators/time_dimension/templates/unit_test.rb +8 -0
  44. data/activewarehouse/generators/time_dimension/time_dimension_generator.rb +14 -0
  45. data/activewarehouse/init.rb +1 -0
  46. data/activewarehouse/install.rb +5 -0
  47. data/activewarehouse/lib/active_warehouse.rb +91 -0
  48. data/activewarehouse/lib/active_warehouse/aggregate.rb +75 -0
  49. data/activewarehouse/lib/active_warehouse/aggregate/dwarf_aggregate.rb +369 -0
  50. data/activewarehouse/lib/active_warehouse/aggregate/dwarf_common.rb +44 -0
  51. data/activewarehouse/lib/active_warehouse/aggregate/dwarf_printer.rb +34 -0
  52. data/activewarehouse/lib/active_warehouse/aggregate/no_aggregate.rb +212 -0
  53. data/activewarehouse/lib/active_warehouse/aggregate/pid_aggregate.rb +29 -0
  54. data/activewarehouse/lib/active_warehouse/aggregate_field.rb +59 -0
  55. data/activewarehouse/lib/active_warehouse/bridge.rb +19 -0
  56. data/activewarehouse/lib/active_warehouse/bridge/hierarchy_bridge.rb +46 -0
  57. data/activewarehouse/lib/active_warehouse/builder.rb +3 -0
  58. data/activewarehouse/lib/active_warehouse/builder/date_dimension_builder.rb +91 -0
  59. data/activewarehouse/lib/active_warehouse/builder/generator/generator.rb +13 -0
  60. data/activewarehouse/lib/active_warehouse/builder/generator/name_generator.rb +20 -0
  61. data/activewarehouse/lib/active_warehouse/builder/generator/paragraph_generator.rb +11 -0
  62. data/activewarehouse/lib/active_warehouse/builder/random_data_builder.rb +239 -0
  63. data/activewarehouse/lib/active_warehouse/builder/test_data_builder.rb +54 -0
  64. data/activewarehouse/lib/active_warehouse/calculated_field.rb +27 -0
  65. data/activewarehouse/lib/active_warehouse/compat/compat.rb +49 -0
  66. data/activewarehouse/lib/active_warehouse/core_ext.rb +1 -0
  67. data/activewarehouse/lib/active_warehouse/core_ext/time.rb +5 -0
  68. data/activewarehouse/lib/active_warehouse/core_ext/time/calculations.rb +40 -0
  69. data/activewarehouse/lib/active_warehouse/cube.rb +235 -0
  70. data/activewarehouse/lib/active_warehouse/cube_query_result.rb +69 -0
  71. data/activewarehouse/lib/active_warehouse/dimension.rb +329 -0
  72. data/activewarehouse/lib/active_warehouse/dimension/date_dimension.rb +15 -0
  73. data/activewarehouse/lib/active_warehouse/dimension/dimension_reflection.rb +21 -0
  74. data/activewarehouse/lib/active_warehouse/dimension/dimension_view.rb +27 -0
  75. data/activewarehouse/lib/active_warehouse/dimension/hierarchical_dimension.rb +99 -0
  76. data/activewarehouse/lib/active_warehouse/dimension/slowly_changing_dimension.rb +147 -0
  77. data/activewarehouse/lib/active_warehouse/fact.rb +239 -0
  78. data/activewarehouse/lib/active_warehouse/field.rb +74 -0
  79. data/activewarehouse/lib/active_warehouse/migrations.rb +64 -0
  80. data/activewarehouse/lib/active_warehouse/ordered_hash.rb +34 -0
  81. data/activewarehouse/lib/active_warehouse/prejoin_fact.rb +97 -0
  82. data/activewarehouse/lib/active_warehouse/report.rb +7 -0
  83. data/activewarehouse/lib/active_warehouse/report/abstract_report.rb +149 -0
  84. data/activewarehouse/lib/active_warehouse/report/chart_report.rb +9 -0
  85. data/activewarehouse/lib/active_warehouse/report/data_cell.rb +21 -0
  86. data/activewarehouse/lib/active_warehouse/report/data_column.rb +19 -0
  87. data/activewarehouse/lib/active_warehouse/report/data_row.rb +15 -0
  88. data/activewarehouse/lib/active_warehouse/report/dimension.rb +58 -0
  89. data/activewarehouse/lib/active_warehouse/report/table_report.rb +38 -0
  90. data/activewarehouse/lib/active_warehouse/version.rb +9 -0
  91. data/activewarehouse/lib/active_warehouse/view.rb +9 -0
  92. data/activewarehouse/lib/active_warehouse/view/crumb.rb +64 -0
  93. data/activewarehouse/lib/active_warehouse/view/report_helper.rb +98 -0
  94. data/activewarehouse/lib/active_warehouse/view/table_view.rb +134 -0
  95. data/activewarehouse/lib/active_warehouse/view/yui_adapter.rb +68 -0
  96. data/activewarehouse/tasks/active_warehouse_tasks.rake +122 -0
  97. metadata +237 -0
@@ -0,0 +1,13 @@
1
+ module ActiveWarehouse
2
+ module Builder
3
+ module Generator
4
+ # Base class for generators
5
+ class Generator
6
+ # Get the next value from the generator.
7
+ def next(options={})
8
+ raise "Abstract method"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveWarehouse #:nodoc:
2
+ module Builder #:nodoc:
3
+ module Generator #:nodoc:
4
+ # Generate a name consisting of one or more words from word groups
5
+ class NameGenerator < ActiveWarehouse::Builder::Generator::Generator
6
+ def next(options={})
7
+ options[:separator] ||= ' '
8
+ parts = []
9
+ word_groups = options[:word_groups]
10
+ 0.upto(word_groups.first.length) do |i|
11
+ word_groups.each do |word_group|
12
+ parts << word_group[i]
13
+ end
14
+ end
15
+ parts.join(options[:separator])
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveWarehouse
2
+ module Builder
3
+ module Generator
4
+ class ParagraphGenerator < ActiveWarehouse::Builder::Generator::Generator
5
+ def next(options={})
6
+
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,239 @@
1
+ module ActiveWarehouse #:nodoc:
2
+ module Builder #:nodoc:
3
+ # Build random data usable for testing.
4
+ class RandomDataBuilder
5
+ # Hash of generators where the key is the class and the value is an
6
+ # implementation of AbstractGenerator
7
+ attr_reader :generators
8
+
9
+ # Hash of names mapped to generators where the name is the column name
10
+ attr_reader :column_generators
11
+
12
+ # Initialize the random data builder
13
+ def initialize
14
+ @generators = {
15
+ Fixnum => FixnumGenerator.new,
16
+ Float => FloatGenerator.new,
17
+ Date => DateGenerator.new,
18
+ Time => TimeGenerator.new,
19
+ String => StringGenerator.new,
20
+ Object => BooleanGenerator.new,
21
+ }
22
+ @column_generators = {}
23
+ end
24
+
25
+ # Build the data for the specified class. Name may be a Class (which must
26
+ # descend from ActiveWarehouse::Dimension
27
+ # or ActiveWarehouse::Fact), a String or a Symbol. String or Symbol will
28
+ # be converted to a class name and then
29
+ # passed back to this method.
30
+ def build(name, options={})
31
+ case name
32
+ when Class
33
+ if name.respond_to?(:base_class)
34
+ return build_dimension(name, options) if name.base_class == ActiveWarehouse::Dimension
35
+ return build_fact(name, options) if name.base_class == ActiveWarehouse::Fact
36
+ end
37
+ raise "#{name} is a class but does not appear to descend from Fact or Dimension"
38
+ when String
39
+ begin
40
+ build(name.classify.constantize, options)
41
+ rescue NameError
42
+ raise "Cannot find a class named #{name.classify}"
43
+ end
44
+ when Symbol
45
+ build(name.to_s, options)
46
+ else
47
+ raise "Unable to determine what to build"
48
+ end
49
+ end
50
+
51
+ # Build test dimension data for the specified dimension name.
52
+ #
53
+ # Options:
54
+ #
55
+ # * <tt>:rows</tt>: The number of rows to create (defaults to 100)
56
+ # * <tt>:generators</tt>: A map of generators where each key is Fixnum,
57
+ # Float, Date, Time, String, or Object and the
58
+ # value is extends from AbstractGenerator.
59
+ def build_dimension(name, options={})
60
+ options[:rows] ||= 100
61
+ options[:generators] ||= {}
62
+ rows = []
63
+
64
+ dimension_class = Dimension.to_dimension(name)
65
+ options[:rows].times do
66
+ row = {}
67
+ dimension_class.content_columns.each do |column|
68
+ generator = (options[:generators][column.klass] ||
69
+ @column_generators[column.name] ||
70
+ @generators[column.klass])
71
+ if generator.nil?
72
+ raise ArgumentError, "No generator found, unknown column type?: #{column.klass}"
73
+ end
74
+ row[column.name] = generator.generate(column, options)
75
+ end
76
+ rows << row
77
+ end
78
+
79
+ rows
80
+ end
81
+
82
+ # Build test fact data for the specified fact name
83
+ #
84
+ # Options:
85
+ # * <tt>:rows</tt>: The number of rows to create (defaults to 100)
86
+ # * <tt>:generators</tt>: A Hash of generators where each key is Fixnum,
87
+ # Float, Date, Time, String, or Object and the
88
+ # value is extends from AbstractGenerator.
89
+ # * <tt>:fk_limit</tt>: A Hash of foreign key limits, where each key is
90
+ # the name of column and the value is
91
+ # a number. For example options[:fk_limit][:date_id] = 1000 would limit
92
+ # the foreign key values to something between
93
+ # 1 and 1000, inclusive.
94
+ # * <tt>:dimensions</tt>: The number of available dimension FKs
95
+ def build_fact(name, options={})
96
+ options[:rows] ||= 100
97
+ options[:generators] ||= {}
98
+ options[:fk_limit] ||= {}
99
+ rows = []
100
+
101
+ fact_class = Fact.to_fact(name)
102
+ options[:rows].times do
103
+ row = {}
104
+ fact_class.content_columns.each do |column|
105
+ generator = (options[:generators][column.klass] || @generators[column.klass])
106
+ row[column.name] = generator.generate(column, options)
107
+ end
108
+ fact_class.dimension_relationships.each do |name, reflection|
109
+ # it would be better to get a count of rows from the dimension tables
110
+ fk_limit = (options[:fk_limit][reflection.primary_key_name] ||
111
+ options[:dimensions] || 100) - 1
112
+ row[reflection.primary_key_name] = rand(fk_limit) + 1
113
+ end
114
+ rows << row
115
+ end
116
+
117
+ rows
118
+ end
119
+ end
120
+
121
+ # Implement this class to provide an generator implementation for a specific class.
122
+ class AbstractGenerator
123
+ # Generate the next value. The column parameter must be an
124
+ # ActiveRecord::Adapter::Column instance.
125
+ # The options hash is implementation dependent.
126
+ def generate(column, options={})
127
+ raise "generate method must be implemented by a subclass"
128
+ end
129
+ end
130
+
131
+ # Basic Date generator
132
+ class DateGenerator < AbstractGenerator
133
+ # Generate a random date value
134
+ #
135
+ # Options:
136
+ # * <tt>:start_date</tt>: The start date as a Date or Time object
137
+ # (default 1 year ago)
138
+ # * <tt>:end_date</tt>: The end date as a Date or Time object (default now)
139
+ def generate(column, options={})
140
+ end_date = (options[:end_date] || Time.now).to_date
141
+ start_date = (options[:start_date] || 1.year.ago).to_date
142
+ number_of_days = end_date - start_date
143
+ start_date + rand(number_of_days)
144
+ end
145
+ end
146
+
147
+ # Basic Time generator
148
+ #
149
+ # Options:
150
+ # * <tt>:start_date</tt>: The start date as a Date or Time object
151
+ # (default 1 year ago)
152
+ # * <tt>:end_date</tt>: The end date as a Date or Time object (default now)
153
+ class TimeGenerator < DateGenerator #:nodoc:
154
+ # Generate a random Time value
155
+ def generate(column, options={})
156
+ super(column, options).to_time
157
+ end
158
+ end
159
+
160
+ # Basic Fixnum generator
161
+ class FixnumGenerator
162
+ # Generate an integer from 0 to options[:max] inclusive
163
+ #
164
+ # Options:
165
+ # * <tt>:max</tt>: The maximum allowed value (default 1000)
166
+ # * <tt>:min</tt>: The minimum allowed value (default 0)
167
+ def generate(column, options={})
168
+ options[:max] ||= 1000
169
+ options[:min] ||= 0
170
+ rand(options[:max] + (-options[:min])) - options[:min]
171
+ end
172
+ end
173
+
174
+ # Basic Float generator
175
+ class FloatGenerator
176
+ # Generate a float from 0 to options[:max] inclusive (default 1000)
177
+ #
178
+ # Options:
179
+ # * <tt>:max</tt>: The maximum allowed value (default 1000)
180
+ def generate(column, options={})
181
+ options[:max] ||= 1000
182
+ rand * options[:max].to_f
183
+ end
184
+ end
185
+
186
+ # Basic BigDecimal generator
187
+ class BigDecimalGenerator
188
+ # Generate a big decimal from 0 to options[:max] inclusive (default 1000)
189
+ #
190
+ # Options:
191
+ # * <tt>:max</tt>: The maximum allowed value (default 1000)
192
+ def generate(column, options={})
193
+ options[:max] ||= 1000
194
+ BigDecimal.new((rand * options[:max].to_f).to_s) # TODO: need BigDecimal type?
195
+ end
196
+ end
197
+
198
+ # A basic String generator
199
+ class StringGenerator
200
+ # Initialize the StringGenerator.
201
+ #
202
+ # Options:
203
+ # * <tt>:values</tt>: List of possible values
204
+ # * <tt>:chars</tt>: List of chars to use to generate random values
205
+ def initialize(options={})
206
+ @options = options
207
+ end
208
+ # Generate a random string
209
+ #
210
+ # Options:
211
+ # * <tt>:values</tt>: An array of values to use. If not specified then
212
+ # random char values will be used.
213
+ # * <tt>:chars</tt>: An array of characters to use to generate random
214
+ # values (default [a..zA..Z])
215
+ def generate(column, options={})
216
+ options[:values] ||= @options[:values]
217
+ options[:chars] ||= @options[:chars]
218
+ if options[:values]
219
+ options[:values][rand(options[:values].length)]
220
+ else
221
+ s = ''
222
+ chars = (options[:chars] || ('a'..'z').to_a + ('A'..'Z').to_a)
223
+ 0.upto(column.limit - 1) do |n|
224
+ s << chars[rand(chars.length)]
225
+ end
226
+ s
227
+ end
228
+ end
229
+ end
230
+
231
+ # A basic Boolean generator
232
+ class BooleanGenerator
233
+ # Generate a random boolean
234
+ def generate(column, options={})
235
+ rand(1) == 1
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,54 @@
1
+ Dir[File.dirname(__FILE__) + "/generator/*.rb"].each { |file| require(file) }
2
+
3
+ module ActiveWarehouse #:nodoc:
4
+ module Builder #:nodoc:
5
+ # Unlike the RandomDataBuilder, which puts truly random data in the warehouse, this
6
+ # generator uses collections of possible values to construct semi-understandable data
7
+ class TestDataBuilder
8
+ def initialize
9
+
10
+ end
11
+
12
+ # Usage:
13
+ #
14
+ # fields = [:id,:product_name,:product_description,:suggested_retail_price]
15
+ # field_definitions = {
16
+ # :id => :sequence, # symbol or string
17
+ # :product_name => [['Foo','Bar']['Baz','Bing']], # array
18
+ # :product_description => IpsumLorumGenerator # class
19
+ # :suggested_retail_price => RandomNumberGenerator.new(0.00, 100.00) # generator instance
20
+ # }
21
+ def build(fields, field_definitions, options={})
22
+ options[:number] ||= 100
23
+ rows = []
24
+ generators = {}
25
+ # set up all of the generators first
26
+ field_definitions.each do |name, fd|
27
+ case fd
28
+ when Class
29
+ generators[name] = fd.new
30
+ when String, Symbol
31
+ generators[name] = "#{fd}Generator".classify.constantize.new
32
+ when Array
33
+ generators[name] = NameGenerator.new(fd)
34
+ when Generator
35
+ generators[name] = fd
36
+ else
37
+ raise "Invalid generator specified: #{fd}"
38
+ end
39
+ end
40
+
41
+ # generate all of the rows
42
+ 0.upto(options[:number]) do
43
+ row = {}
44
+ fields.each do |field|
45
+ row[field] = generators[field].next(options)
46
+ end
47
+ rows << row
48
+ end
49
+
50
+ rows
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ module ActiveWarehouse #:nodoc:
2
+ # A field that uses a Proc to calculate the value
3
+ class CalculatedField < Field
4
+ attr_reader :block
5
+ # Initialize the calculated field
6
+ #
7
+ # +fact_class+ is the fact class that the field is calculated in
8
+ # +name+ is the name of the calculated field
9
+ # +type+ is the type of the calculated field (defaults to :integer)
10
+ # +field_options+ is a Hash of options for the field
11
+ #
12
+ # This method accepts a block which should take a single argument that is the record
13
+ # itself.
14
+ def initialize(fact_class, name, type = :integer, field_options = {}, &block)
15
+ unless block_given?
16
+ raise ArgumentError, "A block is required for the calculated field #{name} in #{fact_class}"
17
+ end
18
+ super(fact_class, name.to_s, type, field_options)
19
+ @block = block
20
+ end
21
+
22
+ # Calculate the field value using the Hash of type-casted values
23
+ def calculate(values)
24
+ @block.call(values)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # Provides 1.1.6 compatibility
2
+ module ActiveRecord #:nodoc:
3
+ module Calculations #:nodoc:
4
+ module ClassMethods #:nodoc:
5
+ protected
6
+ def construct_count_options_from_legacy_args(*args)
7
+ options = {}
8
+ column_name = :all
9
+
10
+ # We need to handle
11
+ # count()
12
+ # count(options={})
13
+ # count(column_name=:all, options={})
14
+ # count(conditions=nil, joins=nil) # deprecated
15
+ if args.size > 2
16
+ raise ArgumentError, "Unexpected parameters passed to count(options={}): #{args.inspect}"
17
+ elsif args.size > 0
18
+ if args[0].is_a?(Hash)
19
+ options = args[0]
20
+ elsif args[1].is_a?(Hash)
21
+ column_name, options = args
22
+ else
23
+ # Deprecated count(conditions, joins=nil)
24
+ ActiveSupport::Deprecation.warn(
25
+ "You called count(#{args[0].inspect}, #{args[1].inspect}), which is a deprecated API call. " +
26
+ "Instead you should use count(column_name, options). Passing the conditions and joins as " +
27
+ "string parameters will be removed in Rails 2.0.", caller(2)
28
+ )
29
+ options.merge!(:conditions => args[0])
30
+ options.merge!(:joins => args[1]) if args[1]
31
+ end
32
+ end
33
+
34
+ [column_name, options]
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class Module #:nodoc:
41
+ def alias_method_chain(target, feature)
42
+ # Strip out punctuation on predicates or bang methods since
43
+ # e.g. target?_without_feature is not a valid method name.
44
+ aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
45
+ yield(aliased_target, punctuation) if block_given?
46
+ alias_method "#{aliased_target}_without_#{feature}#{punctuation}", target
47
+ alias_method target, "#{aliased_target}_with_#{feature}#{punctuation}"
48
+ end
49
+ end
@@ -0,0 +1 @@
1
+ require 'active_warehouse/core_ext/time'
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + '/time/calculations'
2
+
3
+ class Time#:nodoc:
4
+ include ActiveWarehouse::CoreExtensions::Time::Calculations
5
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveWarehouse #:nodoc:
2
+ module CoreExtensions #:nodoc:
3
+ module Time #:nodoc:
4
+ # Enables the use of time calculations within Time itself
5
+ module Calculations
6
+ def week
7
+ cyw = ((yday - 1) / 7) + 1
8
+ cyw = 52 if cyw == 53
9
+ cyw
10
+ end
11
+ def quarter
12
+ ((month - 1) / 3) + 1
13
+ end
14
+ def fiscal_year_week(offset_month=10)
15
+ fyw = ((fiscal_year_yday(offset_month) - 1) / 7) + 1
16
+ fyw = 52 if fyw == 53
17
+ fyw
18
+ end
19
+ def fiscal_year_month(offset_month=10)
20
+ shifted_month = month - (offset_month - 1)
21
+ shifted_month += 12 if shifted_month < 0
22
+ shifted_month
23
+ end
24
+ def fiscal_year_quarter(offset_month=10)
25
+ ((fiscal_year_month(offset_month) - 1) / 3) + 1
26
+ end
27
+ def fiscal_year(offset_month=10)
28
+ month >= offset_month ? year + 1 : year
29
+ end
30
+ def fiscal_year_yday(offset_month=10)
31
+ offset_days = 0
32
+ 1.upto(offset_month - 1) { |m| offset_days += ::Time.days_in_month(m, year) }
33
+ shifted_year_day = yday - offset_days
34
+ shifted_year_day += 365 if shifted_year_day <= 0
35
+ shifted_year_day
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end