jct 0.3.1

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 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: []