fin_it 0.1.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 +7 -0
- data/ARCHITECTURE.md +24 -0
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +20 -0
- data/LICENSE +21 -0
- data/QUICKSTART.md +56 -0
- data/README.md +74 -0
- data/Rakefile +23 -0
- data/SECURITY.md +14 -0
- data/assets/fin_it_logo.png +0 -0
- data/lib/fin_it/account.rb +120 -0
- data/lib/fin_it/calculator/currency_conversion.rb +27 -0
- data/lib/fin_it/calculator/date_helpers.rb +53 -0
- data/lib/fin_it/calculator/variable_hashing.rb +120 -0
- data/lib/fin_it/calculator.rb +480 -0
- data/lib/fin_it/categories/category.rb +137 -0
- data/lib/fin_it/complex_model.rb +169 -0
- data/lib/fin_it/dsl/account_builder.rb +35 -0
- data/lib/fin_it/dsl/calculated_builder.rb +87 -0
- data/lib/fin_it/dsl/config_builder.rb +58 -0
- data/lib/fin_it/dsl/model_builder.rb +938 -0
- data/lib/fin_it/dsl/model_template_builder.rb +29 -0
- data/lib/fin_it/dsl/plan_builder.rb +52 -0
- data/lib/fin_it/dsl/project_inheritance_resolver.rb +46 -0
- data/lib/fin_it/dsl/variable_builder.rb +41 -0
- data/lib/fin_it/dsl.rb +13 -0
- data/lib/fin_it/engine.rb +15 -0
- data/lib/fin_it/financial_model/account_balances.rb +99 -0
- data/lib/fin_it/financial_model/account_hierarchy.rb +158 -0
- data/lib/fin_it/financial_model/category_values.rb +179 -0
- data/lib/fin_it/financial_model/currency_helpers.rb +14 -0
- data/lib/fin_it/financial_model/date_helpers.rb +58 -0
- data/lib/fin_it/financial_model/debugging.rb +353 -0
- data/lib/fin_it/financial_model/period_flows.rb +121 -0
- data/lib/fin_it/financial_model/validation.rb +85 -0
- data/lib/fin_it/financial_model/variable_matching.rb +49 -0
- data/lib/fin_it/financial_model.rb +395 -0
- data/lib/fin_it/model_template.rb +121 -0
- data/lib/fin_it/outputs/base_output.rb +51 -0
- data/lib/fin_it/outputs/console_output.rb +1528 -0
- data/lib/fin_it/outputs/monthly_console_output.rb +145 -0
- data/lib/fin_it/outputs/zaxcel_output.rb +1264 -0
- data/lib/fin_it/payment_schedule.rb +112 -0
- data/lib/fin_it/plan.rb +159 -0
- data/lib/fin_it/reports/balance_sheet.rb +638 -0
- data/lib/fin_it/reports/base_report.rb +239 -0
- data/lib/fin_it/reports/cash_flow_statement.rb +480 -0
- data/lib/fin_it/reports/custom_sheet.rb +436 -0
- data/lib/fin_it/reports/income_statement.rb +793 -0
- data/lib/fin_it/reports/period_comparison.rb +309 -0
- data/lib/fin_it/reports/scenario_comparison.rb +296 -0
- data/lib/fin_it/temporal_value.rb +349 -0
- data/lib/fin_it/transaction_generator/account_resolver.rb +118 -0
- data/lib/fin_it/transaction_generator/cache_management.rb +39 -0
- data/lib/fin_it/transaction_generator/date_generation.rb +57 -0
- data/lib/fin_it/transaction_generator.rb +357 -0
- data/lib/fin_it/version.rb +6 -0
- data/lib/fin_it.rb +27 -0
- data/test/fin_it/calculator_test.rb +109 -0
- data/test/fin_it/complex_model_test.rb +198 -0
- data/test/fin_it/debugging_test.rb +112 -0
- data/test/fin_it/driver_variables_test.rb +109 -0
- data/test/fin_it/dsl_test.rb +581 -0
- data/test/fin_it/financial_model_test.rb +196 -0
- data/test/fin_it/frequency_test.rb +51 -0
- data/test/fin_it/outputs/console_output_test.rb +249 -0
- data/test/fin_it/plan_test.rb +281 -0
- data/test/fin_it/reports/account_balance_test.rb +232 -0
- data/test/fin_it/reports/balance_sheet_test.rb +355 -0
- data/test/fin_it/reports/cash_flow_statement_test.rb +234 -0
- data/test/fin_it/reports/custom_sheet_test.rb +246 -0
- data/test/fin_it/reports/income_statement_test.rb +431 -0
- data/test/fin_it/reports/period_comparison_test.rb +226 -0
- data/test/fin_it/reports/restaurant_model_test.rb +225 -0
- data/test/fin_it/reports/scenario_comparison_test.rb +414 -0
- data/test/scripts/generate_demo_reports.rb +47 -0
- data/test/scripts/startup_saas_demo.rb +62 -0
- data/test/test_helper.rb +25 -0
- data/test/verify_accounting_equation.rb +91 -0
- metadata +264 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FinIt
|
|
4
|
+
module Reports
|
|
5
|
+
# A customizable report for creating intro/summary sheets
|
|
6
|
+
# Supports both static content (headers, paragraphs, tables) and
|
|
7
|
+
# dynamic content (model metrics, account balances, scenario comparisons)
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# # Static content only
|
|
11
|
+
# intro = FinIt::Reports::CustomSheet.build(title: "Overview") do
|
|
12
|
+
# header "Financial Model", level: 1
|
|
13
|
+
# paragraph "This report covers fiscal year 2025."
|
|
14
|
+
# bullet_list ["Scenario A", "Scenario B"]
|
|
15
|
+
# table(headers: ["Metric", "Value"], rows: [["Revenue", "$100k"]])
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # With dynamic model data
|
|
19
|
+
# intro = FinIt::Reports::CustomSheet.build(title: "Summary", model: model) do
|
|
20
|
+
# header "Key Metrics", level: 1
|
|
21
|
+
# model_metric :net_income, label: "Net Income"
|
|
22
|
+
# account_balance :checking, label: "Cash Position"
|
|
23
|
+
# metrics_table(metrics: [:net_income, :total_expenses])
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class CustomSheet
|
|
27
|
+
attr_reader :title, :sections, :model, :output_currency
|
|
28
|
+
|
|
29
|
+
# Alias for compatibility with BaseOutput
|
|
30
|
+
alias_method :currency, :output_currency
|
|
31
|
+
|
|
32
|
+
def initialize(title:, sections: [], model: nil, output_currency: 'USD')
|
|
33
|
+
@title = title
|
|
34
|
+
@sections = sections
|
|
35
|
+
@model = model
|
|
36
|
+
@output_currency = output_currency
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# DSL builder for creating custom sheets
|
|
40
|
+
def self.build(title:, model: nil, output_currency: 'USD', &block)
|
|
41
|
+
sheet = new(title: title, model: model, output_currency: output_currency)
|
|
42
|
+
sheet.instance_eval(&block) if block_given?
|
|
43
|
+
sheet
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# --- Static Content DSL ---
|
|
47
|
+
|
|
48
|
+
def header(text, level: 1)
|
|
49
|
+
@sections << { type: :header, text: text, level: level }
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def paragraph(text)
|
|
54
|
+
@sections << { type: :paragraph, text: text }
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def spacer(rows: 1)
|
|
59
|
+
@sections << { type: :spacer, rows: rows }
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def key_value(label, value)
|
|
64
|
+
@sections << { type: :key_value, label: label, value: value }
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def bullet_list(items)
|
|
69
|
+
@sections << { type: :bullet_list, items: items }
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def numbered_list(items)
|
|
74
|
+
@sections << { type: :numbered_list, items: items }
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def table(headers:, rows:, title: nil)
|
|
79
|
+
@sections << { type: :table, headers: headers, rows: rows, title: title }
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def horizontal_rule
|
|
84
|
+
@sections << { type: :hr }
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def image(path, alt_text: nil, width: nil, height: nil)
|
|
89
|
+
@sections << { type: :image, path: path, alt_text: alt_text, width: width, height: height }
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def note(text, style: :info)
|
|
94
|
+
# style can be :info, :warning, :success, :error
|
|
95
|
+
@sections << { type: :note, text: text, style: style }
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# --- Dynamic Content DSL (requires model) ---
|
|
100
|
+
|
|
101
|
+
def model_metric(variable, label: nil, start_date: nil, end_date: nil, format: :currency)
|
|
102
|
+
require_model!("model_metric")
|
|
103
|
+
@sections << {
|
|
104
|
+
type: :model_metric,
|
|
105
|
+
variable: variable,
|
|
106
|
+
label: label || humanize_name(variable),
|
|
107
|
+
start_date: start_date,
|
|
108
|
+
end_date: end_date,
|
|
109
|
+
format: format
|
|
110
|
+
}
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def account_balance(account_name, label: nil, as_of: nil, format: :currency)
|
|
115
|
+
require_model!("account_balance")
|
|
116
|
+
@sections << {
|
|
117
|
+
type: :account_balance,
|
|
118
|
+
account: account_name,
|
|
119
|
+
label: label || humanize_name(account_name),
|
|
120
|
+
as_of: as_of,
|
|
121
|
+
format: format
|
|
122
|
+
}
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def metrics_table(metrics:, title: nil, start_date: nil, end_date: nil)
|
|
127
|
+
require_model!("metrics_table")
|
|
128
|
+
@sections << {
|
|
129
|
+
type: :metrics_table,
|
|
130
|
+
title: title,
|
|
131
|
+
metrics: metrics,
|
|
132
|
+
start_date: start_date,
|
|
133
|
+
end_date: end_date
|
|
134
|
+
}
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def scenario_comparison_summary(scenarios:, metrics:, start_date: nil, end_date: nil, title: nil)
|
|
139
|
+
require_model!("scenario_comparison_summary")
|
|
140
|
+
@sections << {
|
|
141
|
+
type: :scenario_comparison_summary,
|
|
142
|
+
scenarios: scenarios,
|
|
143
|
+
metrics: metrics,
|
|
144
|
+
start_date: start_date,
|
|
145
|
+
end_date: end_date,
|
|
146
|
+
title: title
|
|
147
|
+
}
|
|
148
|
+
self
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def period_comparison_summary(periods:, metrics:, title: nil)
|
|
152
|
+
require_model!("period_comparison_summary")
|
|
153
|
+
@sections << {
|
|
154
|
+
type: :period_comparison_summary,
|
|
155
|
+
periods: periods,
|
|
156
|
+
metrics: metrics,
|
|
157
|
+
title: title
|
|
158
|
+
}
|
|
159
|
+
self
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# --- Report Generation ---
|
|
163
|
+
|
|
164
|
+
def generate
|
|
165
|
+
# Resolve dynamic content if model is present
|
|
166
|
+
resolved_sections = resolve_dynamic_sections
|
|
167
|
+
|
|
168
|
+
{
|
|
169
|
+
report_type: "CustomSheet",
|
|
170
|
+
title: @title,
|
|
171
|
+
sections: resolved_sections,
|
|
172
|
+
currency: @output_currency,
|
|
173
|
+
generated_at: Time.now
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def output(output_class, **options)
|
|
178
|
+
output_class.new(self, options).generate
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def require_model!(method_name)
|
|
184
|
+
raise ArgumentError, "Model required for #{method_name}. Pass model: to CustomSheet.build" unless @model
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def humanize_name(name)
|
|
188
|
+
name.to_s.split('_').map(&:capitalize).join(' ')
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def resolve_dynamic_sections
|
|
192
|
+
@sections.map do |section|
|
|
193
|
+
case section[:type]
|
|
194
|
+
when :model_metric
|
|
195
|
+
resolve_model_metric(section)
|
|
196
|
+
when :account_balance
|
|
197
|
+
resolve_account_balance(section)
|
|
198
|
+
when :metrics_table
|
|
199
|
+
resolve_metrics_table(section)
|
|
200
|
+
when :scenario_comparison_summary
|
|
201
|
+
resolve_scenario_comparison_summary(section)
|
|
202
|
+
when :period_comparison_summary
|
|
203
|
+
resolve_period_comparison_summary(section)
|
|
204
|
+
else
|
|
205
|
+
section
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def resolve_model_metric(section)
|
|
211
|
+
start_date = parse_date(section[:start_date]) if section[:start_date]
|
|
212
|
+
end_date = parse_date(section[:end_date]) if section[:end_date]
|
|
213
|
+
|
|
214
|
+
# Use end_date for calculation, or model's default end
|
|
215
|
+
calc_date = end_date || Date.today
|
|
216
|
+
|
|
217
|
+
# Ensure transactions are generated
|
|
218
|
+
@model.generate_transactions(calc_date)
|
|
219
|
+
|
|
220
|
+
value = calculate_metric(@model, section[:variable], start_date, end_date)
|
|
221
|
+
formatted_value = format_value(value, section[:format])
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
type: :key_value,
|
|
225
|
+
label: section[:label],
|
|
226
|
+
value: formatted_value
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def resolve_account_balance(section)
|
|
231
|
+
as_of = section[:as_of] ? parse_date(section[:as_of]) : Date.today
|
|
232
|
+
|
|
233
|
+
@model.generate_transactions(as_of)
|
|
234
|
+
|
|
235
|
+
value = @model.account_balance(section[:account], as_of_date: as_of)
|
|
236
|
+
formatted_value = format_value(value, section[:format])
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
type: :key_value,
|
|
240
|
+
label: section[:label],
|
|
241
|
+
value: formatted_value
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def resolve_metrics_table(section)
|
|
246
|
+
start_date = parse_date(section[:start_date]) if section[:start_date]
|
|
247
|
+
end_date = parse_date(section[:end_date]) if section[:end_date]
|
|
248
|
+
calc_date = end_date || Date.today
|
|
249
|
+
|
|
250
|
+
@model.generate_transactions(calc_date)
|
|
251
|
+
|
|
252
|
+
rows = section[:metrics].map do |metric|
|
|
253
|
+
value = calculate_metric(@model, metric, start_date, end_date)
|
|
254
|
+
[humanize_name(metric), format_value(value, :currency)]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
type: :table,
|
|
259
|
+
title: section[:title],
|
|
260
|
+
headers: ["Metric", "Value"],
|
|
261
|
+
rows: rows
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def resolve_scenario_comparison_summary(section)
|
|
266
|
+
start_date = parse_date(section[:start_date]) if section[:start_date]
|
|
267
|
+
end_date = parse_date(section[:end_date]) if section[:end_date]
|
|
268
|
+
calc_date = end_date || Date.today
|
|
269
|
+
|
|
270
|
+
@model.generate_transactions(calc_date)
|
|
271
|
+
section[:scenarios].each_value { |m| m.generate_transactions(calc_date) }
|
|
272
|
+
|
|
273
|
+
headers = ["Metric", "Base"] + section[:scenarios].keys.map { |k| humanize_name(k) }
|
|
274
|
+
|
|
275
|
+
rows = section[:metrics].map do |metric|
|
|
276
|
+
base_value = calculate_metric(@model, metric, start_date, end_date)
|
|
277
|
+
scenario_values = section[:scenarios].values.map do |scenario_model|
|
|
278
|
+
calculate_metric(scenario_model, metric, start_date, end_date)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
[humanize_name(metric), format_value(base_value, :currency)] +
|
|
282
|
+
scenario_values.map { |v| format_value(v, :currency) }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
{
|
|
286
|
+
type: :table,
|
|
287
|
+
title: section[:title] || "Scenario Comparison",
|
|
288
|
+
headers: headers,
|
|
289
|
+
rows: rows
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def resolve_period_comparison_summary(section)
|
|
294
|
+
periods = section[:periods].map do |p|
|
|
295
|
+
{
|
|
296
|
+
name: p[:name],
|
|
297
|
+
start_date: parse_date(p[:start_date]),
|
|
298
|
+
end_date: parse_date(p[:end_date])
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Generate transactions for the latest period
|
|
303
|
+
last_date = periods.map { |p| p[:end_date] }.max
|
|
304
|
+
@model.generate_transactions(last_date)
|
|
305
|
+
|
|
306
|
+
headers = ["Metric"] + periods.map { |p| p[:name] }
|
|
307
|
+
|
|
308
|
+
rows = section[:metrics].map do |metric|
|
|
309
|
+
period_values = periods.map do |period|
|
|
310
|
+
calculate_metric(@model, metric, period[:start_date], period[:end_date])
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
[humanize_name(metric)] + period_values.map { |v| format_value(v, :currency) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
{
|
|
317
|
+
type: :table,
|
|
318
|
+
title: section[:title] || "Period Comparison",
|
|
319
|
+
headers: headers,
|
|
320
|
+
rows: rows
|
|
321
|
+
}
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def calculate_metric(model, metric, start_date, end_date)
|
|
325
|
+
case metric
|
|
326
|
+
when :net_income
|
|
327
|
+
model.period_net_income(
|
|
328
|
+
start_date || model.start_date,
|
|
329
|
+
end_date || Date.today,
|
|
330
|
+
output_currency: @output_currency
|
|
331
|
+
)
|
|
332
|
+
when :total_revenue, :total_income
|
|
333
|
+
calculate_total_income(model, start_date, end_date)
|
|
334
|
+
when :total_expenses
|
|
335
|
+
calculate_total_expenses(model, start_date, end_date)
|
|
336
|
+
when :ending_cash
|
|
337
|
+
calculate_ending_cash(model, end_date)
|
|
338
|
+
else
|
|
339
|
+
model.calculator.calculate(
|
|
340
|
+
metric,
|
|
341
|
+
date: end_date || Date.today,
|
|
342
|
+
output_currency: @output_currency
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def calculate_total_income(model, start_date, end_date)
|
|
348
|
+
income_categories = model.categories.select { |c| c.type == :income }
|
|
349
|
+
income_categories.sum do |category|
|
|
350
|
+
model.category_total_for_period(
|
|
351
|
+
category,
|
|
352
|
+
start_date || model.start_date,
|
|
353
|
+
end_date || Date.today,
|
|
354
|
+
period_type: :annual,
|
|
355
|
+
output_currency: @output_currency
|
|
356
|
+
).abs
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def calculate_total_expenses(model, start_date, end_date)
|
|
361
|
+
expense_categories = model.categories.select { |c| c.type == :expense }
|
|
362
|
+
expense_categories.sum do |category|
|
|
363
|
+
model.category_total_for_period(
|
|
364
|
+
category,
|
|
365
|
+
start_date || model.start_date,
|
|
366
|
+
end_date || Date.today,
|
|
367
|
+
period_type: :annual,
|
|
368
|
+
output_currency: @output_currency
|
|
369
|
+
).abs
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def calculate_ending_cash(model, end_date)
|
|
374
|
+
cash_accounts = model.accounts.values.select do |acc|
|
|
375
|
+
acc.type == :asset && is_cash_account?(acc)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
cash_accounts.sum do |account|
|
|
379
|
+
model.account_balance(account.name, as_of_date: end_date || Date.today)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def is_cash_account?(account)
|
|
384
|
+
name = account.name.to_s.downcase
|
|
385
|
+
cash_patterns = ['cash', 'checking', 'savings', 'bank', 'money_market']
|
|
386
|
+
cash_patterns.any? { |pattern| name.include?(pattern) }
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def format_value(value, format)
|
|
390
|
+
case format
|
|
391
|
+
when :currency
|
|
392
|
+
if value.is_a?(Money)
|
|
393
|
+
"$#{format_number(value.to_f)}"
|
|
394
|
+
else
|
|
395
|
+
"$#{format_number(value.to_f)}"
|
|
396
|
+
end
|
|
397
|
+
when :number
|
|
398
|
+
format_number(value.to_f)
|
|
399
|
+
when :percentage
|
|
400
|
+
"#{(value.to_f * 100).round(2)}%"
|
|
401
|
+
else
|
|
402
|
+
value.to_s
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def format_number(num)
|
|
407
|
+
# Format with thousands separator and 2 decimal places
|
|
408
|
+
parts = ('%.2f' % num).split('.')
|
|
409
|
+
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, '\\1,')
|
|
410
|
+
parts.join('.')
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def parse_date(date)
|
|
414
|
+
return date if date.is_a?(Date)
|
|
415
|
+
return nil if date.nil?
|
|
416
|
+
|
|
417
|
+
case date
|
|
418
|
+
when String
|
|
419
|
+
if date =~ /^\d{4}$/
|
|
420
|
+
Date.new(date.to_i, 1, 1)
|
|
421
|
+
elsif date =~ /^\d{4}-\d{2}$/
|
|
422
|
+
Date.parse("#{date}-01")
|
|
423
|
+
else
|
|
424
|
+
Date.parse(date)
|
|
425
|
+
end
|
|
426
|
+
when Integer
|
|
427
|
+
Date.new(date, 1, 1)
|
|
428
|
+
when Time
|
|
429
|
+
date.to_date
|
|
430
|
+
else
|
|
431
|
+
raise ArgumentError, "Cannot parse date: #{date}"
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|