finmodeling 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -0
- data/Gemfile +2 -0
- data/README.md +289 -269
- data/Rakefile +12 -0
- data/TODO.txt +113 -20
- data/examples/{dump_report.rb → dump_latest_10k.rb} +1 -1
- data/examples/list_disclosures.rb +50 -0
- data/examples/lists/nasdaq-mid-to-mega-tech-symbols.txt +0 -0
- data/examples/show_report.rb +112 -32
- data/examples/show_reports.rb +162 -33
- data/finmodeling.gemspec +4 -1
- data/lib/finmodeling/annual_report_filing.rb +97 -18
- data/lib/finmodeling/array_with_stats.rb +0 -0
- data/lib/finmodeling/assets_calculation.rb +12 -3
- data/lib/finmodeling/assets_item.rb +0 -0
- data/lib/finmodeling/assets_item_vectors.rb +0 -0
- data/lib/finmodeling/balance_sheet_analyses.rb +19 -4
- data/lib/finmodeling/balance_sheet_calculation.rb +52 -37
- data/lib/finmodeling/calculation_summary.rb +119 -14
- data/lib/finmodeling/can_cache_classifications.rb +0 -0
- data/lib/finmodeling/can_cache_summaries.rb +0 -0
- data/lib/finmodeling/can_choose_successive_periods.rb +15 -0
- data/lib/finmodeling/can_classify_rows.rb +0 -0
- data/lib/finmodeling/capm.rb +80 -0
- data/lib/finmodeling/cash_change_calculation.rb +3 -3
- data/lib/finmodeling/cash_change_item.rb +0 -0
- data/lib/finmodeling/cash_change_item_vectors.rb +0 -0
- data/lib/finmodeling/cash_change_summary_from_differences.rb +36 -0
- data/lib/finmodeling/cash_flow_statement_analyses.rb +36 -0
- data/lib/finmodeling/cash_flow_statement_calculation.rb +28 -52
- data/lib/finmodeling/classifiers.rb +2 -0
- data/lib/finmodeling/company.rb +0 -0
- data/lib/finmodeling/company_filing.rb +30 -7
- data/lib/finmodeling/company_filing_calculation.rb +16 -6
- data/lib/finmodeling/company_filings.rb +112 -46
- data/lib/finmodeling/comprehensive_income_calculation.rb +60 -0
- data/lib/finmodeling/comprehensive_income_statement_calculation.rb +74 -0
- data/lib/finmodeling/comprehensive_income_statement_item.rb +20 -0
- data/lib/finmodeling/comprehensive_income_statement_item_vectors.rb +235 -0
- data/lib/finmodeling/config.rb +0 -0
- data/lib/finmodeling/debt_cost_of_capital.rb +14 -0
- data/lib/finmodeling/equity_change_calculation.rb +43 -0
- data/lib/finmodeling/equity_change_item.rb +25 -0
- data/lib/finmodeling/equity_change_item_vectors.rb +156 -0
- data/lib/finmodeling/factory.rb +0 -0
- data/lib/finmodeling/fama_french_cost_of_equity.rb +119 -0
- data/lib/finmodeling/float_helpers.rb +14 -8
- data/lib/finmodeling/forecasted_reformulated_balance_sheet.rb +55 -0
- data/lib/finmodeling/forecasted_reformulated_income_statement.rb +110 -0
- data/lib/finmodeling/forecasts.rb +4 -4
- data/lib/finmodeling/has_string_classifer.rb +0 -0
- data/lib/finmodeling/income_statement_analyses.rb +23 -17
- data/lib/finmodeling/income_statement_calculation.rb +46 -43
- data/lib/finmodeling/income_statement_item.rb +1 -1
- data/lib/finmodeling/income_statement_item_vectors.rb +24 -13
- data/lib/finmodeling/invalid_filing_error.rb +4 -0
- data/lib/finmodeling/liabs_and_equity_calculation.rb +18 -8
- data/lib/finmodeling/liabs_and_equity_item.rb +1 -1
- data/lib/finmodeling/liabs_and_equity_item_vectors.rb +24 -24
- data/lib/finmodeling/linear_trend_forecasting_policy.rb +23 -0
- data/lib/finmodeling/net_income_calculation.rb +23 -10
- data/lib/finmodeling/net_income_summary_from_differences.rb +51 -0
- data/lib/finmodeling/paths.rb +0 -0
- data/lib/finmodeling/period_array.rb +8 -4
- data/lib/finmodeling/quarterly_report_filing.rb +9 -4
- data/lib/finmodeling/rate.rb +8 -0
- data/lib/finmodeling/ratio.rb +0 -0
- data/lib/finmodeling/reformulated_balance_sheet.rb +47 -88
- data/lib/finmodeling/reformulated_cash_flow_statement.rb +18 -41
- data/lib/finmodeling/reformulated_income_statement.rb +44 -206
- data/lib/finmodeling/reformulated_shareholder_equity_statement.rb +50 -0
- data/lib/finmodeling/reoi_valuation.rb +104 -0
- data/lib/finmodeling/shareholder_equity_statement_calculation.rb +34 -0
- data/lib/finmodeling/string_helpers.rb +18 -1
- data/lib/finmodeling/time_series_estimator.rb +25 -0
- data/lib/finmodeling/trailing_avg_forecasting_policy.rb +23 -0
- data/lib/finmodeling/version.rb +1 -1
- data/lib/finmodeling/weighted_avg_cost_of_capital.rb +35 -0
- data/lib/finmodeling/yahoo_finance_helpers.rb +20 -0
- data/lib/finmodeling.rb +33 -2
- data/spec/annual_report_filing_spec.rb +81 -45
- data/spec/assets_calculation_spec.rb +7 -4
- data/spec/assets_item_spec.rb +9 -14
- data/spec/balance_sheet_analyses_spec.rb +13 -13
- data/spec/balance_sheet_calculation_spec.rb +45 -51
- data/spec/calculation_summary_spec.rb +113 -21
- data/spec/can_classify_rows_spec.rb +0 -0
- data/spec/cash_change_calculation_spec.rb +1 -10
- data/spec/cash_change_item_spec.rb +10 -18
- data/spec/cash_flow_statement_calculation_spec.rb +10 -24
- data/spec/company_beta_spec.rb +53 -0
- data/spec/company_filing_calculation_spec.rb +39 -49
- data/spec/company_filing_spec.rb +0 -0
- data/spec/company_filings_spec.rb +75 -25
- data/spec/company_spec.rb +37 -47
- data/spec/comprehensive_income_statement_calculation_spec.rb +54 -0
- data/spec/comprehensive_income_statement_item_spec.rb +56 -0
- data/spec/debt_cost_of_capital_spec.rb +19 -0
- data/spec/equity_change_calculation_spec.rb +33 -0
- data/spec/equity_change_item_spec.rb +58 -0
- data/spec/factory_spec.rb +2 -2
- data/spec/forecasts_spec.rb +2 -2
- data/spec/income_statement_analyses_spec.rb +23 -21
- data/spec/income_statement_calculation_spec.rb +17 -49
- data/spec/income_statement_item_spec.rb +17 -29
- data/spec/liabs_and_equity_calculation_spec.rb +6 -3
- data/spec/liabs_and_equity_item_spec.rb +14 -22
- data/spec/linear_trend_forecasting_policy_spec.rb +37 -0
- data/spec/matchers/custom_matchers.rb +79 -0
- data/spec/mocks/calculation.rb +0 -0
- data/spec/mocks/income_statement_analyses.rb +0 -0
- data/spec/mocks/sec_query.rb +0 -0
- data/spec/net_income_calculation_spec.rb +16 -10
- data/spec/period_array.rb +0 -0
- data/spec/quarterly_report_filing_spec.rb +21 -38
- data/spec/rate_spec.rb +0 -0
- data/spec/ratio_spec.rb +0 -0
- data/spec/reformulated_balance_sheet_spec.rb +56 -33
- data/spec/reformulated_cash_flow_statement_spec.rb +18 -10
- data/spec/reformulated_income_statement_spec.rb +16 -15
- data/spec/reformulated_shareholder_equity_statement_spec.rb +43 -0
- data/spec/reoi_valuation_spec.rb +146 -0
- data/spec/shareholder_equity_statement_calculation_spec.rb +59 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/string_helpers_spec.rb +15 -13
- data/spec/time_series_estimator_spec.rb +61 -0
- data/spec/trailing_avg_forecasting_policy_spec.rb +37 -0
- data/spec/weighted_avg_cost_of_capital_spec.rb +32 -0
- data/tools/create_equity_change_training_vectors.rb +49 -0
- data/tools/time_specs.sh +7 -0
- metadata +182 -36
- data/lib/finmodeling/constant_forecasting_policy.rb +0 -23
- data/lib/finmodeling/generic_forecasting_policy.rb +0 -19
- data/spec/constant_forecasting_policy_spec.rb +0 -37
- data/spec/generic_forecasting_policy_spec.rb +0 -33
@@ -2,41 +2,6 @@ module FinModeling
|
|
2
2
|
class ReformulatedCashFlowStatement
|
3
3
|
attr_accessor :period
|
4
4
|
|
5
|
-
class FakeCashChangeSummary
|
6
|
-
def initialize(re_cfs1, re_cfs2)
|
7
|
-
@re_cfs1 = re_cfs1
|
8
|
-
@re_cfs2 = re_cfs2
|
9
|
-
end
|
10
|
-
def filter_by_type(key)
|
11
|
-
case key
|
12
|
-
when :c
|
13
|
-
@cs = FinModeling::CalculationSummary.new
|
14
|
-
@cs.title = "Cash from Operations"
|
15
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.cash_from_operations.total] ),
|
16
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.cash_from_operations.total] ) ]
|
17
|
-
return @cs
|
18
|
-
when :i
|
19
|
-
@cs = FinModeling::CalculationSummary.new
|
20
|
-
@cs.title = "Cash Investments in Operations"
|
21
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.cash_investments_in_operations.total] ),
|
22
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.cash_investments_in_operations.total] ) ]
|
23
|
-
return @cs
|
24
|
-
when :d
|
25
|
-
@cs = FinModeling::CalculationSummary.new
|
26
|
-
@cs.title = "Payments to Debtholders"
|
27
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.payments_to_debtholders.total] ),
|
28
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.payments_to_debtholders.total] ) ]
|
29
|
-
return @cs
|
30
|
-
when :f
|
31
|
-
@cs = FinModeling::CalculationSummary.new
|
32
|
-
@cs.title = "Payments to Stockholders"
|
33
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @re_cfs1.payments_to_stockholders.total] ),
|
34
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@re_cfs2.payments_to_stockholders.total] ) ]
|
35
|
-
return @cs
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
5
|
def initialize(period, cash_change_summary)
|
41
6
|
@period = period
|
42
7
|
|
@@ -50,7 +15,7 @@ module FinModeling
|
|
50
15
|
@d.title = "Payments to debtholders"
|
51
16
|
@f.title = "Payments to stockholders"
|
52
17
|
|
53
|
-
if cash_change_summary.
|
18
|
+
if !(cash_change_summary.is_a? CashChangeSummaryFromDifferences)
|
54
19
|
@d.rows << CalculationRow.new(:key => "Investment in Cash and Equivalents",
|
55
20
|
:type => :d,
|
56
21
|
:vals => [-cash_change_summary.total])
|
@@ -58,7 +23,7 @@ module FinModeling
|
|
58
23
|
end
|
59
24
|
|
60
25
|
def -(re_cfs2)
|
61
|
-
summary =
|
26
|
+
summary = CashChangeSummaryFromDifferences.new(self, re_cfs2)
|
62
27
|
return ReformulatedCashFlowStatement.new(@period, summary)
|
63
28
|
end
|
64
29
|
|
@@ -105,10 +70,10 @@ module FinModeling
|
|
105
70
|
analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."])
|
106
71
|
|
107
72
|
analysis.rows = []
|
108
|
-
analysis.rows << CalculationRow.new(:key => "C
|
109
|
-
analysis.rows << CalculationRow.new(:key => "I
|
110
|
-
analysis.rows << CalculationRow.new(:key => "d
|
111
|
-
analysis.rows << CalculationRow.new(:key => "F
|
73
|
+
analysis.rows << CalculationRow.new(:key => "C ($MM)", :vals => [nil])
|
74
|
+
analysis.rows << CalculationRow.new(:key => "I ($MM)", :vals => [nil])
|
75
|
+
analysis.rows << CalculationRow.new(:key => "d ($MM)", :vals => [nil])
|
76
|
+
analysis.rows << CalculationRow.new(:key => "F ($MM)", :vals => [nil])
|
112
77
|
analysis.rows << CalculationRow.new(:key => "FCF ($MM)", :vals => [nil])
|
113
78
|
analysis.rows << CalculationRow.new(:key => "NI / C", :vals => [nil])
|
114
79
|
|
@@ -136,5 +101,17 @@ module FinModeling
|
|
136
101
|
return analysis
|
137
102
|
end
|
138
103
|
|
104
|
+
ALLOWED_IMBALANCE = 1.0
|
105
|
+
def flows_are_balanced?
|
106
|
+
(free_cash_flow.total - financing_flows.total) < ALLOWED_IMBALANCE
|
107
|
+
end
|
108
|
+
|
109
|
+
def flows_are_plausible?
|
110
|
+
return [ payments_to_debtholders,
|
111
|
+
payments_to_stockholders,
|
112
|
+
cash_from_operations,
|
113
|
+
cash_investments_in_operations ].all?{ |x| x.total.abs > 1.0 }
|
114
|
+
end
|
115
|
+
|
139
116
|
end
|
140
117
|
end
|
@@ -1,86 +1,45 @@
|
|
1
1
|
module FinModeling
|
2
2
|
class ReformulatedIncomeStatement
|
3
3
|
attr_accessor :period
|
4
|
+
attr_accessor :operating_revenues, :cost_of_revenues, :operating_expenses
|
4
5
|
|
5
|
-
|
6
|
-
def initialize(ris1, ris2)
|
7
|
-
@ris1 = ris1
|
8
|
-
@ris2 = ris2
|
9
|
-
end
|
10
|
-
def filter_by_type(key)
|
11
|
-
case key
|
12
|
-
when :or
|
13
|
-
@cs = FinModeling::CalculationSummary.new
|
14
|
-
@cs.title = "Operating Revenues"
|
15
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_revenues.total] ),
|
16
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_revenues.total] ) ]
|
17
|
-
return @cs
|
18
|
-
when :cogs
|
19
|
-
@cs = FinModeling::CalculationSummary.new
|
20
|
-
@cs.title = "Cost of Revenues"
|
21
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.cost_of_revenues.total] ),
|
22
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.cost_of_revenues.total] ) ]
|
23
|
-
return @cs
|
24
|
-
when :oe
|
25
|
-
@cs = FinModeling::CalculationSummary.new
|
26
|
-
@cs.title = "Operating Expenses"
|
27
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_expenses.total] ),
|
28
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_expenses.total] ) ]
|
29
|
-
return @cs
|
30
|
-
when :oibt
|
31
|
-
@cs = FinModeling::CalculationSummary.new
|
32
|
-
@cs.title = "Operating Income from Sales, Before taxes"
|
33
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_income_after_tax.rows[1].vals.first] ),
|
34
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_income_after_tax.rows[1].vals.first] ) ]
|
35
|
-
return @cs
|
36
|
-
when :fibt
|
37
|
-
@cs = FinModeling::CalculationSummary.new
|
38
|
-
@cs.title = "Financing Income, Before Taxes"
|
39
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.net_financing_income.rows[0].vals.first] ),
|
40
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.net_financing_income.rows[0].vals.first] ) ]
|
41
|
-
return @cs
|
42
|
-
when :tax
|
43
|
-
@cs = FinModeling::CalculationSummary.new
|
44
|
-
@cs.title = "Taxes"
|
45
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.income_from_sales_after_tax.rows[1].vals.first] ),
|
46
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.income_from_sales_after_tax.rows[1].vals.first] ) ]
|
47
|
-
return @cs
|
48
|
-
when :ooiat
|
49
|
-
@cs = FinModeling::CalculationSummary.new
|
50
|
-
@cs.title = "Other Operating Income, After Taxes"
|
51
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.operating_income_after_tax.rows[3].vals.first] ),
|
52
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.operating_income_after_tax.rows[3].vals.first] ) ]
|
53
|
-
return @cs
|
54
|
-
when :fiat
|
55
|
-
@cs = FinModeling::CalculationSummary.new
|
56
|
-
@cs.title = "Financing Income, After Taxes"
|
57
|
-
@cs.rows = [ CalculationRow.new(:key => "First Row", :vals => [ @ris1.net_financing_income.rows[2].vals.first] ),
|
58
|
-
CalculationRow.new(:key => "Second Row", :vals => [-@ris2.net_financing_income.rows[2].vals.first] ) ]
|
59
|
-
return @cs
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def initialize(period, net_income_summary, tax_rate=0.35)
|
6
|
+
def initialize(period, net_income_summary, comprehensive_income_summary, tax_rate=0.35) # FIXME: clarify naming. This is effective tax rate? marginal? statutory?
|
65
7
|
@period = period
|
66
8
|
@tax_rate = tax_rate
|
67
|
-
|
68
|
-
@
|
69
|
-
@
|
70
|
-
@
|
71
|
-
@oibt
|
72
|
-
@fibt
|
73
|
-
@tax
|
74
|
-
@ooiat
|
75
|
-
@
|
9
|
+
|
10
|
+
@operating_revenues = net_income_summary.filter_by_type(:or )
|
11
|
+
@cost_of_revenues = net_income_summary.filter_by_type(:cogs)
|
12
|
+
@operating_expenses = net_income_summary.filter_by_type(:oe )
|
13
|
+
@oibt = net_income_summary.filter_by_type(:oibt )
|
14
|
+
@fibt = net_income_summary.filter_by_type(:fibt )
|
15
|
+
@tax = net_income_summary.filter_by_type(:tax )
|
16
|
+
@ooiat = net_income_summary.filter_by_type(:ooiat )
|
17
|
+
@ooiat_nci = net_income_summary.filter_by_type(:ooiat_nci)
|
18
|
+
@fiat = net_income_summary.filter_by_type(:fiat )
|
19
|
+
|
20
|
+
# If there is a CI statement:
|
21
|
+
# Strip O-NCI-OCI out of the NI calculation (***)
|
22
|
+
# Double-check that the NI portion of the CI calculation matches (NI minus O-NCI-OCI)
|
23
|
+
# Add OOCI + 1/2 of the UNKOCI in as an OOIAT
|
24
|
+
# Add FOCI + 1/2 of the UNKOCI in as a FIAT
|
25
|
+
if comprehensive_income_summary
|
26
|
+
@ooiat.rows += comprehensive_income_summary.filter_by_type(:ooci).rows
|
27
|
+
@ooiat.rows += comprehensive_income_summary.filter_by_type(:ooci_nci).rows
|
28
|
+
@fiat.rows += comprehensive_income_summary.filter_by_type(:foci).rows
|
29
|
+
comprehensive_income_summary.filter_by_type(:unkoci).rows.each do |row|
|
30
|
+
row.vals = row.vals.map{ |val| val / 2.0 }
|
31
|
+
@ooiat.rows << row
|
32
|
+
@fiat.rows << row
|
33
|
+
end
|
34
|
+
end
|
76
35
|
|
77
36
|
@fibt_tax_effect = (@fibt.total * @tax_rate).round.to_f
|
78
37
|
@nfi = @fibt.total + -@fibt_tax_effect + @fiat.total
|
79
38
|
|
80
39
|
@oibt_tax_effect = (@oibt.total * @tax_rate).round.to_f
|
81
40
|
|
82
|
-
@gm = @
|
83
|
-
@oisbt = @gm + @
|
41
|
+
@gm = @operating_revenues.total + @cost_of_revenues.total
|
42
|
+
@oisbt = @gm + @operating_expenses.total
|
84
43
|
|
85
44
|
@oisat = @oisbt + @tax.total + @fibt_tax_effect + @oibt_tax_effect
|
86
45
|
|
@@ -90,35 +49,23 @@ module FinModeling
|
|
90
49
|
end
|
91
50
|
|
92
51
|
def -(ris2)
|
93
|
-
net_income_summary =
|
94
|
-
return ReformulatedIncomeStatement.new(@period, net_income_summary, @tax_rate)
|
95
|
-
end
|
96
|
-
|
97
|
-
def operating_revenues
|
98
|
-
@orev
|
99
|
-
end
|
100
|
-
|
101
|
-
def cost_of_revenues
|
102
|
-
@cogs
|
52
|
+
net_income_summary = NetIncomeSummaryFromDifferences.new(self, ris2)
|
53
|
+
return ReformulatedIncomeStatement.new(@period, net_income_summary, nil, @tax_rate)
|
103
54
|
end
|
104
55
|
|
105
56
|
def gross_revenue
|
106
57
|
cs = FinModeling::CalculationSummary.new
|
107
58
|
cs.title = "Gross Revenue"
|
108
|
-
cs.rows = [ CalculationRow.new(:key => "Operating Revenues (OR)", :vals => [@
|
109
|
-
CalculationRow.new(:key => "Cost of Goods Sold (COGS)", :vals => [@
|
59
|
+
cs.rows = [ CalculationRow.new(:key => "Operating Revenues (OR)", :vals => [@operating_revenues.total] ),
|
60
|
+
CalculationRow.new(:key => "Cost of Goods Sold (COGS)", :vals => [@cost_of_revenues.total] ) ]
|
110
61
|
return cs
|
111
62
|
end
|
112
63
|
|
113
|
-
def operating_expenses
|
114
|
-
@oe
|
115
|
-
end
|
116
|
-
|
117
64
|
def income_from_sales_before_tax
|
118
65
|
cs = FinModeling::CalculationSummary.new
|
119
66
|
cs.title = "Operating Income from sales, before tax (OISBT)"
|
120
67
|
cs.rows = [ CalculationRow.new(:key => "Gross Margin (GM)", :vals => [@gm] ),
|
121
|
-
CalculationRow.new(:key => "Operating Expense (OE)", :vals => [@
|
68
|
+
CalculationRow.new(:key => "Operating Expense (OE)", :vals => [@operating_expenses.total] ) ]
|
122
69
|
return cs
|
123
70
|
end
|
124
71
|
|
@@ -204,7 +151,7 @@ module FinModeling
|
|
204
151
|
return annualize_rate(prev, rate)
|
205
152
|
end
|
206
153
|
|
207
|
-
def re_oi(prev_bal_sheet, expected_rate_of_return
|
154
|
+
def re_oi(prev_bal_sheet, expected_rate_of_return)
|
208
155
|
e_ror = deannualize_rate(prev_bal_sheet, expected_rate_of_return)
|
209
156
|
return (operating_income_after_tax.total - (e_ror * prev_bal_sheet.net_operating_assets.total))
|
210
157
|
end
|
@@ -242,7 +189,7 @@ module FinModeling
|
|
242
189
|
return analysis
|
243
190
|
end
|
244
191
|
|
245
|
-
def analysis(re_bs, prev_re_is, prev_re_bs)
|
192
|
+
def analysis(re_bs, prev_re_is, prev_re_bs, expected_rate_of_return)
|
246
193
|
analysis = CalculationSummary.new
|
247
194
|
analysis.title = ""
|
248
195
|
analysis.rows = []
|
@@ -253,11 +200,11 @@ module FinModeling
|
|
253
200
|
analysis.header_row = CalculationHeader.new(:key => "", :vals => [re_bs.period.to_pretty_s])
|
254
201
|
end
|
255
202
|
|
256
|
-
analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [operating_revenues.total.to_nearest_million])
|
203
|
+
analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [@operating_revenues.total.to_nearest_million])
|
257
204
|
if Config.income_detail_enabled?
|
258
|
-
analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [@
|
205
|
+
analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [@cost_of_revenues.total.to_nearest_million])
|
259
206
|
analysis.rows << CalculationRow.new(:key => "GM ($MM)", :vals => [@gm.to_nearest_million])
|
260
|
-
analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [@
|
207
|
+
analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [@operating_expenses.total.to_nearest_million])
|
261
208
|
analysis.rows << CalculationRow.new(:key => "OISBT ($MM)", :vals => [income_from_sales_before_tax.total.to_nearest_million])
|
262
209
|
end
|
263
210
|
analysis.rows << CalculationRow.new(:key => "Core OI ($MM)", :vals => [income_from_sales_after_tax.total.to_nearest_million])
|
@@ -276,7 +223,7 @@ module FinModeling
|
|
276
223
|
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [revenue_growth(prev_re_is)])
|
277
224
|
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [core_oi_growth(prev_re_is)])
|
278
225
|
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [oi_growth( prev_re_is)])
|
279
|
-
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [re_oi(
|
226
|
+
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [re_oi(prev_re_bs, expected_rate_of_return).to_nearest_million])
|
280
227
|
else
|
281
228
|
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [nil])
|
282
229
|
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [nil])
|
@@ -290,10 +237,9 @@ module FinModeling
|
|
290
237
|
end
|
291
238
|
|
292
239
|
def self.forecast_next(period, policy, last_re_bs, last_re_is)
|
293
|
-
operating_revenues =
|
294
|
-
income_from_sales_after_tax = operating_revenues * policy.
|
295
|
-
net_financing_income = last_re_bs.net_financial_assets.total * Ratio.new(policy.
|
296
|
-
|
240
|
+
operating_revenues = policy.revenue_on(period.value["end_date"])
|
241
|
+
income_from_sales_after_tax = operating_revenues * policy.sales_pm_on(period.value["end_date"])
|
242
|
+
net_financing_income = last_re_bs.net_financial_assets.total * Ratio.new(policy.fi_over_nfa_on(period.value["end_date"])).yearly_to_quarterly
|
297
243
|
comprehensive_income = income_from_sales_after_tax + net_financing_income
|
298
244
|
|
299
245
|
ForecastedReformulatedIncomeStatement.new(period, operating_revenues,
|
@@ -325,112 +271,4 @@ module FinModeling
|
|
325
271
|
|
326
272
|
end
|
327
273
|
|
328
|
-
class ForecastedReformulatedIncomeStatement < ReformulatedIncomeStatement
|
329
|
-
def initialize(period, operating_revenues, income_from_sales_after_tax, net_financing_income, comprehensive_income)
|
330
|
-
@period = period
|
331
|
-
@orev = operating_revenues
|
332
|
-
@income_from_sales_after_tax = income_from_sales_after_tax
|
333
|
-
@net_financing_income = net_financing_income
|
334
|
-
@comprehensive_income = comprehensive_income
|
335
|
-
end
|
336
|
-
|
337
|
-
def -(ris2)
|
338
|
-
raise RuntimeError.new("not implmeneted")
|
339
|
-
end
|
340
|
-
|
341
|
-
def operating_revenues
|
342
|
-
cs = FinModeling::CalculationSummary.new
|
343
|
-
cs.title = "Operating Revenues"
|
344
|
-
cs.rows = [ CalculationRow.new(:key => "Operating Revenues (OR)", :vals => [@orev] ) ]
|
345
|
-
return cs
|
346
|
-
end
|
347
|
-
|
348
|
-
def cost_of_revenues
|
349
|
-
nil
|
350
|
-
end
|
351
|
-
|
352
|
-
def gross_revenue
|
353
|
-
nil
|
354
|
-
end
|
355
|
-
|
356
|
-
def operating_expenses
|
357
|
-
nil
|
358
|
-
end
|
359
|
-
|
360
|
-
def income_from_sales_before_tax
|
361
|
-
nil
|
362
|
-
end
|
363
|
-
|
364
|
-
def income_from_sales_after_tax
|
365
|
-
cs = FinModeling::CalculationSummary.new
|
366
|
-
cs.title = "Operating Income from sales, after tax (OISAT)"
|
367
|
-
cs.rows = [ CalculationRow.new(:key => "Operating income from sales (after tax)", :vals => [@income_from_sales_after_tax] ) ]
|
368
|
-
return cs
|
369
|
-
end
|
370
|
-
|
371
|
-
def operating_income_after_tax
|
372
|
-
income_from_sales_after_tax # this simplified version assumes no non-sales operating income
|
373
|
-
end
|
374
|
-
|
375
|
-
def net_financing_income
|
376
|
-
cs = FinModeling::CalculationSummary.new
|
377
|
-
cs.title = "Net financing income, after tax (NFI)"
|
378
|
-
cs.rows = [ CalculationRow.new(:key => "Net financing income", :vals => [@net_financing_income] ) ]
|
379
|
-
return cs
|
380
|
-
end
|
381
|
-
|
382
|
-
def comprehensive_income
|
383
|
-
cs = FinModeling::CalculationSummary.new
|
384
|
-
cs.title = "Comprehensive Income (CI)"
|
385
|
-
cs.rows = [ CalculationRow.new(:key => "Comprehensive income", :vals => [@comprehensive_income] ) ]
|
386
|
-
return cs
|
387
|
-
end
|
388
|
-
|
389
|
-
def analysis(re_bs, prev_re_is, prev_re_bs)
|
390
|
-
analysis = CalculationSummary.new
|
391
|
-
analysis.title = ""
|
392
|
-
analysis.rows = []
|
393
|
-
|
394
|
-
if re_bs.nil?
|
395
|
-
analysis.header_row = CalculationHeader.new(:key => "", :vals => ["Unknown..."])
|
396
|
-
else
|
397
|
-
analysis.header_row = CalculationHeader.new(:key => "", :vals => [re_bs.period.to_pretty_s + "E"])
|
398
|
-
end
|
399
|
-
|
400
|
-
analysis.rows << CalculationRow.new(:key => "Revenue ($MM)", :vals => [operating_revenues.total.to_nearest_million])
|
401
|
-
if Config.income_detail_enabled?
|
402
|
-
analysis.rows << CalculationRow.new(:key => "COGS ($MM)", :vals => [nil])
|
403
|
-
analysis.rows << CalculationRow.new(:key => "GM ($MM)", :vals => [nil])
|
404
|
-
analysis.rows << CalculationRow.new(:key => "OE ($MM)", :vals => [nil])
|
405
|
-
analysis.rows << CalculationRow.new(:key => "OISBT ($MM)", :vals => [nil])
|
406
|
-
end
|
407
|
-
analysis.rows << CalculationRow.new(:key => "Core OI ($MM)", :vals => [income_from_sales_after_tax.total.to_nearest_million])
|
408
|
-
analysis.rows << CalculationRow.new(:key => "OI ($MM)", :vals => [nil])
|
409
|
-
analysis.rows << CalculationRow.new(:key => "FI ($MM)", :vals => [net_financing_income.total.to_nearest_million])
|
410
|
-
analysis.rows << CalculationRow.new(:key => "NI ($MM)", :vals => [comprehensive_income.total.to_nearest_million])
|
411
|
-
analysis.rows << CalculationRow.new(:key => "Gross Margin", :vals => [nil])
|
412
|
-
analysis.rows << CalculationRow.new(:key => "Sales PM", :vals => [sales_profit_margin])
|
413
|
-
analysis.rows << CalculationRow.new(:key => "Operating PM", :vals => [nil])
|
414
|
-
analysis.rows << CalculationRow.new(:key => "FI / Sales", :vals => [fi_over_sales])
|
415
|
-
analysis.rows << CalculationRow.new(:key => "NI / Sales", :vals => [ni_over_sales])
|
416
|
-
|
417
|
-
if !prev_re_bs.nil? && !prev_re_is.nil?
|
418
|
-
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [sales_over_noa(prev_re_bs)])
|
419
|
-
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [fi_over_nfa( prev_re_bs)])
|
420
|
-
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [revenue_growth(prev_re_is)])
|
421
|
-
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [core_oi_growth(prev_re_is)])
|
422
|
-
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil])
|
423
|
-
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [re_oi( prev_re_bs).to_nearest_million])
|
424
|
-
else
|
425
|
-
analysis.rows << CalculationRow.new(:key => "Sales / NOA", :vals => [nil])
|
426
|
-
analysis.rows << CalculationRow.new(:key => "FI / NFA", :vals => [nil])
|
427
|
-
analysis.rows << CalculationRow.new(:key => "Revenue Growth",:vals => [nil])
|
428
|
-
analysis.rows << CalculationRow.new(:key => "Core OI Growth",:vals => [nil])
|
429
|
-
analysis.rows << CalculationRow.new(:key => "OI Growth", :vals => [nil])
|
430
|
-
analysis.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => [nil])
|
431
|
-
end
|
432
|
-
|
433
|
-
return analysis
|
434
|
-
end
|
435
|
-
end
|
436
274
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ReformulatedShareholderEquityStatement
|
3
|
+
attr_accessor :period
|
4
|
+
|
5
|
+
def initialize(period, equity_change_summary)
|
6
|
+
@period = period
|
7
|
+
|
8
|
+
@share_issue = equity_change_summary.filter_by_type(:share_issue )
|
9
|
+
@minority_int = equity_change_summary.filter_by_type(:minority_int )
|
10
|
+
@share_repurch = equity_change_summary.filter_by_type(:share_repurch)
|
11
|
+
@common_div = equity_change_summary.filter_by_type(:common_div )
|
12
|
+
@net_income = equity_change_summary.filter_by_type(:net_income )
|
13
|
+
@oci = equity_change_summary.filter_by_type(:oci )
|
14
|
+
@preferred_div = equity_change_summary.filter_by_type(:preferred_div)
|
15
|
+
end
|
16
|
+
|
17
|
+
def transactions_with_shareholders
|
18
|
+
cs = FinModeling::CalculationSummary.new
|
19
|
+
cs.title = "Transactions with Shareholders"
|
20
|
+
cs.rows = [ CalculationRow.new(:key => "Share Issues", :vals => [@share_issue .total] ),
|
21
|
+
CalculationRow.new(:key => "Minority Interest", :vals => [@minority_int .total] ),
|
22
|
+
CalculationRow.new(:key => "Share Repurchases", :vals => [@share_repurch.total] ),
|
23
|
+
CalculationRow.new(:key => "Common Dividends", :vals => [@common_div .total] ) ]
|
24
|
+
return cs
|
25
|
+
end
|
26
|
+
|
27
|
+
def comprehensive_income
|
28
|
+
cs = FinModeling::CalculationSummary.new
|
29
|
+
cs.title = "Comprehensive Income"
|
30
|
+
cs.rows = [ CalculationRow.new(:key => "Net Income", :vals => [@net_income .total] ),
|
31
|
+
CalculationRow.new(:key => "Other Comprehensive Income", :vals => [@oci .total] ),
|
32
|
+
CalculationRow.new(:key => "Preferred Dividends", :vals => [@preferred_div.total] ) ]
|
33
|
+
return cs
|
34
|
+
end
|
35
|
+
|
36
|
+
def analysis
|
37
|
+
analysis = CalculationSummary.new
|
38
|
+
|
39
|
+
analysis.title = ""
|
40
|
+
analysis.header_row = CalculationHeader.new(:key => "", :vals => [@period.value["end_date"].to_s])
|
41
|
+
|
42
|
+
analysis.rows = []
|
43
|
+
analysis.rows << CalculationRow.new(:key => "Tx w Shareholders ($MM)", :vals => [transactions_with_shareholders.total.to_nearest_million])
|
44
|
+
analysis.rows << CalculationRow.new(:key => "CI ($MM)", :vals => [comprehensive_income.total.to_nearest_million])
|
45
|
+
|
46
|
+
return analysis
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ReOIValuation
|
3
|
+
def initialize(filings, forecasts, cost_of_capital, num_shares)
|
4
|
+
@filings, @forecasts, @cost_of_capital, @num_shares = [filings, forecasts, cost_of_capital, num_shares]
|
5
|
+
@discount_rate = FinModeling::DiscountRate.new(@cost_of_capital.value + 1.0)
|
6
|
+
end
|
7
|
+
|
8
|
+
def summary
|
9
|
+
s = CalculationSummary.new
|
10
|
+
s.title = "ReOI Valuation"
|
11
|
+
s.totals_row_enabled = false
|
12
|
+
|
13
|
+
s.header_row = CalculationHeader.new(:key => "", :vals => periods.map{ |x| x.to_pretty_s + ((x.value > Date.today) ? "E" : "") })
|
14
|
+
|
15
|
+
s.rows = [ ]
|
16
|
+
|
17
|
+
s.rows << CalculationRow.new(:key => "ReOI ($MM)", :vals => reoi_vals.map{ |x| x ? x.to_nearest_million : nil })
|
18
|
+
s.rows << CalculationRow.new(:key => "PV(ReOI) ($MM)", :vals => pv_reoi_vals.map{ |x| x ? x.to_nearest_million : nil })
|
19
|
+
s.rows << CalculationRow.new(:key => "CV ($MM)", :vals => cv_vals.map{ |x| x ? x.to_nearest_million : nil })
|
20
|
+
s.rows << CalculationRow.new(:key => "PV(CV) ($MM)", :vals => pv_cv_vals.map{ |x| x ? x.to_nearest_million : nil })
|
21
|
+
s.rows << CalculationRow.new(:key => "Book Value of Common Equity ($MM)", :vals => bv_cse_vals.map{ |x| x ? x.to_nearest_million : nil })
|
22
|
+
s.rows << CalculationRow.new(:key => "Enterprise Value ($MM)", :vals => ev_vals.map{ |x| x ? x.to_nearest_million : nil })
|
23
|
+
s.rows << CalculationRow.new(:key => "NFA ($MM)", :vals => bv_nfa_vals.map{ |x| x ? x.to_nearest_million : nil })
|
24
|
+
s.rows << CalculationRow.new(:key => "Value of Common Equity ($MM)", :vals => cse_vals.map{ |x| x ? x.to_nearest_million : nil })
|
25
|
+
s.rows << CalculationRow.new(:key => "# Shares (MM)", :vals => num_shares_vals.map{ |x| x ? x.to_nearest_million : nil })
|
26
|
+
s.rows << CalculationRow.new(:key => "Value / Share ($)", :vals => val_per_shares_vals.map{ |x| x ? x.to_nearest_dollar(num_decimals=2) : nil })
|
27
|
+
|
28
|
+
return s
|
29
|
+
end
|
30
|
+
|
31
|
+
def periods
|
32
|
+
@periods ||= [ @filings.re_bs_arr.last.period ] +
|
33
|
+
@forecasts.reformulated_balance_sheets.map{ |x| x.period }
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def reoi_vals
|
39
|
+
prev_re_bses = [@filings.re_bs_arr.last] + @forecasts.reformulated_balance_sheets[0..-2]
|
40
|
+
re_ises = @forecasts.reformulated_income_statements
|
41
|
+
re_ois = [nil] + re_ises.zip(prev_re_bses).map{ |pair| pair[0].re_oi(pair[1], @cost_of_capital.value) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def pv_reoi_vals
|
45
|
+
reoi_vals[0..-2].each_with_index.map do |reoi, idx|
|
46
|
+
days_from_now = periods[idx].value - Date.today
|
47
|
+
d = @discount_rate.annualize(from_days=365, to_days=days_from_now)
|
48
|
+
reoi ? (reoi / d) : nil
|
49
|
+
end + [nil]
|
50
|
+
end
|
51
|
+
|
52
|
+
def cv_vals
|
53
|
+
vals = [nil]*periods.length
|
54
|
+
d = @cost_of_capital.annualize(from_days=365, @forecasts.reformulated_income_statements.last.period.days)
|
55
|
+
vals[-2] = reoi_vals.last / d
|
56
|
+
vals
|
57
|
+
end
|
58
|
+
|
59
|
+
def pv_cv_vals
|
60
|
+
cv_vals[0..-2].each_with_index.map do |cv, idx|
|
61
|
+
days_from_now = periods[idx].value - Date.today
|
62
|
+
d = @discount_rate.annualize(from_days=365, to_days=days_from_now)
|
63
|
+
cv ? (cv / d) : nil
|
64
|
+
end + [nil]
|
65
|
+
end
|
66
|
+
|
67
|
+
def bv_cse_vals
|
68
|
+
vals = [nil]*periods.length
|
69
|
+
vals[0] = @filings.re_bs_arr.last.common_shareholders_equity.total
|
70
|
+
vals
|
71
|
+
end
|
72
|
+
|
73
|
+
def ev_vals
|
74
|
+
vals = [nil]*periods.length
|
75
|
+
vals[0] = (pv_reoi_vals[1..-2] + pv_cv_vals[-2..-2] + bv_cse_vals[0..0]).inject(:+)
|
76
|
+
vals
|
77
|
+
end
|
78
|
+
|
79
|
+
def bv_nfa_vals
|
80
|
+
vals = [nil]*periods.length
|
81
|
+
vals[0] = @filings.re_bs_arr.last.net_financial_assets.total
|
82
|
+
vals
|
83
|
+
end
|
84
|
+
|
85
|
+
def cse_vals
|
86
|
+
vals = [nil]*periods.length
|
87
|
+
vals[0] = ev_vals[0] + bv_nfa_vals[0]
|
88
|
+
vals
|
89
|
+
end
|
90
|
+
|
91
|
+
def num_shares_vals
|
92
|
+
vals = [nil]*periods.length
|
93
|
+
vals[0] = @num_shares
|
94
|
+
vals
|
95
|
+
end
|
96
|
+
|
97
|
+
def val_per_shares_vals
|
98
|
+
vals = [nil]*periods.length
|
99
|
+
vals[0] = cse_vals[0] / @num_shares
|
100
|
+
vals
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class ShareholderEquityStatementCalculation < CompanyFilingCalculation
|
3
|
+
#include CanChooseSuccessivePeriods
|
4
|
+
|
5
|
+
EC_GOAL = "change in shareholder equity"
|
6
|
+
EC_LABELS = [ /^stockholders equity period increase decrease$/ ]
|
7
|
+
EC_ANTI_LABELS = [ ]
|
8
|
+
EC_IDS = [ /^(|Locator_|loc_)(|us-gaap_)StockholdersEquityPeriodIncreaseDecrease[_a-z0-9]+/ ]
|
9
|
+
def equity_change_calculation
|
10
|
+
begin
|
11
|
+
@ec ||= EquityChangeCalculation.new(find_calculation_arc(EC_GOAL, EC_LABELS, EC_ANTI_LABELS, EC_IDS))
|
12
|
+
rescue FinModeling::InvalidFilingError => e
|
13
|
+
pre_msg = "calculation tree:\n" + self.calculation.sprint_tree
|
14
|
+
raise e, pre_msg+e.message, e.backtrace
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_valid?
|
19
|
+
return true
|
20
|
+
end
|
21
|
+
|
22
|
+
def reformulated(period)
|
23
|
+
return ReformulatedShareholderEquityStatement.new(period,
|
24
|
+
equity_change_calculation.summary(:period=>period))
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_constructor(file, item_name)
|
28
|
+
item_calc_name = item_name + "_calc"
|
29
|
+
@calculation.write_constructor(file, item_calc_name)
|
30
|
+
file.puts "#{item_name} = FinModeling::ShareholderEquityStatementCalculation.new(#{item_calc_name})"
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -20,7 +20,24 @@ class String
|
|
20
20
|
self.gsub(r, '\1')
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def matches_any_regex?(regexes)
|
24
24
|
return regexes.inject(false){ |matches, regex| matches or regex =~ self }
|
25
25
|
end
|
26
|
+
|
27
|
+
def split_into_lines_shorter_than(max_line_width)
|
28
|
+
lines = []
|
29
|
+
cur_line = []
|
30
|
+
|
31
|
+
split(' ').each do |word|
|
32
|
+
if (cur_line + [word]).join(' ').length > max_line_width
|
33
|
+
lines << cur_line.join(' ')
|
34
|
+
cur_line = []
|
35
|
+
end
|
36
|
+
|
37
|
+
cur_line << word
|
38
|
+
end
|
39
|
+
|
40
|
+
lines << cur_line.join(' ') if !cur_line.empty?
|
41
|
+
lines
|
42
|
+
end
|
26
43
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module FinModeling
|
2
|
+
class TimeSeriesEstimator
|
3
|
+
attr_reader :a, :b
|
4
|
+
|
5
|
+
def initialize(a, b)
|
6
|
+
@a, @b = a, b
|
7
|
+
end
|
8
|
+
|
9
|
+
def estimate_on(date)
|
10
|
+
x = (date - Date.today)
|
11
|
+
a + (b*x)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from_time_series(dates, ys)
|
15
|
+
xs = dates.map{ |date| date - Date.today }
|
16
|
+
|
17
|
+
simple_regression = Statsample::Regression.simple(xs.to_scale, ys.to_scale)
|
18
|
+
TimeSeriesEstimator.new(simple_regression.a, simple_regression.b)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_const(y)
|
22
|
+
TimeSeriesEstimator.new(a=y, b=0.0)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|