jct 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 999863b6aff0596f9de9e81a5901977188fed109b6910d13115c105fa26aeabd
4
+ data.tar.gz: 290c91deb5b83e1b061d9b4dd191e8f45f203f64e5c8ee2ae8c7eba4399ee642
5
+ SHA512:
6
+ metadata.gz: 074226647e6e2e0ecb87fe0ca08801eb8eb11c69257e834a11cc92a2a43ece7baac7c79c3a145ef6c30b6427b2eface7ba029dc5909ed4ceca20fa977920eb58
7
+ data.tar.gz: a6322bd07ab29617bacd9e7688038191f4af12da086b93c812d66e2c06873eaed7b9c6083a04f50e0767bc1a67c035eec56842c965465f69a4a7ee0ef994912a
@@ -0,0 +1,68 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths-ignore:
8
+ - 'LICENSE'
9
+ - '**.md'
10
+ pull_request:
11
+ paths-ignore:
12
+ - 'LICENSE'
13
+ - '**.md'
14
+
15
+ jobs:
16
+ test:
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os: [ubuntu-latest]
21
+ ruby: ['2.5', '2.6', '2.7', '3.1']
22
+ runs-on: ${{ matrix.os }}
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - name: Set up Ruby ${{ matrix.ruby }}
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ - name: Generate Gemfile.lock
30
+ run: bundle lock --lockfile=Gemfile.lock
31
+ - uses: actions/cache@v2
32
+ with:
33
+ path: vendor/bundle
34
+ key: bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby }}-${{ hashFiles('**/Gemfile.lock') }}
35
+ restore-keys: |
36
+ bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby }}-
37
+ - run: bundle check --path .bundle || bundle install --path .bundle
38
+ - name: MiniTest
39
+ run: bundle exec rake no_slow_test
40
+ timeout-minutes: 5
41
+
42
+ slow_test:
43
+ strategy:
44
+ fail-fast: false
45
+ matrix:
46
+ os: [ubuntu-latest]
47
+ ruby: ['2.5', '2.6', '2.7', '3.1']
48
+ runs-on: ${{ matrix.os }}
49
+ steps:
50
+ - uses: actions/checkout@v2
51
+ - name: Set up Ruby ${{ matrix.ruby }}
52
+ uses: ruby/setup-ruby@v1
53
+ with:
54
+ ruby-version: ${{ matrix.ruby }}
55
+ - name: Generate Gemfile.lock
56
+ run: bundle lock --lockfile=Gemfile.lock
57
+ - uses: actions/cache@v2
58
+ with:
59
+ path: vendor/bundle
60
+ key: bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby }}-${{ hashFiles('**/Gemfile.lock') }}
61
+ restore-keys: |
62
+ bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby }}-
63
+ - run: bundle check --path .bundle || bundle install --path .bundle
64
+ - name: MiniTest
65
+ run: |
66
+ TESTFILES=$(ls -d test/jct/slow_test/*)
67
+ bundle exec ruby -e "%w[$TESTFILES].each { |test_file| load test_file }"
68
+ timeout-minutes: 60
@@ -0,0 +1,23 @@
1
+ name: Publish gem
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v2
13
+ - name: Tag with the gem version
14
+ run: |
15
+ GEM_VERSION=$(ruby -e "require 'rubygems'; gemspec = Dir.glob(\"./**/*.gemspec\").first; puts Gem::Specification::load(gemspec).version")
16
+ TAG="v$GEM_VERSION"
17
+ git tag $TAG && git push origin $TAG
18
+ - name: Build and push gem
19
+ run: |
20
+ gem build *.gemspec
21
+ gem push ./*.gem
22
+ env:
23
+ GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'pry'
5
+ gem 'pry-power_assert'
6
+ end
7
+
8
+ group :test do
9
+ gem 'minitest'
10
+ gem 'minitest-around'
11
+ gem 'minitest-power_assert'
12
+ end
13
+
14
+ # Specify your gem's dependencies in jct.gemspec
15
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Money Forward, Inc.
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Jct
2
+ Japanese excise tax calculator
3
+
4
+ ## Installation
5
+
6
+ ```
7
+ $ gem install jct
8
+ ```
9
+
10
+ ## Usage
11
+ ```ruby
12
+ require 'jct'
13
+
14
+ today = Date.new(2014, 3, 31)
15
+ Jct.amount_with_tax(100, date: today) # => 105
16
+ Jct.rate(today) # => 1.05
17
+
18
+ Jct.amount_with_tax(100) # => 108
19
+ Jct.rate # => 1.08
20
+
21
+ # Calculate using 10% sales tax from 10/01/2019.
22
+ today = Date.new(2019, 10, 1)
23
+ Jct.amount_with_tax(100) # => 110
24
+ Jct.rate # => 1.1
25
+
26
+ Jct.amount_with_tax(999) # => 1078
27
+ Jct.amount_with_tax(999, fraction: :floor) # => 1078
28
+ Jct.amount_with_tax(999, fraction: :ceil) # => 1079
29
+ ```
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/moneyforward/jct.
34
+
35
+ ### tips
36
+ - Please separate the PR for additional features from the PR for versioning.
37
+
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ # Run all tests including slow tests
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ end
10
+
11
+ Rake::TestTask.new(:no_slow_test) do |t|
12
+ t.libs << "test"
13
+ t.libs << "lib"
14
+ t.test_files = FileList['test/jct/*_test.rb']
15
+ end
16
+
17
+ # No rake task for slow tests is prepared. (CI does not need it since ruby commands run them one at a time)
18
+
19
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "jct"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/jct.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jct/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "jct"
8
+ spec.version = Jct::VERSION
9
+ spec.authors = ["Ryo Shibuya"]
10
+ spec.email = ["shibuya.ryo@moneyforward.co.jp"]
11
+
12
+ spec.summary = %q{Japanese excise tax calculator}
13
+ spec.description = %q{Japanese excise tax calculator}
14
+ spec.homepage = "https://github.com/moneyforward/jct"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "> 1.10"
22
+ spec.add_development_dependency "rake", "> 10.0"
23
+ end
@@ -0,0 +1,3 @@
1
+ module Jct
2
+ VERSION = "0.3.1"
3
+ end
data/lib/jct.rb ADDED
@@ -0,0 +1,163 @@
1
+ require 'date'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+ require 'jct/version'
5
+
6
+ module Jct
7
+ extend self
8
+
9
+ RATE100 = 1r.freeze
10
+ RATE103 = 1.03r.freeze
11
+ RATE105 = 1.05r.freeze
12
+ RATE108 = 1.08r.freeze
13
+ RATE110 = 1.10r.freeze
14
+ EXCISE_HASHES = [
15
+ # 1873/1/1 is the date when Japan changed its calendar to the solar calendar (Meiji era).
16
+ { rate: RATE100, start_on: Date.new(1873, 1, 1), end_on: Date.new(1989, 3, 31) },
17
+ { rate: RATE103, start_on: Date.new(1989, 4, 1), end_on: Date.new(1997, 3, 31) },
18
+ { rate: RATE105, start_on: Date.new(1997, 4, 1), end_on: Date.new(2014, 3, 31) },
19
+ { rate: RATE108, start_on: Date.new(2014, 4, 1), end_on: Date.new(2019, 9, 30) },
20
+ # If we were to use Date::Infinity.new for end_on, an exception would occur in the later calculation,
21
+ # so here we will use a date far in the future.
22
+ { rate: RATE110, start_on: Date.new(2019, 10, 1), end_on: Date.new(2999, 1, 1) }
23
+ ]
24
+
25
+ private_constant :EXCISE_HASHES
26
+
27
+ def amount_with_tax(amount, date: Date.today, fraction: :truncate)
28
+ return amount if amount < 0
29
+
30
+ (BigDecimal("#{amount}") * rate(date)).__send__(fraction)
31
+ end
32
+
33
+ def yearly_amount_with_tax(amount:, start_on:, end_on:, fraction: :truncate)
34
+ # You can convert Integer/BigDecimal/Float/String/Rational classes to Rational,
35
+ # but the `amount` keyword argument does not accept BigDeciaml, Float and String for the following reasons.
36
+ # - Rational objects may be implicitly converted to BigDecimal type when performing arithmetic operations using BigDecimal and Rational.
37
+ # - Also, when you try to convert BigDecimal to Rational, the resulting value may not be Rational, but BigDecimal.
38
+ # - Float is not accepted because it is not suitable for calculating sales tax rates.
39
+ # - String is not accepted because an exception is raised by data that cannot be converted, such as 1.1.1, for example.
40
+ raise ArgumentError.new('amount data-type must be Integer or Rational') unless amount.is_a?(Integer) || amount.is_a?(Rational)
41
+ raise ArgumentError.new('start_on data-type must be Date') unless start_on.is_a?(Date)
42
+ raise ArgumentError.new('end_on data-type must be Date') unless end_on.is_a?(Date)
43
+ raise ArgumentError.new('start_on must not be after than end_on') if start_on > end_on
44
+ return amount if amount < 0
45
+
46
+ daily_amount = Rational(amount, (start_on..end_on).count)
47
+
48
+ EXCISE_HASHES.inject(0) do |sum, hash|
49
+ # It determines whether there are overlapping periods by comparing the start and end dates of a certain consumption tax with
50
+ # the start and end dates of the period for which the tax-inclusive price is to be calculated this time.
51
+ # If there is an overlap, the tax-inclusive price is calculated by multiplying the consumption tax rate for the applicable period
52
+ # by the number of days and pro rata amount for the overlapping period.
53
+ larger_start_on = [start_on, hash[:start_on]].max
54
+ smaller_end_on = [end_on, hash[:end_on]].min
55
+
56
+ # Check if there is an overlapping period
57
+ if larger_start_on <= smaller_end_on
58
+ # Number of days of overlapping period
59
+ number_of_days_in_this_excise_rate_term = (larger_start_on..smaller_end_on).count
60
+
61
+ sum += (daily_amount * number_of_days_in_this_excise_rate_term * hash[:rate]).__send__(fraction)
62
+ end
63
+
64
+ sum
65
+ end
66
+ end
67
+
68
+ # Takes the amount and period and returns a HASH with the amount divided by the sales tax period.
69
+ # e.g. 1000, Date.new(1997, 3, 31), Date.new(1997, 4, 9)
70
+ # => { Jct::RATE103 => 100, Jct::RATE105 => 900 }
71
+ #
72
+ # MEMO: This method does not perform sales tax calculations
73
+ # For example, if there is an amount to which the 8% tax rate applies and an amount to which the 10% tax rate applies,
74
+ # and there are other charges that should be combined (e.g., the annual basic fee and the optional fee),
75
+ # if this method returns the amount including tax, it cannot be combined with the other charges.
76
+ def amount_separated_by_rate(amount:, start_on:, end_on:)
77
+ # You can convert Integer/BigDecimal/Float/String/Rational classes to Rational,
78
+ # but the `amount` keyword argument does not accept BigDeciaml, Float and String in for the following reasons.
79
+ # - Rational objects may be implicitly converted to BigDecimal or Float type
80
+ # when performing arithmetic operations using BigDecimal and Rational, or Float and Rational.
81
+ # - String is not accepted because an exception is raised by data that cannot be converted, such as 1.1.1, for example.
82
+ raise ArgumentError.new('amount data-type must be Integer or Rational') unless amount.is_a?(Integer) || amount.is_a?(Rational)
83
+ raise ArgumentError.new('start_on data-type must be Date') unless start_on.is_a?(Date)
84
+ raise ArgumentError.new('end_on data-type must be Date') unless end_on.is_a?(Date)
85
+
86
+ # By using the modified Julian date, we can handle all Date as Integer. This speeds up the process.
87
+ start_on_mjd = start_on.mjd
88
+ end_on_mjd = end_on.mjd
89
+
90
+ raise ArgumentError.new('start_on must not be after than end_on') if start_on_mjd > end_on_mjd
91
+ raise ArgumentError.new('start_on must bigger than 1873/1/1') if start_on_mjd < EXCISE_HASHES.first[:start_on].mjd
92
+ raise ArgumentError.new('amount must be greater than or equal to zero') if amount < 0
93
+
94
+ # Use the number of days until end_on_mjd.
95
+ daily_amount = Rational(amount, (start_on_mjd..end_on_mjd).count)
96
+
97
+ {}.tap do |return_hash|
98
+ EXCISE_HASHES.inject(0) do |sum, hash|
99
+ # It determines whether there are overlapping periods by comparing the start and end dates of a certain consumption tax with
100
+ # the start and end dates of the period for which the tax-inclusive price is to be calculated this time.
101
+ # If there is an overlap, the price for the subject period is calculated by multiplying the number of days of the overlapping period
102
+ # by the pro rata amount.
103
+ larger_start_on_mjd = [start_on_mjd, hash[:start_on].mjd].max
104
+ smaller_end_on_mjd = [end_on_mjd, hash[:end_on].mjd].min
105
+
106
+ # Check if there is an overlapping period
107
+ if larger_start_on_mjd <= smaller_end_on_mjd
108
+ # Number of days of overlapping period
109
+ number_of_days_in_this_excise_rate_term = (larger_start_on_mjd..smaller_end_on_mjd).count
110
+ return_hash[hash[:rate]] = (daily_amount * number_of_days_in_this_excise_rate_term).truncate
111
+ end
112
+ end
113
+
114
+ # If the divided amount is not divisible by the number of target tax rates,
115
+ # the sum of the amount in the argument and the divided amount may be less than the actual value.
116
+ # This is because the undivided value is truncated at the time of division.
117
+ # e.g.
118
+ # amount: 100000, start_on: 1997/3/31, end_on 2014/4/1の場合
119
+ # 3%:16
120
+ # 5%:99_967
121
+ # 8%:16
122
+ # => 16+99967+16=99999
123
+ # Add the amount that is out of alignment to the amount that belongs to the lowest sales tax amount
124
+ # to equal the sum of the argument amount and the divided amount.
125
+ # The reason for adding the shortfall to the amount that belongs to the least amount of consumption tax
126
+ # is so that the user will have an advantage when the consumption tax is calculated based on this amount.
127
+ # Example 1
128
+ # amount: 100000, start_on: 1997/3/31, end_on 2014/4/1の場合
129
+ # 3%:17 <- Actually 16, but add 1 yen.
130
+ # 5%:99_967
131
+ # 8%:16
132
+ # => 17+99967+16=100000
133
+ #
134
+ # Example 2:
135
+ # amount: 100000, start_on: 2014/3/31, end_on 2019/10/1の場合
136
+ # 5%:51 <- Actually 49, but add 2 yen.
137
+ # 8%:99_900
138
+ # 10%:49
139
+ # => 51+99900+49=100000
140
+ #
141
+ # FIXME: `Enumerable#sum` has been supported since ruby 2.4, but this gem uses `reduce` because it still needs to support ruby 2.3 series.
142
+ summarize_separated_amount = return_hash.each_value.reduce(&:+)
143
+ if amount != summarize_separated_amount
144
+ return_hash[return_hash.each_key.min] += (amount - summarize_separated_amount)
145
+ end
146
+ end
147
+ end
148
+
149
+ def rate(date = Date.today)
150
+ case date
151
+ when Date.new(1989, 4, 1)..Date.new(1997, 3, 31)
152
+ RATE103
153
+ when Date.new(1997, 4, 1)..Date.new(2014, 3, 31)
154
+ RATE105
155
+ when Date.new(2014, 4, 1)..Date.new(2019, 9, 30)
156
+ RATE108
157
+ when Date.new(2019, 10, 1)..Date::Infinity.new
158
+ RATE110
159
+ else
160
+ RATE100
161
+ end
162
+ end
163
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Ryo Shibuya
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: Japanese excise tax calculator
42
+ email:
43
+ - shibuya.ryo@moneyforward.co.jp
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".github/workflows/ci.yml"
49
+ - ".github/workflows/release.yml"
50
+ - ".gitignore"
51
+ - Gemfile
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - bin/console
56
+ - bin/setup
57
+ - jct.gemspec
58
+ - lib/jct.rb
59
+ - lib/jct/version.rb
60
+ homepage: https://github.com/moneyforward/jct
61
+ licenses: []
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.1.2
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Japanese excise tax calculator
82
+ test_files: []