├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── age_jp.gemspec ├── lib ├── age_jp.rb └── age_jp │ ├── calculator.rb │ ├── core_ext │ └── date.rb │ └── version.rb └── spec ├── age_jp_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.sw* 16 | .rspec 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.8 4 | - 2.4.5 5 | - 2.5.5 6 | - 2.6.2 7 | before_install: 8 | - gem update --system 9 | - gem install bundler -v 1.16.1 10 | cache: bundler 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in age_jp.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 ryoff 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AgeJp 2 | 3 | [![Build Status](https://travis-ci.org/ryoff/age_jp.svg?branch=master)](https://travis-ci.org/ryoff/age_jp) 4 | 5 | 日本の法律に則った年齢計算です 6 | [年齢計算ニ関スル法律 - Wikipedia](http://ja.wikipedia.org/wiki/%E5%B9%B4%E9%BD%A2%E8%A8%88%E7%AE%97%E3%83%8B%E9%96%A2%E3%82%B9%E3%83%AB%E6%B3%95%E5%BE%8B) 7 | 8 | >年を取る日は誕生日の前日となる 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | gem 'age_jp' 16 | ``` 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install age_jp 25 | 26 | ## Usage 27 | 28 | ```lang:age_jp.rb 29 | Timecop.freeze(Time.new(2014, 12, 31)) 30 | 31 | birthday = Date.new(2000, 1, 1) 32 | birthday.age # 14 (通常の年齢) 33 | birthday.age_at(Date.today) # same as birthday.age 34 | birthday.age_jp # 15 (日本の法律準拠) 35 | birthday.age_jp_at(Date.today) # same as birthday.age_jp 36 | birthday.east_asian_age_reckoning # 15 (数え年) 37 | birthday.east_asian_age_reckoning_at(Date.today) # same as birthday.east_asian_age_reckoning 38 | birthday.insurance_age # 14 (保険年齢) 39 | birthday.insurance_age_at(Date.today) # same as birthday.insurance_age 40 | birthday.to_years_old(17) # 2017/01/01. 17歳の誕生日を返却 41 | birthday.to_years_old_jp(17) # 2016/12/31. 17歳の年齢加算日(日本の法律準拠)を返却 42 | ``` 43 | 44 | ## Contributing 45 | 46 | 1. Fork it ( https://github.com/[my-github-username]/age_jp/fork ) 47 | 2. Create your feature branch (`git checkout -b my-new-feature`) 48 | 3. Commit your changes (`git commit -am 'Add some feature'`) 49 | 4. Push to the branch (`git push origin my-new-feature`) 50 | 5. Create a new Pull Request 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /age_jp.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'age_jp/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "age_jp" 8 | spec.version = AgeJp::VERSION 9 | spec.authors = ["ryoff"] 10 | spec.email = ["ryoffes@gmail.com"] 11 | spec.summary = %q{A simpel age calculator for a law of Japan.} 12 | spec.description = %q{A simpel age calculator for a law of Japan.} 13 | spec.homepage = "https://github.com/ryoff/age_jp" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "activesupport", '>= 4.1' 22 | 23 | spec.add_development_dependency "bundler", ">= 1.0" 24 | spec.add_development_dependency "rake", ">= 10.0" 25 | spec.add_development_dependency "rspec", '~> 3.0' 26 | end 27 | -------------------------------------------------------------------------------- /lib/age_jp.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'age_jp/version' 3 | require 'age_jp/calculator' 4 | require 'age_jp/core_ext/date' 5 | require 'active_support' 6 | require 'active_support/core_ext' 7 | -------------------------------------------------------------------------------- /lib/age_jp/calculator.rb: -------------------------------------------------------------------------------- 1 | module AgeJp 2 | class Calculator 3 | attr_reader :birthday 4 | 5 | def initialize(birthday) 6 | @birthday = birthday 7 | end 8 | 9 | def age_at(date = Date.current) 10 | return unless valid_birthday? && valid_date?(date) 11 | 12 | calculate_age(date) 13 | end 14 | 15 | def age_jp_at(date = Date.current) 16 | return unless valid_birthday? && valid_date?(date) 17 | 18 | calculate_age_jp(date) 19 | end 20 | 21 | def east_asian_age_reckoning_at(date = Date.current) 22 | return unless valid_birthday? && valid_date?(date) 23 | 24 | age = calculate_age(date) 25 | until_birthday_this_year?(date) ? age + 2 : age + 1 26 | end 27 | 28 | def insurance_age_at(date = Date.current) 29 | return unless valid_birthday? && valid_date?(date) 30 | 31 | # date時点での満年齢を取得 32 | age = calculate_age(date) 33 | 34 | # その年齢に到達した誕生日を取得 35 | last_birthday = birthday.next_year(age) 36 | 37 | # 前回の誕生日から計算基準日までの月差分を取得 38 | month_diff = (date.year * 12 + date.month) - (last_birthday.year * 12 + last_birthday.month) 39 | 40 | # 月差分だけlast_birthdayをmonths_sinceした日付と、dateを比較し、month_diffの値を調整する 41 | # ex) 42 | # - last_birthday: 1/25 43 | # - date: 2/15 44 | # の場合、month_diffは、1だが、実際には0ヶ月差分としたい 45 | # - last_birthday.months_since(month_diff) => 2/25 46 | # として、date < last_birthday.months_since(month_diff) の場合は、month_diff -= 1 を月差分とする 47 | # 48 | # last_birthday date month_diff months_since後 adjusted_month_diff 49 | # 2/25 2/27 0 2/25 0 50 | # 2/25 3/1 1 3/25 0 51 | # 2/25 3/27 1 3/25 1 52 | # 2/25 4/1 2 4/25 1 53 | # 2/25 4/27 2 4/25 2 54 | month_diff -= 1 if date < last_birthday.months_since(month_diff) 55 | 56 | age += 1 if month_diff > 6 57 | 58 | age 59 | end 60 | 61 | private 62 | 63 | def calculate_age_jp(date) 64 | # 誕生日が閏日の場合は、日本の民法ではdateが閏年であろうとなかろうと、2/28に年齢加算される 65 | # つまり、誕生日が閏日 かつ dateが2/27の場合は、閏年であろうと無かろうと、年齢加算しない 66 | return calculate_age(date) if leap_date?(birthday) && february_twenty_seven?(date) 67 | 68 | (calculate_age(date) - calculate_age(date.tomorrow)).zero? ? calculate_age(date) : calculate_age(date.tomorrow) 69 | end 70 | 71 | def calculate_age(date) 72 | date_ymd_to_i = date.strftime('%Y%m%d').to_i 73 | birthday_ymd_to_i = birthday.strftime('%Y%m%d').to_i 74 | 75 | # 誕生日が閏日 かつ dateが閏年ではない場合 76 | birthday_ymd_to_i -= 1 if leap_date?(birthday) && !date.leap? 77 | 78 | (date_ymd_to_i - birthday_ymd_to_i) / 10_000 79 | end 80 | 81 | def until_birthday_this_year?(date) 82 | date.strftime('%m%d').to_i < birthday.strftime('%m%d').to_i 83 | end 84 | 85 | def valid_birthday? 86 | valid_date?(birthday) 87 | end 88 | 89 | def valid_date?(date) 90 | raise ArgumentError, 'invalid date' unless date && date.is_a?(Date) 91 | 92 | true 93 | end 94 | 95 | def leap_date?(date) 96 | date.leap? && date.month == 2 && date.day == 29 97 | end 98 | 99 | def february_twenty_seven?(date) 100 | date.month == 2 && date.day == 27 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/age_jp/core_ext/date.rb: -------------------------------------------------------------------------------- 1 | class Date 2 | def age 3 | AgeJp::Calculator.new(self).age_at 4 | end 5 | 6 | def age_at(date) 7 | AgeJp::Calculator.new(self).age_at(date) 8 | end 9 | 10 | def age_jp 11 | AgeJp::Calculator.new(self).age_jp_at 12 | end 13 | 14 | def age_jp_at(date) 15 | AgeJp::Calculator.new(self).age_jp_at(date) 16 | end 17 | 18 | def east_asian_age_reckoning 19 | AgeJp::Calculator.new(self).east_asian_age_reckoning_at 20 | end 21 | 22 | def east_asian_age_reckoning_at(date) 23 | AgeJp::Calculator.new(self).east_asian_age_reckoning_at(date) 24 | end 25 | 26 | def insurance_age 27 | AgeJp::Calculator.new(self).insurance_age_at 28 | end 29 | 30 | def insurance_age_at(date) 31 | AgeJp::Calculator.new(self).insurance_age_at(date) 32 | end 33 | 34 | alias_method :to_years_old, :next_year 35 | 36 | def to_years_old_jp(n) 37 | return to_years_old(n).change(day: 28) if leap? && month == 2 && day == 29 38 | 39 | to_years_old(n).yesterday 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/age_jp/version.rb: -------------------------------------------------------------------------------- 1 | module AgeJp 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/age_jp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AgeJp do 4 | describe '#age' do 5 | context "birthday is 2000/01/01. " do 6 | around do |example| 7 | travel_to(current_date) { example.run } 8 | end 9 | 10 | let(:birthday) { Date.new(2000, 1, 1) } 11 | subject { birthday.age } 12 | 13 | context 'when today is 2014/12/30' do 14 | let(:current_date) { Date.new(2014, 12, 30) } 15 | it { is_expected.to eq 14 } 16 | end 17 | 18 | context 'when today is 2014/12/31' do 19 | let(:current_date) { Date.new(2014, 12, 31) } 20 | it { is_expected.to eq 14 } 21 | end 22 | 23 | context 'when today is 2015/1/1' do 24 | let(:current_date) { Date.new(2015, 1, 1) } 25 | it { is_expected.to eq 15 } 26 | end 27 | end 28 | 29 | context "birthday is 2000/02/29. " do 30 | around do |example| 31 | travel_to(current_date) { example.run } 32 | end 33 | 34 | let(:birthday) { Date.new(2000, 2, 29) } 35 | subject { birthday.age } 36 | 37 | context 'when today is 2015/02/28' do 38 | let(:current_date) { Date.new(2015, 2, 28) } 39 | it { is_expected.to eq 15 } 40 | end 41 | 42 | context 'when today is 2016/02/28' do 43 | let(:current_date) { Date.new(2016, 2, 28) } 44 | it { is_expected.to eq 15 } 45 | end 46 | 47 | context 'when today is 2016/2/29' do 48 | let(:current_date) { Date.new(2016, 2, 29) } 49 | it { is_expected.to eq 16 } 50 | end 51 | end 52 | end 53 | 54 | describe '#age_at' do 55 | context "birthday is 2000/01/01. " do 56 | let(:birthday) { Date.new(2000, 1, 1) } 57 | subject { birthday.age_at(today) } 58 | 59 | context 'when date is 2016/12/30' do 60 | let(:today) { Date.new(2016, 12, 30) } 61 | it { is_expected.to eq 16 } 62 | end 63 | 64 | context 'when date is 2016/12/31' do 65 | let(:today) { Date.new(2016, 12, 31) } 66 | it { is_expected.to eq 16 } 67 | end 68 | 69 | context 'when date is 2017/1/1' do 70 | let(:today) { Date.new(2017, 1, 1) } 71 | it { is_expected.to eq 17 } 72 | end 73 | end 74 | 75 | context 'invalid date' do 76 | let(:birthday) { Date.new(2000, 1, 1) } 77 | let(:invalid_date) { 'String' } 78 | subject { birthday.age_at(invalid_date) } 79 | 80 | it { expect { subject }.to raise_error(ArgumentError, /invalid date/) } 81 | end 82 | end 83 | 84 | describe '#age_jp' do 85 | context "birthday is 2000/01/01. " do 86 | around do |example| 87 | travel_to(current_date) { example.run } 88 | end 89 | 90 | let(:birthday) { Date.new(2000, 1, 1) } 91 | subject { birthday.age_jp } 92 | 93 | context 'when today is 2014/12/30' do 94 | let(:current_date) { Date.new(2014, 12, 30) } 95 | it { is_expected.to eq 14 } 96 | end 97 | 98 | context 'when today is 2014/12/31' do 99 | let(:current_date) { Date.new(2014, 12, 31) } 100 | it { is_expected.to eq 15 } 101 | end 102 | 103 | context 'when today is 2015/1/1' do 104 | let(:current_date) { Date.new(2015, 1, 1) } 105 | it { is_expected.to eq 15 } 106 | end 107 | end 108 | 109 | context "birthday is 2000/02/29. " do 110 | around do |example| 111 | travel_to(current_date) { example.run } 112 | end 113 | 114 | let(:birthday) { Date.new(2000, 2, 29) } 115 | subject { birthday.age_jp } 116 | 117 | context 'when today is 2015/02/27' do 118 | let(:current_date) { Date.new(2015, 2, 27) } 119 | it { is_expected.to eq 14 } 120 | end 121 | 122 | context 'when today is 2015/02/28' do 123 | let(:current_date) { Date.new(2015, 2, 28) } 124 | it { is_expected.to eq 15 } 125 | end 126 | 127 | context 'when today is 2016/02/28' do 128 | let(:current_date) { Date.new(2016, 2, 28) } 129 | it { is_expected.to eq 16 } 130 | end 131 | 132 | context 'when today is 2016/2/29' do 133 | let(:current_date) { Date.new(2016, 2, 29) } 134 | it { is_expected.to eq 16 } 135 | end 136 | end 137 | end 138 | 139 | describe '#age_jp_at' do 140 | context "birthday is 2000/01/01. " do 141 | let(:birthday) { Date.new(2000, 1, 1) } 142 | subject { birthday.age_jp_at(today) } 143 | 144 | context 'when date is 2016/12/30' do 145 | let(:today) { Date.new(2016, 12, 30) } 146 | it { is_expected.to eq 16 } 147 | end 148 | 149 | context 'when date is 2016/12/31' do 150 | let(:today) { Date.new(2016, 12, 31) } 151 | it { is_expected.to eq 17 } 152 | end 153 | 154 | context 'when date is 2017/1/1' do 155 | let(:today) { Date.new(2017, 1, 1) } 156 | it { is_expected.to eq 17 } 157 | end 158 | end 159 | end 160 | 161 | describe '#east_asian_age_reckoning' do 162 | context "birthday is 1999/12/31. " do 163 | around do |example| 164 | travel_to(current_date) { example.run } 165 | end 166 | 167 | let(:birthday) { Date.new(1999, 12, 31) } 168 | subject { birthday.east_asian_age_reckoning } 169 | 170 | context 'when today is 1999/12/31' do 171 | let(:current_date) { Date.new(1999, 12, 31) } 172 | it { is_expected.to eq 1 } 173 | end 174 | 175 | context 'when today is 2000/01/01' do 176 | let(:current_date) { Date.new(2000, 1, 1) } 177 | it { is_expected.to eq 2 } 178 | end 179 | 180 | context 'when today is 2000/12/31' do 181 | let(:current_date) { Date.new(2000, 12, 31) } 182 | it { is_expected.to eq 2 } 183 | end 184 | end 185 | end 186 | 187 | describe '#east_asian_age_reckoning_at' do 188 | context "birthday is 1999/12/31. " do 189 | let(:birthday) { Date.new(1999, 12, 31) } 190 | subject { birthday.east_asian_age_reckoning_at(today) } 191 | 192 | context 'when date is 2016/12/31' do 193 | let(:today) { Date.new(2016, 12, 31) } 194 | it { is_expected.to eq 18 } 195 | end 196 | 197 | context 'when date is 2017/01/01' do 198 | let(:today) { Date.new(2017, 1, 1) } 199 | it { is_expected.to eq 19 } 200 | end 201 | 202 | context 'when date is 2017/12/31' do 203 | let(:today) { Date.new(2017, 12, 31) } 204 | it { is_expected.to eq 19 } 205 | end 206 | end 207 | end 208 | 209 | describe '#to_years_old' do 210 | subject { birthday.to_years_old(year) } 211 | 212 | context 'with birthday is 2000/01/01' do 213 | let(:birthday) { Date.new(2000, 1, 1) } 214 | 215 | context 'with year is 16' do 216 | let(:year) { 16 } 217 | it { is_expected.to eq Date.new(2016, 1, 1) } 218 | end 219 | 220 | context 'with year is 17' do 221 | let(:year) { 17 } 222 | it { is_expected.to eq Date.new(2017, 1, 1) } 223 | end 224 | end 225 | 226 | context 'with birthday is 2000/02/29' do 227 | let(:birthday) { Date.new(2000, 2, 29) } 228 | 229 | context 'with year is 16' do 230 | let(:year) { 16 } 231 | it { is_expected.to eq Date.new(2016, 2, 29) } 232 | end 233 | 234 | context 'with year is 17' do 235 | let(:year) { 17 } 236 | it { is_expected.to eq Date.new(2017, 2, 28) } 237 | end 238 | end 239 | end 240 | 241 | describe '#to_years_old_jp' do 242 | subject { birthday.to_years_old_jp(year) } 243 | 244 | context 'with birthday is 2000/01/01' do 245 | let(:birthday) { Date.new(2000, 1, 1) } 246 | 247 | context 'with year is 16' do 248 | let(:year) { 16 } 249 | it { is_expected.to eq Date.new(2015, 12, 31) } 250 | end 251 | 252 | context 'with year is 17' do 253 | let(:year) { 17 } 254 | it { is_expected.to eq Date.new(2016, 12, 31) } 255 | end 256 | end 257 | 258 | context 'with birthday is 2000/02/29' do 259 | let(:birthday) { Date.new(2000, 2, 29) } 260 | 261 | context 'with year is 16' do 262 | let(:year) { 16 } 263 | it { is_expected.to eq Date.new(2016, 2, 28) } 264 | end 265 | 266 | context 'with year is 17' do 267 | let(:year) { 17 } 268 | it { is_expected.to eq Date.new(2017, 2, 28) } 269 | end 270 | end 271 | end 272 | 273 | describe '#insurance_age' do 274 | context "birthday is 2000/01/01. " do 275 | around do |example| 276 | travel_to(current_date) { example.run } 277 | end 278 | 279 | let(:birthday) { Date.new(2000, 1, 1) } 280 | subject { birthday.insurance_age } 281 | 282 | context 'when today is 2015/01/01' do 283 | let(:current_date) { Date.new(2015, 1, 1) } 284 | it { is_expected.to eq 15 } 285 | end 286 | 287 | context 'when today is 2015/07/31' do 288 | let(:current_date) { Date.new(2015, 7, 31) } 289 | it { is_expected.to eq 15 } 290 | end 291 | 292 | context 'when today is 2015/08/01' do 293 | let(:current_date) { Date.new(2015, 8, 1) } 294 | it { is_expected.to eq 16 } 295 | end 296 | end 297 | 298 | context "birthday is 2000/07/31. " do 299 | around do |example| 300 | travel_to(current_date) { example.run } 301 | end 302 | 303 | let(:birthday) { Date.new(2000, 7, 31) } 304 | subject { birthday.insurance_age } 305 | 306 | context 'when today is 2016/02/28' do 307 | let(:current_date) { Date.new(2016, 2, 28) } 308 | it { is_expected.to eq 15 } 309 | end 310 | 311 | context 'when today is 2016/02/29' do 312 | let(:current_date) { Date.new(2016, 2, 29) } 313 | it { is_expected.to eq 16 } 314 | end 315 | 316 | context 'when today is 2016/03/01' do 317 | let(:current_date) { Date.new(2016, 3, 1) } 318 | it { is_expected.to eq 16 } 319 | end 320 | end 321 | end 322 | end 323 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'age_jp' 3 | require "active_support/testing/time_helpers" 4 | 5 | RSpec.configure do |config| 6 | config.include ActiveSupport::Testing::TimeHelpers 7 | end 8 | --------------------------------------------------------------------------------