finrb 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5331d4b4769cc183d279c1eb4d837d05137cb6b09dd69394addb7be0c6debfae
4
- data.tar.gz: de97a959862d55711e7e6989dc8c47ffa277dcea32cc8184cc89f2158edad160
3
+ metadata.gz: 8474be54c480d3150125ba2a00e7b935fbfb10281ce857304889ebca48839bac
4
+ data.tar.gz: 571f0f38b0a662c749df453e314cb3f916efe27e2f466896e600719a9048aef1
5
5
  SHA512:
6
- metadata.gz: e57aeeac9a952790c895d367170596097d15fe5cbdb43bcab6a9943bfa80bff0a3a4d1e9d016e859d6f554b7a892a78ee2b3896f961b850f9ac1fafd7e064948
7
- data.tar.gz: 84e26bbd5a9862c6733cbce98cebe4ebe1640751992c6fffe74d33ba31b39dc77f0f0ca5cf4ec8b6fd6bb6d0c62fa5bab1cbbacd3b0a8384d4d1eb1573b9528d
6
+ metadata.gz: 31edbc91ddfe6bfd433daaae2598be24b7fe22d604e40745b238a66c01ad8cd597d9a441b4643f9cd0a1adc5230cd2e3780fc1ba791e5799e921ffb0a6bc6e59
7
+ data.tar.gz: 59b760dfb7f1c27686d34362ad9da310b2bb20ccfc440b42f39e6c551fd14c1ac134dd3ed8795e0cf9d4274c264e085a95b59339bd71b5c11e6763f99f9b82a1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # finrb changelog
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Validation and financial correctness
6
+
7
+ - Apply the shared finite-decimal validation contract across yields, returns,
8
+ accounting, ratios, cashflows, rates, amortization, and TVM helpers.
9
+ - Replace abbreviated validation errors with descriptive financial terms such
10
+ as period count, future value, face value, and compounding periods.
11
+ - Add explicit denominator, rate, share-count, inventory, useful-life, and
12
+ residual-value domain checks while preserving valid decimal calculations and
13
+ return types.
14
+ - Fix LIFO ending-inventory retention when a sale is satisfied by the second
15
+ purchase layer, preserving inventory cost conservation.
16
+ - Reject invalid weighted-return vectors and replace the previous unsolicited
17
+ weighted-portfolio warning with an explicit validation error.
18
+
19
+ ### Development and project maintenance
20
+
21
+ - Add deterministic `benchmark:run` scenarios using `benchmark-ips` for IRR,
22
+ XIRR, and amortization scaling, with known-root, equation-residual, solver
23
+ evaluation, and schedule-reconciliation preflight diagnostics.
24
+ - Add contributor and security guidance, and link the project entry documents
25
+ from the README.
26
+ - Include contributor and security documentation in packaged gem metadata.
27
+
3
28
  ## 1.1.0
4
29
 
5
30
  ### Investment returns and risk
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,89 @@
1
+ # Contributing to finrb
2
+
3
+ finrb welcomes focused bug fixes, documentation improvements, tests, and
4
+ financial calculations that fit the project's scope. Before starting a large
5
+ feature or public API change, open an issue so its financial conventions and
6
+ design can be agreed upon first.
7
+
8
+ ## Development setup
9
+
10
+ finrb requires Ruby 3.3 or newer. MRI 3.3, 3.4, and 4.0 are supported; JRuby
11
+ and TruffleRuby are tested as informational compatibility targets.
12
+
13
+ ```shell
14
+ bundle install
15
+ bundle exec rake quality
16
+ bundle exec rubocop
17
+ ```
18
+
19
+ `rake quality` runs the RSpec suite with coverage, verifies maintained API
20
+ examples, and validates the packaged RBS declarations. Additional checks are
21
+ available for changes that affect their domains:
22
+
23
+ ```shell
24
+ bundle exec rake security:audit
25
+ bundle exec rake package:verify
26
+ bundle exec rake benchmark:run
27
+ bundle exec rake solver:verify
28
+ ```
29
+
30
+ The solver verification task requires the optional Python environment
31
+ documented in [spec/fixtures/README.md](spec/fixtures/README.md). The Python
32
+ packages are development references and are not gem dependencies.
33
+
34
+ ## Making changes
35
+
36
+ - Preserve unrelated code and behavior. Keep pull requests small enough to
37
+ review their financial and numerical consequences directly.
38
+ - Follow the local Ruby style and run RuboCop. Comments should explain a
39
+ non-obvious reason or convention, not restate the code.
40
+ - Route public numeric inputs through `Finrb::Validation`. Use `ArgumentError`
41
+ for malformed inputs and namespaced finrb errors for financial or numerical
42
+ domains.
43
+ - Preserve `Flt::DecNum` calculations and return types unless the change has a
44
+ documented compatibility reason.
45
+ - Update RBS declarations and user documentation when a public contract
46
+ changes.
47
+ - Do not add an unreleased changelog section for ordinary development work.
48
+ The maintainer prepares the changelog as part of an actual release.
49
+
50
+ ## Numerical and financial changes
51
+
52
+ A formula compiling or producing a plausible number is not sufficient
53
+ verification. Describe the convention being implemented and test the relevant
54
+ invariants, boundaries, and failure modes.
55
+
56
+ For a solver or precision-sensitive change, include as appropriate:
57
+
58
+ - known-root or algebraically constructed cases;
59
+ - normalized equation residuals;
60
+ - zero, negative-rate, long-horizon, and scale-sensitive cases;
61
+ - attributable fixtures from a reputable independent implementation under
62
+ matching financial conventions;
63
+ - deterministic seeds for generated cases; and
64
+ - benchmarks that compare the same scenario across revisions, runtimes, or
65
+ architectures rather than ranking unrelated calculations.
66
+
67
+ Reference fixtures must record their source, version, conventions, generation
68
+ method, and tolerance. Do not replace committed fixtures merely to make a
69
+ changed implementation pass without explaining why the reference changed.
70
+
71
+ ## Pull requests
72
+
73
+ Use the pull request template and include:
74
+
75
+ - the financial or technical problem;
76
+ - the intended contract and compatibility impact;
77
+ - the evidence used to verify correctness; and
78
+ - any checks skipped because an optional runtime or external reference was
79
+ unavailable.
80
+
81
+ Commit subjects in this repository commonly use the form
82
+ `domain [Category]: Imperative summary`, for example:
83
+
84
+ ```text
85
+ cashflows [Fix]: Preserve negative-root selection
86
+ ```
87
+
88
+ By contributing, you agree that your work is provided under the repository's
89
+ LGPL-3.0-or-later license.
data/README.md CHANGED
@@ -64,8 +64,8 @@ API explicitly returns another financial object, such as `Finrb::Rate`.
64
64
  | `Finrb::Ratios` | Liquidity, leverage, profitability, and per-share ratios |
65
65
  | `Finrb::Accounting` | Inventory costing and depreciation |
66
66
 
67
- The detailed [API and examples guide](docs/api.md) lists each calculation and
68
- its parameters. Packaged RBS declarations are available under `sig/`.
67
+ The [API and examples guide](docs/api.md) documents each financial domain and
68
+ its public calculations. Packaged RBS declarations are available under `sig/`.
69
69
 
70
70
  ## Cashflows
71
71
 
@@ -338,6 +338,8 @@ Docker, batching, and tolerance details.
338
338
  - [RubyGems](https://rubygems.org/gems/finrb)
339
339
  - [Source](https://github.com/ncs1/finrb)
340
340
  - [Issue tracker](https://github.com/ncs1/finrb/issues)
341
+ - [Contributing](CONTRIBUTING.md)
342
+ - [Security policy](SECURITY.md)
341
343
 
342
344
  ## Acknowledgements
343
345
 
data/SECURITY.md ADDED
@@ -0,0 +1,41 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are made against the current released finrb line. Older
6
+ versions may receive a fix when practical, but they are not guaranteed to be
7
+ maintained. Users should reproduce an issue on the newest release before
8
+ reporting it when possible.
9
+
10
+ JRuby and TruffleRuby are informational compatibility targets. A
11
+ runtime-specific vulnerability may also need to be reported to the relevant
12
+ Ruby implementation or dependency maintainers.
13
+
14
+ ## Reporting a vulnerability
15
+
16
+ Do not disclose a suspected vulnerability in a public issue, discussion, or
17
+ pull request. Use GitHub's private vulnerability reporting for finrb:
18
+
19
+ <https://github.com/ncs1/finrb/security/advisories/new>
20
+
21
+ If private reporting is unavailable, contact the maintainer through the email
22
+ listed in the gem metadata. Include only enough information in the initial
23
+ message to establish a private channel.
24
+
25
+ A useful report contains:
26
+
27
+ - affected finrb and Ruby versions;
28
+ - the affected API or packaged artifact;
29
+ - minimal reproduction steps;
30
+ - the expected and observed impact; and
31
+ - any known mitigations or disclosure constraints.
32
+
33
+ Please avoid including production credentials, private financial data, access
34
+ tokens, or other secrets. There is no guaranteed response or remediation
35
+ timeline; this project is maintained by one person. Reports will be evaluated
36
+ according to reproducibility, impact, and available maintainer capacity.
37
+
38
+ Numerical disagreement alone is normally a correctness bug rather than a
39
+ security vulnerability. Treat it as security-sensitive when it can cross a
40
+ trust boundary, bypass validation, enable denial of service, corrupt packaged
41
+ artifacts, or predictably cause unsafe downstream financial decisions.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'decimal'
4
4
  require_relative 'errors'
5
+ require_relative 'validation'
5
6
 
6
7
  module Finrb
7
8
  # Inventory costing and depreciation calculations.
@@ -34,74 +35,32 @@ module Finrb
34
35
  # @example
35
36
  # Finrb::Accounting.cogs(uinv=2,pinv=2,units=[3,5],price=[3,5],sinv=7,method="WAC")
36
37
  def self.cogs(uinv:, pinv:, units:, price:, sinv:, method: 'FIFO')
37
- uinv = Flt::DecNum(uinv.to_s)
38
- pinv = Flt::DecNum(pinv.to_s)
39
- units = wrap_array(units).map { |value| Flt::DecNum(value.to_s) }
40
- price = wrap_array(price).map { |value| Flt::DecNum(value.to_s) }
41
- sinv = Flt::DecNum(sinv.to_s)
42
- method = method.to_s
38
+ uinv, pinv, units, price, sinv, method = inventory_inputs(uinv:, pinv:, units:, price:, sinv:, method:)
43
39
 
44
40
  n = units.size
45
- m = price.size
46
- cost_of_goods = 0
47
- ending_inventory = 0
48
- if m == n
49
- case method
50
- when 'FIFO'
51
- if sinv <= uinv
52
- cost_of_goods = sinv * pinv
53
- ending_inventory = (uinv - sinv) * pinv
54
- (0...n).each do |i|
55
- ending_inventory += (units[i] * price[i])
56
- end
57
- else
58
- cost_of_goods = uinv * pinv
59
- sinv -= uinv
60
- (0...n).each do |i|
61
- if sinv <= units[i]
62
- cost_of_goods += (sinv * price[i])
63
- ending_inventory = (units[i] - sinv) * price[i]
64
- if i < n
65
- temp = i + 1
66
- (temp...n).each do |j|
67
- ending_inventory += (units[j] * price[j])
68
- end
69
- end
70
- sinv = 0
71
- break
72
- else
73
- cost_of_goods += (units[i] * price[i])
74
- sinv -= units[i]
75
- end
76
- end
77
- raise(Error, "Inventory is not enough to sell\n") if sinv.positive?
78
- end
79
- when 'WAC'
80
- ending_inventory = uinv * pinv
81
- tu = uinv
41
+ cost_of_goods = Flt::DecNum(0)
42
+ ending_inventory = Flt::DecNum(0)
43
+ case method
44
+ when 'FIFO'
45
+ if sinv <= uinv
46
+ cost_of_goods = sinv * pinv
47
+ ending_inventory = (uinv - sinv) * pinv
82
48
  (0...n).each do |i|
83
49
  ending_inventory += (units[i] * price[i])
84
- tu += units[i]
85
- end
86
- if tu >= sinv
87
- cost_of_goods = ending_inventory / tu * sinv
88
- ending_inventory = ending_inventory / tu * (tu - sinv)
89
- else
90
- raise(Error, "Inventory is not enough to sell\n")
91
50
  end
92
-
93
- when 'LIFO'
94
- (n - 1).downto(0).each do |i|
51
+ else
52
+ cost_of_goods = uinv * pinv
53
+ sinv -= uinv
54
+ (0...n).each do |i|
95
55
  if sinv <= units[i]
96
56
  cost_of_goods += (sinv * price[i])
97
57
  ending_inventory = (units[i] - sinv) * price[i]
98
- if i > 1
99
- temp = i - 1
100
- temp.downto(0).each do |j|
58
+ if i < n
59
+ temp = i + 1
60
+ (temp...n).each do |j|
101
61
  ending_inventory += (units[j] * price[j])
102
62
  end
103
63
  end
104
- ending_inventory += (uinv * pinv)
105
64
  sinv = 0
106
65
  break
107
66
  else
@@ -109,18 +68,52 @@ module Finrb
109
68
  sinv -= units[i]
110
69
  end
111
70
  end
112
- if sinv.positive?
113
- if sinv <= uinv
114
- cost_of_goods += (sinv * pinv)
115
- ending_inventory += ((uinv - sinv) * pinv)
116
- else
117
- raise(Error, "Inventory is not enough to sell\n")
71
+ raise(DomainError, 'Available inventory is insufficient for the requested sale.') if sinv.positive?
72
+ end
73
+ when 'WAC'
74
+ ending_inventory = uinv * pinv
75
+ tu = uinv
76
+ (0...n).each do |i|
77
+ ending_inventory += (units[i] * price[i])
78
+ tu += units[i]
79
+ end
80
+ if tu.zero? && sinv.zero?
81
+ cost_of_goods = Flt::DecNum(0)
82
+ ending_inventory = Flt::DecNum(0)
83
+ elsif tu >= sinv
84
+ cost_of_goods = ending_inventory / tu * sinv
85
+ ending_inventory = ending_inventory / tu * (tu - sinv)
86
+ else
87
+ raise(DomainError, 'Available inventory is insufficient for the requested sale.')
88
+ end
89
+
90
+ when 'LIFO'
91
+ (n - 1).downto(0).each do |i|
92
+ if sinv <= units[i]
93
+ cost_of_goods += (sinv * price[i])
94
+ ending_inventory = (units[i] - sinv) * price[i]
95
+ if i.positive?
96
+ temp = i - 1
97
+ temp.downto(0).each do |j|
98
+ ending_inventory += (units[j] * price[j])
99
+ end
118
100
  end
101
+ ending_inventory += (uinv * pinv)
102
+ sinv = 0
103
+ break
104
+ else
105
+ cost_of_goods += (units[i] * price[i])
106
+ sinv -= units[i]
107
+ end
108
+ end
109
+ if sinv.positive?
110
+ if sinv <= uinv
111
+ cost_of_goods += (sinv * pinv)
112
+ ending_inventory += ((uinv - sinv) * pinv)
113
+ else
114
+ raise(DomainError, 'Available inventory is insufficient for the requested sale.')
119
115
  end
120
116
  end
121
-
122
- else
123
- raise(Error, "length of units and price are not the same\n")
124
117
  end
125
118
 
126
119
  {
@@ -129,6 +122,20 @@ module Finrb
129
122
  }
130
123
  end
131
124
 
125
+ def self.inventory_inputs(uinv:, pinv:, units:, price:, sinv:, method:)
126
+ uinv = Validation.non_negative_decimal(uinv, name: 'beginning inventory units')
127
+ pinv = Validation.non_negative_decimal(pinv, name: 'beginning inventory unit cost')
128
+ units = inventory_values(units, name: 'purchase units')
129
+ price = inventory_values(price, name: 'purchase unit cost')
130
+ sinv = Validation.non_negative_decimal(sinv, name: 'units sold')
131
+ method = method.to_s
132
+ raise(ArgumentError, 'Inventory costing method must be FIFO, LIFO, or WAC.') unless %w[FIFO LIFO WAC].include?(method)
133
+ raise(ArgumentError, 'Purchase units and unit costs must have equal lengths.') unless units.size == price.size
134
+
135
+ [uinv, pinv, units, price, sinv, method]
136
+ end
137
+ private_class_method :inventory_inputs
138
+
132
139
  # Depreciation Expense Recognition -- double-declining balance (DDB), the most common declining balance method, which applies two times the straight-line rate to the declining balance.
133
140
  #
134
141
  # @param cost cost of long-lived assets
@@ -137,11 +144,10 @@ module Finrb
137
144
  # @example
138
145
  # Finrb::Accounting.ddb(cost=1200,rv=200,t=5)
139
146
  def self.ddb(cost:, rv:, t:)
140
- cost = Flt::DecNum(cost.to_s)
141
- rv = Flt::DecNum(rv.to_s)
142
- t = Flt::DecNum(t.to_s)
143
-
144
- raise(Error, 't should be larger than 1') if t < 2
147
+ cost = Validation.non_negative_decimal(cost, name: 'asset cost')
148
+ rv = residual_value(rv, cost:)
149
+ t = Validation.positive_integer(t, name: 'useful life')
150
+ raise(DomainError, 'Useful life must be at least 2 periods for double-declining depreciation.') if t < 2
145
151
 
146
152
  ddb = [Flt::DecNum(0)] * t
147
153
  ddb[0] = cost * 2 / t
@@ -170,11 +176,24 @@ module Finrb
170
176
  # @example
171
177
  # Finrb::Accounting.slde(cost=1200,rv=200,t=5)
172
178
  def self.slde(cost:, rv:, t:)
173
- cost = Flt::DecNum(cost.to_s)
174
- rv = Flt::DecNum(rv.to_s)
175
- t = Flt::DecNum(t.to_s)
179
+ cost = Validation.non_negative_decimal(cost, name: 'asset cost')
180
+ rv = residual_value(rv, cost:)
181
+ t = Validation.positive_decimal(t, name: 'useful life', error: DomainError)
176
182
 
177
183
  ((cost - rv) / t)
178
184
  end
185
+
186
+ def self.inventory_values(values, name:)
187
+ wrap_array(values).map { |value| Validation.non_negative_decimal(value, name:) }
188
+ end
189
+ private_class_method :inventory_values
190
+
191
+ def self.residual_value(value, cost:)
192
+ value = Validation.non_negative_decimal(value, name: 'residual value')
193
+ raise(DomainError, 'Residual value must not exceed asset cost.') if value > cost
194
+
195
+ value
196
+ end
197
+ private_class_method :residual_value
179
198
  end
180
199
  end
@@ -37,7 +37,7 @@ module Finrb
37
37
  @interest_only = interest_only
38
38
  MONETARY_ATTRIBUTES.each do |name|
39
39
  value = binding.local_variable_get(name)
40
- instance_variable_set("@#{name}", Validation.decimal(value, name: name.to_s))
40
+ instance_variable_set("@#{name}", Validation.decimal(value, name: name.to_s.tr('_', ' ')))
41
41
  end
42
42
  freeze
43
43
  end
@@ -91,16 +91,14 @@ module Finrb
91
91
  # Amortization.payment(200000, rate.monthly, rate.duration) #=> Flt::DecNum('-926.23')
92
92
  # @see https://en.wikipedia.org/wiki/Amortization_calculator
93
93
  def self.payment(principal, rate, periods, balloon: 0)
94
- principal = Validation.decimal(principal, name: 'principal')
95
- raise(ArgumentError, 'principal must be positive.') unless principal.positive?
94
+ principal = Validation.positive_decimal(principal, name: 'principal', message: 'principal must be positive.')
96
95
 
97
96
  balloon = Validation.decimal(balloon, name: 'balloon')
98
97
  raise(ArgumentError, 'balloon must be non-negative and no greater than principal.') unless balloon.between?(0, principal)
99
98
 
100
- rate = Validation.decimal(rate, name: 'rate')
101
- raise(ArgumentError, 'periodic rate must be greater than -1.') if rate <= -1
99
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'periodic rate')
102
100
 
103
- periods = Validation.positive_integer(periods, name: 'periods')
101
+ periods = Validation.positive_integer(periods, name: 'period count')
104
102
 
105
103
  if rate.zero?
106
104
  # simplified formula to avoid division-by-zero when interest rate is zero
@@ -117,11 +115,9 @@ module Finrb
117
115
  # @param [Rate] rates the applicable interest rates
118
116
  # @param [Proc] block
119
117
  def initialize(principal, *rates, balloon: 0, interest_only_periods: 0, origination_fee: 0, finance_origination_fee: false, &block)
120
- @principal = Validation.decimal(principal, name: 'principal')
121
- raise(ArgumentError, 'principal must be positive.') unless @principal.positive?
118
+ @principal = Validation.positive_decimal(principal, name: 'principal', message: 'principal must be positive.')
122
119
 
123
- @origination_fee = Validation.decimal(origination_fee, name: 'origination_fee')
124
- raise(ArgumentError, 'origination_fee must be non-negative.') if @origination_fee.negative?
120
+ @origination_fee = Validation.non_negative_decimal(origination_fee, name: 'origination fee')
125
121
  raise(ArgumentError, 'finance_origination_fee must be true or false.') unless [true, false].include?(finance_origination_fee)
126
122
  raise(ArgumentError, 'an unfinanced origination_fee must be less than principal.') if !finance_origination_fee && @origination_fee >= @principal
127
123
 
@@ -77,8 +77,7 @@ module Finrb
77
77
  validate_numeric_cashflows!
78
78
  cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }
79
79
 
80
- rate = Validation.decimal(rate, name: 'rate')
81
- raise(DomainError, 'Rate must be greater than -1.') if rate <= -1
80
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'rate', error: DomainError)
82
81
 
83
82
  total = Flt::DecNum.new(0.to_s)
84
83
  cashflows.each_with_index do |cashflow, index|
@@ -98,9 +97,8 @@ module Finrb
98
97
  cashflows = map { |entry| Validation.decimal(entry, name: 'cashflow amount') }
99
98
  raise(InvalidCashflowError, 'Cashflow needs at least one positive and one negative value.') if cashflows.none?(&:positive?) || cashflows.none?(&:negative?)
100
99
 
101
- finance_rate = Validation.decimal(finance_rate, name: 'finance_rate')
102
- reinvestment_rate = Validation.decimal(reinvestment_rate, name: 'reinvestment_rate')
103
- raise(DomainError, 'Finance and reinvestment rates must be greater than -1.') if finance_rate <= -1 || reinvestment_rate <= -1
100
+ finance_rate = Validation.decimal_greater_than(finance_rate, minimum: -1, name: 'finance rate', error: DomainError)
101
+ reinvestment_rate = Validation.decimal_greater_than(reinvestment_rate, minimum: -1, name: 'reinvestment rate', error: DomainError)
104
102
 
105
103
  last_period = cashflows.size - 1
106
104
  future_positive =
@@ -156,8 +154,7 @@ module Finrb
156
154
  # Finrb::Cashflow.xnpv(@transactions, 0.6).round(2) #=> -937.41
157
155
  def xnpv(rate)
158
156
  validate_dated_cashflows!
159
- rate = Validation.decimal(rate, name: 'rate')
160
- raise(DomainError, 'Rate must be greater than -1.') if rate <= -1
157
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'rate', error: DomainError)
161
158
 
162
159
  sum do |t|
163
160
  t.amount / ((rate + 1)**(date_diff(start, t.date) / days_in_period))
data/lib/finrb/config.rb CHANGED
@@ -41,16 +41,16 @@ module Finrb
41
41
  end
42
42
 
43
43
  def self.build_configuration(values)
44
- eps = configuration_decimal(values.fetch(:eps), name: 'eps')
45
- raise(ArgumentError, 'eps must be positive.') unless eps.positive?
44
+ eps = configuration_decimal(values.fetch(:eps), name: 'solver tolerance')
45
+ raise(ArgumentError, 'solver tolerance must be positive.') unless eps.positive?
46
46
 
47
- guess = configuration_decimal(values.fetch(:guess), name: 'guess')
48
- raise(ArgumentError, 'guess must be greater than -1.') if guess <= -1
47
+ guess = configuration_decimal(values.fetch(:guess), name: 'rate guess')
48
+ raise(ArgumentError, 'rate guess must be greater than -1.') if guess <= -1
49
49
 
50
50
  business_days = values.fetch(:business_days)
51
51
  periodic_compound = values.fetch(:periodic_compound)
52
52
  booleans = [business_days, periodic_compound].all? { |value| value.equal?(true) || value.equal?(false) }
53
- raise(ArgumentError, 'business_days and periodic_compound must be boolean.') unless booleans
53
+ raise(ArgumentError, 'business days and periodic compounding settings must be boolean.') unless booleans
54
54
 
55
55
  Configuration.new(eps:, guess:, business_days:, periodic_compound:)
56
56
  end
data/lib/finrb/rates.rb CHANGED
@@ -17,10 +17,7 @@ module Finrb
17
17
  infinite = value.infinite? if value.respond_to?(:infinite?)
18
18
  return Flt::DecNum.infinity if [true, 1].include?(infinite)
19
19
 
20
- periods = Validation.decimal(value, name: 'compounding periods')
21
- raise(ArgumentError, 'compounding periods must be positive.') unless periods.positive?
22
-
23
- periods
20
+ Validation.positive_decimal(value, name: 'compounding periods', message: 'compounding periods must be positive.')
24
21
  end
25
22
  private_class_method :compounding_periods
26
23
 
@@ -49,8 +46,7 @@ module Finrb
49
46
  # Rate.to_nominal(0.06, 365) #=> Flt::DecNum('0.05827')
50
47
  # @see https://www.miniwebtool.com/nominal-interest-rate-calculator/
51
48
  def self.to_nominal(rate, periods)
52
- rate = Validation.decimal(rate, name: 'rate')
53
- raise(ArgumentError, 'effective rate must be greater than -1.') if rate <= -1
49
+ rate = Validation.decimal_greater_than(rate, minimum: -1, name: 'effective rate')
54
50
 
55
51
  periods = compounding_periods(periods)
56
52
 
@@ -141,7 +137,7 @@ module Finrb
141
137
  end
142
138
 
143
139
  def duration=(value)
144
- @duration = Validation.positive_integer(value, name: 'duration')
140
+ @duration = Validation.positive_integer(value, name: 'duration in months')
145
141
  end
146
142
 
147
143
  # set the effective interest rate