cashify 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +55 -0
- data/LICENSE.txt +21 -0
- data/README.md +227 -0
- data/Rakefile +14 -0
- data/bin/console +13 -0
- data/bin/setup +8 -0
- data/lib/cashify/arithmetic.rb +83 -0
- data/lib/cashify/cashify.rb +25 -0
- data/lib/cashify/errors.rb +11 -0
- data/lib/cashify/version.rb +3 -0
- data/lib/cashify.rb +91 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0d31e15d5b41014afa8408f2eb88f2951ceb129c8b1f189c0fb4783d506a9e99
|
4
|
+
data.tar.gz: 3d88a0eab4b01d2dbfb4ea67cc32c26e255d677f6d71ea34b056fcaca426a3ef
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4fc2adb94912de958291b7a5aa878a89cde682aa1dad935136576e4df4a9705570b7bfc85fbeadb027e9d0478de0b269e4482fbefbbe664f0914c2d8b83247ac
|
7
|
+
data.tar.gz: 8dac497d6a56923a1d7dadfe96ef64dba8e5fb7aba33b1278e392b8759b83c4ee0f6515a8c7e55c8a0ac7d948b4298cbc2e210449415bc93b19fecafc7377b14
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.0.0
|
3
|
+
NewCops: enable
|
4
|
+
|
5
|
+
Layout/LineLength:
|
6
|
+
Max: 120
|
7
|
+
|
8
|
+
Metrics/CyclomaticComplexity:
|
9
|
+
Max: 8
|
10
|
+
|
11
|
+
Metrics/MethodLength:
|
12
|
+
Max: 16
|
13
|
+
|
14
|
+
Style/Documentation:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/FrozenStringLiteralComment:
|
18
|
+
EnforcedStyle: never
|
19
|
+
|
20
|
+
Style/StringLiterals:
|
21
|
+
Enabled: true
|
22
|
+
EnforcedStyle: double_quotes
|
23
|
+
|
24
|
+
Style/StringLiteralsInInterpolation:
|
25
|
+
Enabled: true
|
26
|
+
EnforcedStyle: double_quotes
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-3.0.2
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
cashify (0.9.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.2)
|
10
|
+
coderay (1.1.3)
|
11
|
+
method_source (1.0.0)
|
12
|
+
minitest (5.14.4)
|
13
|
+
parallel (1.21.0)
|
14
|
+
parser (3.0.2.0)
|
15
|
+
ast (~> 2.4.1)
|
16
|
+
pry (0.14.1)
|
17
|
+
coderay (~> 1.1)
|
18
|
+
method_source (~> 1.0)
|
19
|
+
rainbow (3.0.0)
|
20
|
+
rake (13.0.6)
|
21
|
+
regexp_parser (2.1.1)
|
22
|
+
rexml (3.2.5)
|
23
|
+
rubocop (1.22.1)
|
24
|
+
parallel (~> 1.10)
|
25
|
+
parser (>= 3.0.0.0)
|
26
|
+
rainbow (>= 2.2.2, < 4.0)
|
27
|
+
regexp_parser (>= 1.8, < 3.0)
|
28
|
+
rexml
|
29
|
+
rubocop-ast (>= 1.12.0, < 2.0)
|
30
|
+
ruby-progressbar (~> 1.7)
|
31
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
32
|
+
rubocop-ast (1.12.0)
|
33
|
+
parser (>= 3.0.1.1)
|
34
|
+
rubocop-minitest (0.15.1)
|
35
|
+
rubocop (>= 0.90, < 2.0)
|
36
|
+
rubocop-rake (0.6.0)
|
37
|
+
rubocop (~> 1.0)
|
38
|
+
ruby-progressbar (1.11.0)
|
39
|
+
unicode-display_width (2.1.0)
|
40
|
+
|
41
|
+
PLATFORMS
|
42
|
+
x86_64-darwin-19
|
43
|
+
x86_64-linux
|
44
|
+
|
45
|
+
DEPENDENCIES
|
46
|
+
cashify!
|
47
|
+
minitest (~> 5.0)
|
48
|
+
pry
|
49
|
+
rake (~> 13.0)
|
50
|
+
rubocop
|
51
|
+
rubocop-minitest
|
52
|
+
rubocop-rake
|
53
|
+
|
54
|
+
BUNDLED WITH
|
55
|
+
2.2.26
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Johan Halse
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
[![Main](https://github.com/johanhalse/cashify/actions/workflows/main.yml/badge.svg)](https://github.com/johanhalse/cashify/actions/workflows/main.yml)
|
2
|
+
|
3
|
+
# Cashify
|
4
|
+
|
5
|
+
Ever had to work with money in more than one currency? Ever wish you could do stuff like add them to one another without getting murdered by exceptions? Here's the gem for you.
|
6
|
+
|
7
|
+
![Cashify](https://media4.giphy.com/media/EnoO73pTnn99JrnRR3/giphy.gif)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem "cashify"
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle install
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install cashify
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Cashify is a money handling gem that's inherently nice to currencies. It lets you treat your money as mostly numbers, freeing you from a lot of headache around manipulating and handling it. Instantiate a new `Cash` object like so:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
puts Cash.new(SEK: 100_00)
|
31
|
+
# 100 SEK
|
32
|
+
```
|
33
|
+
|
34
|
+
Add another currency? Sure.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
puts Cash.new(EUR: 100_00, SEK: 100_00)
|
38
|
+
# 100 EUR, 100 SEK
|
39
|
+
```
|
40
|
+
|
41
|
+
And then maybe do some addition or subtraction?
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
puts Cash.new(SEK: 100_00) + Cash.new(USD: 100_00) + Cash.new(USD: 50_00)
|
45
|
+
# 100 SEK, 150 USD
|
46
|
+
```
|
47
|
+
|
48
|
+
Perhaps add a little discount here or there?
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
puts (Cash.new(SEK: 100_00) + Cash.new(USD: 100_00)) * 0.9
|
52
|
+
# 90 SEK, 90 USD
|
53
|
+
```
|
54
|
+
|
55
|
+
Increase the values a little? Why not!
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
puts (Cash.new(SEK: 100_00, USD: 100_00) + 20
|
59
|
+
# 120 SEK, 120 USD
|
60
|
+
```
|
61
|
+
|
62
|
+
### What. You can't just add integers to currencies
|
63
|
+
|
64
|
+
I can, in fact, totally add integers to currencies. And now you can, too! Also subtract, divide, multiply... look: this probably sounds bad to you. Foreign, somehow. Your parents raised you well, and you've already used the ubiquitous [Money](https://github.com/RubyMoney/money) gem and tried to add a shipping cost or something. The Money gem then told you to stop immediately and stomped off into Exception Land, leaving you empty-handed and full of regret.
|
65
|
+
|
66
|
+
Or maybe you wanted to work with zero? You'll get answers on the Internet saying things like "aha but what would zero even mean when using a currency" and my answer to that would be "well at least 0 USD is the same as 0 SEK, right?" and so you can casually do `Cash.new(SEK: 0) + Cash.new(USD: 100)` and get `100 USD` back and then imagine these pedants popping a vein and dying. A zero is a zero. Don't try to tell me otherwise.
|
67
|
+
|
68
|
+
### Can you _really_ compare currencies that way though?
|
69
|
+
|
70
|
+
Well, I guess we can! And we're not even done. Check this out:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
Cash.new(SEK: 100_00) > Cash.new(USD: 50_00)
|
74
|
+
# true
|
75
|
+
```
|
76
|
+
|
77
|
+
Is this right or wrong? Who even knows at this point. But it's _useful_, and that's all we care about! I'll agree that the comparison is super naive, and you can Freedom Patch that if you like, or go the Money Gem route and pipe those objects through your own functions. But you see where we're going, right? Wouldn't it be nice to just grab various sums of currencies from your database, cast them to Cash objects, and have them just sort of _work_ without putting up guardrails everywhere? Yes it would. The default assumption of "when you try to add one money to another money that probably means you want a conversion somewhere" is wrong in most cases. Also, initialization matters! Cashify is very database friendly. Imagine a Rails app with `Purchase` objects that all have a cost. Then look at this beauty:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
Cash.new Purchase.all.group(:cost_currency).sum(:cost_cents)
|
81
|
+
```
|
82
|
+
|
83
|
+
That piece of code pulls _however many objects in however many currencies_ and tallies them up in SQL, returning a useful Cash object containing something along the lines of `1100 EUR, 1400 SEK, 2155 USD`. If you want, you can smack a multiplier on those and add a discount. Or add a processing fee of `100 SEK`. You be the boss.
|
84
|
+
|
85
|
+
### This sounds dangerous and illegal, shouldn't these operations throw exceptions?
|
86
|
+
|
87
|
+
No, they probably shouldn't. The Money Gem school of thought says that every time you do something potentially hazardous to your money — like try to add an integer, not caring which currency you're dealing with — there's an exception, and then you'll have to handle that exception in a way that makes sense for your use case. I do see the logic, but in practice it gets old REAL fast. You'll be stuck in a never-ending shouting match with your money objects, having to drag them kicking and screaming through a bunch of guard statements in order to do simple things like addition. If you're only dealing with a single currency it's not too bad, but as soon as you add another one to the mix the noise level is deafening. And honestly: if you're only dealing with a single currency, why are you even using a money gem? You could use fixed point integers instead, pushing any currency-specific stuff to the boundaries of your code.
|
88
|
+
|
89
|
+
This in mind, Cashify posits that if you want to get these things right, you should be treating your money as simple numbers as often as possible. Currencies are a necessary evil but they're not your overarching concern, and this gem will allow them to lurk in the background where they belong. Throw those Cash objects around, stuff them into arrays, collide them in interesting ways. Swim around in your money like you're Scrooge McDuck and let your database do most of the work for you! It'll feel great, I promise.
|
90
|
+
|
91
|
+
### Do you handle displaying currencies nicely, too?
|
92
|
+
|
93
|
+
Oof, sounds hard. Out of scope. Write a view helper or something.
|
94
|
+
|
95
|
+
### And when you actually do want to convert currencies?
|
96
|
+
|
97
|
+
There are plenty of gems for that and it's honestly pretty easy. But you'll have to write your own code for it.
|
98
|
+
|
99
|
+
## API
|
100
|
+
|
101
|
+
#### `.positive?` and `.negative?`
|
102
|
+
|
103
|
+
Returns true if all currencies are positive/negative.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
Cash.new(SEK: 100_00, USD: 100_00).positive?
|
107
|
+
# true
|
108
|
+
|
109
|
+
Cash.new(SEK: 100_00, USD: -1_00).positive?
|
110
|
+
# false
|
111
|
+
|
112
|
+
Cash.new(SEK: -100_00, USD: -100_00).negative?
|
113
|
+
# true
|
114
|
+
```
|
115
|
+
|
116
|
+
#### `.zero?`
|
117
|
+
|
118
|
+
Returns true if all currencies are zero.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
Cash.new(SEK: 0).zero?
|
122
|
+
# true
|
123
|
+
|
124
|
+
Cash.new(SEK: 0, USD: 1_00).zero?
|
125
|
+
# false
|
126
|
+
```
|
127
|
+
|
128
|
+
#### `.abs`
|
129
|
+
|
130
|
+
Makes all values positive.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Cash.new(SEK: -100_00, USD: 1_00).abs
|
134
|
+
# 100 SEK, 1 USD
|
135
|
+
```
|
136
|
+
|
137
|
+
#### `.zero`
|
138
|
+
|
139
|
+
Shorthand for a zero currency. Use it instead of `Cash.new(DKK: 0)` because having to specify currency for zero feels wordy and awkward.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
Cash.zero
|
143
|
+
# 0 USD
|
144
|
+
```
|
145
|
+
|
146
|
+
#### `.to_a`
|
147
|
+
|
148
|
+
Splits all the currencies out into an array of `Cash` objects.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
Cash.new(EUR: 100_00, SEK: 100_00, USD: 100_00).to_a
|
152
|
+
# [Cash(EUR: 100_00), Cash(SEK: 100_00), Cash(USD: 100_00)]
|
153
|
+
```
|
154
|
+
|
155
|
+
#### `.to_s`
|
156
|
+
|
157
|
+
Get a simple string back.
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
Cash.new(EUR: 100_00, SEK: 100_00, USD: 100_00).to_s
|
161
|
+
# "100 EUR, 100 SEK, 100 USD"
|
162
|
+
```
|
163
|
+
|
164
|
+
#### `.sum`
|
165
|
+
|
166
|
+
You'll be working with arrays of Cash objects a lot. You should absolutely be using Ruby's excellent array methods on them, but unfortunately `cash_array.sum` won't work. Ruby's `sum` method injects a `0` as the first value, and even though `Cash.new(USD: 100) + 0` is super valid and nice, `0 + Cash.new(USD: 100)` will try to coerce your Cash object into an integer which just won't work. So if you want to sum an array of Cash, you can instead use:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
Cash.sum [Cash.new(EUR: 100_00), Cash.new(SEK: 100_00), Cash.new(EUR: 100_00)]
|
170
|
+
# 200 EUR, 100 SEK
|
171
|
+
```
|
172
|
+
|
173
|
+
#### `.empty?`
|
174
|
+
|
175
|
+
Returns true if there are no currencies in the object.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
Cash.new.empty?
|
179
|
+
# true
|
180
|
+
|
181
|
+
Cash.new(NOK: 100_00).empty?
|
182
|
+
# false
|
183
|
+
```
|
184
|
+
|
185
|
+
#### `.round`
|
186
|
+
|
187
|
+
Rounds currencies to a power interval.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
Cash.new(EUR: 100_50, SEK: 122_25).round(100)
|
191
|
+
# 101 EUR, 122 SEK
|
192
|
+
|
193
|
+
Cash.new(EUR: 100_50, SEK: 122_25).round(1000)
|
194
|
+
# 100 EUR, 120 SEK
|
195
|
+
```
|
196
|
+
|
197
|
+
#### `.currency`
|
198
|
+
|
199
|
+
Returns the first currency in the Cash object as a symbol
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
Cash.new(NOK: 100).currency
|
203
|
+
# :NOK
|
204
|
+
```
|
205
|
+
|
206
|
+
#### `.value`
|
207
|
+
|
208
|
+
Returns the first value in the Cash object.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
Cash.new(NOK: 100).value
|
212
|
+
# 100
|
213
|
+
```
|
214
|
+
|
215
|
+
## Development
|
216
|
+
|
217
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
218
|
+
|
219
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
220
|
+
|
221
|
+
## Contributing
|
222
|
+
|
223
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/johanhalse/cashify.
|
224
|
+
|
225
|
+
## License
|
226
|
+
|
227
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
end
|
9
|
+
|
10
|
+
require "rubocop/rake_task"
|
11
|
+
|
12
|
+
RuboCop::RakeTask.new
|
13
|
+
|
14
|
+
task default: %i[test rubocop]
|
data/bin/console
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "bundler/setup"
|
3
|
+
require "cashify"
|
4
|
+
|
5
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
6
|
+
# with your gem easier. You can also use a different console, if you like.
|
7
|
+
|
8
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
9
|
+
# require "pry"
|
10
|
+
# Pry.start
|
11
|
+
|
12
|
+
require "irb"
|
13
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
class Cash
|
2
|
+
module Arithmetic
|
3
|
+
def +(other)
|
4
|
+
raise Cash::Errors::AdditionError unless other.respond_to?(:zero?)
|
5
|
+
return self unless present?(other)
|
6
|
+
return self if other.zero?
|
7
|
+
return other if zero?
|
8
|
+
|
9
|
+
return Cash.new(**add_currencies(other)) if other.is_a?(Cash)
|
10
|
+
|
11
|
+
Cash.new(**add_number(other)) if other.is_a?(Integer) || other.is_a?(Float)
|
12
|
+
end
|
13
|
+
|
14
|
+
def -(other)
|
15
|
+
raise Cash::Errors::SubtractionError unless other.respond_to?(:zero?)
|
16
|
+
return self unless present?(other)
|
17
|
+
return Cash.new(**subtract_currencies(other)) if other.is_a?(Cash) || other.zero?
|
18
|
+
|
19
|
+
Cash.new(**subtract_number(other)) if other.is_a?(Integer) || other.is_a?(Float)
|
20
|
+
end
|
21
|
+
|
22
|
+
def *(other)
|
23
|
+
return Cash.new(**multiply_number(other)) if other.is_a?(Integer) || other.is_a?(Float)
|
24
|
+
return Cash.new(**multiply_currencies(other)) if other.is_a?(Cash)
|
25
|
+
|
26
|
+
raise Cash::Errors::MultiplicationError
|
27
|
+
end
|
28
|
+
|
29
|
+
def /(other)
|
30
|
+
return Cash.new(**divide_number(other)) if other.is_a?(Integer) || other.is_a?(Float)
|
31
|
+
return Cash.new(**divide_currencies(other)) if other.is_a?(Cash)
|
32
|
+
|
33
|
+
raise Cash::Errors::DivisionError
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def present?(other)
|
39
|
+
!(other.nil? || other == "" || other == false)
|
40
|
+
end
|
41
|
+
|
42
|
+
def multiply_number(other)
|
43
|
+
currencies.transform_values { |v| v * other }
|
44
|
+
end
|
45
|
+
|
46
|
+
def multiply_currencies(other)
|
47
|
+
currencies.merge(other.currencies) { |_k, a, b| a * b }
|
48
|
+
end
|
49
|
+
|
50
|
+
def divide_number(other)
|
51
|
+
currencies.transform_values { |v| v / other }
|
52
|
+
end
|
53
|
+
|
54
|
+
def divide_currencies(other)
|
55
|
+
currencies.merge(other.currencies) { |_k, a, b| a / b }
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_currencies(other)
|
59
|
+
currencies.merge(other.currencies) { |_k, a, b| a + b }
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_number(other)
|
63
|
+
currencies.transform_values { |v| v + other }
|
64
|
+
end
|
65
|
+
|
66
|
+
def subtract_number(other)
|
67
|
+
currencies.transform_values { |v| v - other }
|
68
|
+
end
|
69
|
+
|
70
|
+
def subtract_currencies(other) # rubocop:disable Metrics/AbcSize
|
71
|
+
return currencies unless other.respond_to?(:zero?)
|
72
|
+
return currencies if other.zero?
|
73
|
+
return other.currencies.transform_values { |v| 0 - v } if zero?
|
74
|
+
|
75
|
+
(@currencies.keys + other.currencies.keys).uniq.map do |k|
|
76
|
+
minuend = @currencies[k] || 0
|
77
|
+
subtrahend = other.currencies[k] || 0
|
78
|
+
|
79
|
+
[k, minuend - subtrahend]
|
80
|
+
end.to_h
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Cash
|
2
|
+
module Cashify
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def self.cashify(*fields)
|
7
|
+
fields.each do |field_name|
|
8
|
+
cents_field = "#{field_name}_cents"
|
9
|
+
currency_field = "#{field_name}_currency"
|
10
|
+
|
11
|
+
define_method(field_name) do
|
12
|
+
return nil if read_attribute(currency_field).nil? || read_attribute(cents_field).nil?
|
13
|
+
|
14
|
+
Cash.new(read_attribute(currency_field) => read_attribute(cents_field))
|
15
|
+
end
|
16
|
+
|
17
|
+
define_method("#{field_name}=") do |cash|
|
18
|
+
write_attribute(cents_field, cash.value)
|
19
|
+
write_attribute(currency_field, cash.currency)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/cashify.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative "cashify/version"
|
2
|
+
require_relative "cashify/arithmetic"
|
3
|
+
require_relative "cashify/errors"
|
4
|
+
|
5
|
+
if defined?(::Rails::Railtie)
|
6
|
+
require_relative "cashify/cashify"
|
7
|
+
|
8
|
+
class CashifyRailtie < Rails::Railtie
|
9
|
+
initializer "cashify.configure_rails_initialization" do
|
10
|
+
ActiveSupport.on_load(:active_record) do
|
11
|
+
::ActiveRecord::Base.include(Cash::Cashify)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Cash
|
18
|
+
include Cash::Arithmetic
|
19
|
+
include Cash::Errors
|
20
|
+
include Comparable
|
21
|
+
|
22
|
+
attr_accessor :currencies
|
23
|
+
|
24
|
+
def self.zero
|
25
|
+
new(USD: 0)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.sum(cashes)
|
29
|
+
cashes.inject(Cash.zero, :+)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(**currencies)
|
33
|
+
@currencies = currencies
|
34
|
+
.transform_keys(&:to_sym)
|
35
|
+
.transform_values(&:to_i)
|
36
|
+
.sort.to_h
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
return currencies == other.currencies || (zero? && other.zero?) if other.is_a?(Cash)
|
41
|
+
|
42
|
+
zero? && (other.respond_to?(:zero?) && other.zero?)
|
43
|
+
end
|
44
|
+
|
45
|
+
def zero?
|
46
|
+
currencies.values.all?(&:zero?)
|
47
|
+
end
|
48
|
+
|
49
|
+
def positive?
|
50
|
+
currencies.values.all?(&:positive?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def negative?
|
54
|
+
currencies.values.all?(&:negative?)
|
55
|
+
end
|
56
|
+
|
57
|
+
def <=>(other)
|
58
|
+
other_sum = other if other.is_a?(Integer) || other.is_a?(Float)
|
59
|
+
other_sum = other.currencies.values.sum if other.is_a?(Cash)
|
60
|
+
|
61
|
+
currencies.values.sum <=> other_sum
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_a
|
65
|
+
currencies.map { |k, v| Cash.new(k => v) }
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
currencies.map { |k, v| "#{(v * 0.01).round} #{k}" }.join(", ")
|
70
|
+
end
|
71
|
+
|
72
|
+
def currency
|
73
|
+
currencies.keys.first
|
74
|
+
end
|
75
|
+
|
76
|
+
def value
|
77
|
+
currencies.values.first
|
78
|
+
end
|
79
|
+
|
80
|
+
def empty?
|
81
|
+
currencies.empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
def abs
|
85
|
+
Cash.new(**currencies.transform_values(&:abs))
|
86
|
+
end
|
87
|
+
|
88
|
+
def round(interval = 100)
|
89
|
+
Cash.new(**currencies.transform_values { |v| (v / interval.to_f).round * interval })
|
90
|
+
end
|
91
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cashify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Johan Halse
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-08 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Money trouble no more! Add, subtract, and have fun with money in different
|
14
|
+
currencies.
|
15
|
+
email:
|
16
|
+
- johan@hal.se
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- ".ruby-version"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- Gemfile
|
25
|
+
- Gemfile.lock
|
26
|
+
- LICENSE.txt
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- bin/console
|
30
|
+
- bin/setup
|
31
|
+
- lib/cashify.rb
|
32
|
+
- lib/cashify/arithmetic.rb
|
33
|
+
- lib/cashify/cashify.rb
|
34
|
+
- lib/cashify/errors.rb
|
35
|
+
- lib/cashify/version.rb
|
36
|
+
homepage: https://github.com/johanhalse/cashify
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata:
|
40
|
+
allowed_push_host: https://rubygems.org
|
41
|
+
homepage_uri: https://github.com/johanhalse/cashify
|
42
|
+
source_code_uri: https://github.com/johanhalse/cashify
|
43
|
+
changelog_uri: https://github.com/johanhalse/cashify/CHANGELOG.md
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 3.0.0
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
requirements: []
|
59
|
+
rubygems_version: 3.2.22
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: A sensible way to handle money
|
63
|
+
test_files: []
|