😽

Rails で business_time と holiday_jp を用いた営業日計算のコード例

に公開

Rails で business_time と holiday_jp を用いた営業日計算のコード例

1. はじめに

この記事では、Railsアプリケーションにおける営業日計算の実装方法を解説します。営業日計算は以下のようなビジネスシナリオで必要とされます

  • 注文からの配送予定日の算出(休日を除いた営業日で計算)
  • サポートSLAの計算(問い合わせから対応までの営業時間内の経過時間)
  • 金融サービスにおける取引決済日の計算(銀行休業日を考慮)
  • 予約システムにおける利用可能日時の表示(営業時間内のみ)

この記事を通じて、以下の内容について学ぶことができます

  • business_time gem と holiday_jp gem の設定方法と利点
  • 営業日・営業時間計算のさまざまなコード例
  • 実際のビジネスロジックへの適用方法

使用するライブラリ

2. 営業日計算の実装方法

2.1 gemの追加

まず、必要なgemをGemfileに追加します。

Gemfile
gem 'business_time'
gem 'holiday_jp'

その後、以下のコマンドで依存関係をインストールします

$ bundle install

2.2 Initializer の設定

config/initializers/business_time.rb ファイルを作成し、営業日計算の基本設定を行います。

config/initializers/business_time.rb
# 現在の日付から3年前から翌年末までの範囲を動的に設定
# 注: 過去の日付計算と将来の予定計算の両方に対応するための十分な範囲
start_date = Time.current.beginning_of_year - 3.years
end_date = Time.current.next_year.end_of_year

# 営業時間の設定 (午前10時〜午後8時)
# 注: アプリケーションの要件に合わせて調整してください
BusinessTime::Config.beginning_of_workday = '10:00:00 am'
BusinessTime::Config.end_of_workday = '08:00:00 pm'

# 土日を休業日として設定 (デフォルトでは日曜のみ休業)
BusinessTime::Config.work_week = %w(monday tuesday wednesday thursday friday)

# 日本の祝祭日情報を営業日計算ライブラリに読み込み
HolidayJp.between(start_date, end_date).each { |h| BusinessTime::Config.holidays << h.date }

# 特定の日を休業日/営業日として個別に設定(必要に応じて)
# BusinessTime::Config.holidays << Date.new(2024, 12, 30) # 例: 12/30を休業日に設定
# BusinessTime::Config.work_hours[Date.new(2024, 12, 24)] = {"10:00" => "15:00"} # 例: クリスマスイブは15時まで

日付範囲の設定について
過去3年分の休日データを含めることで、過去の営業日計算も正確に行えます。また、翌年までのデータを含めることで、年末年始をまたぐ予定計算などに対応できます。業務要件によってはさらに長期の範囲が必要な場合もあります。

2.3 様々な営業日計算のコード例

以下にExampleControllerを使った実装例を示します。各メソッドごとに返り値の型(DateかTime)に注意してください。

class ExampleController < ApplicationController
  def index
    # 基本的な営業日・時間計算
    @next_business_hour = 1.business_hour.from_now  # Time型
    @next_business_day = 1.business_day.from_now    # Time型
    @ten_business_days_later = 10.business_days.from_now  # Time型
    
    # 過去の営業日・時間計算
    @one_business_day_before = 1.business_day.before(Time.current)  # Time型
    @two_business_hours_before = 2.business_hours.before(Time.current)  # Time型
    
    # 期間内の営業日数計算
    @business_days_until_next_month = Time.current.business_days_until(1.month.from_now)  # Integer型
    
    # 過去期間の営業日数
    last_year_start = 1.year.ago.beginning_of_year
    last_year_end = 1.year.ago.end_of_year
    @business_days_last_year = last_year_start.business_days_until(last_year_end)  # Integer型
    
    # 特定日からの営業日計算
    specific_date = Date.new(2024, 4, 1)
    @three_business_days_before = 3.business_days.before(specific_date)  # Date型
    
    # 営業日の開始・終了時刻
    @next_business_day_start = Time.current.next_business_day.beginning_of_workday  # Time型
    @previous_business_day_end = Time.current.previous_business_day.end_of_workday  # Time型
    
    # 特定の曜日と営業時間の組み合わせ
    @two_weeks_friday_end = 2.weeks.from_now.end_of_week(:friday).end_of_workday  # Time型
    
    # 月末の営業日
    @last_business_day_of_month = Time.current.end_of_month.prev_business_day  # Time型
    
    # 営業時間の判定と次の営業時間
    @is_during_business_hours = Time.current.during_business_hours?  # Boolean型
    @next_business_hour_start = Time.current.during_business_hours? ? 
                                Time.current : Time.current.next_business_hour  # Time型
    
    # 長期間の営業時間計算
    @hundred_business_hours_later = 100.business_hours.from_now  # Time型
    @twenty_four_business_hours_later = 24.business_hours.from_now  # Time型
    
    # 営業日の配列生成
    start_date = Date.current
    end_date = 1.month.from_now.to_date
    @business_days_array = (start_date..end_date).select(&:workday?)  # Array型(Date要素)
    
    # 祝日の取得(holiday_jp gemの機能)
    @next_holiday = HolidayJp.between(Date.current, 1.year.from_now).first  # Holiday型
    
    # 単数形/複数形の使い分け例(値が1の場合は単数形、2以上は複数形)
    @one_business_hour_later = 1.business_hour.from_now    # 単数形
    @two_business_hours_later = 2.business_hours.from_now  # 複数形
    
    @one_business_day_later = 1.business_day.from_now      # 単数形
    @five_business_days_later = 5.business_days.from_now   # 複数形
    
    @one_business_week_later = 1.business_week.from_now    # 単数形
    @three_business_weeks_later = 3.business_weeks.from_now # 複数形
    
    # ログ出力(開発時の動作確認用)
    log_examples
  end
  
  private
  
  # ログ出力用の補助メソッド
  def log_examples
    Rails.logger.debug "Next business hour: #{@next_business_hour}"
    Rails.logger.debug "Next business day: #{@next_business_day}"
    # その他のログ出力...
  end
end

重要 メソッド名の単数形と複数形について、business_time gemでは以下のルールがあります

  • 計算する単位が1の場合は単数形(例:business_hourbusiness_daybusiness_week
  • 2以上の場合は複数形(例:business_hoursbusiness_daysbusiness_weeks

3. コード例の解説

3.1 基本的な営業日・時間計算

現在時刻から1営業時間後を計算

@next_business_hour = 1.business_hour.from_now
  • 現在の時刻から1営業時間後の日時を計算します
  • 休日や営業時間外の時間は自動的に考慮されます
  • 例:金曜日の午後7時からの1営業時間は、翌月曜午前10時から午前11時になります

現在日から1営業日後を計算

@next_business_day = 1.business_day.from_now
  • 現在の日付から1営業日後の日付を計算します
  • 休日は営業日としてカウントされません
  • 返される値は営業日の開始時刻(設定された始業時間)になります

現在から10営業日後の日付を計算

@ten_business_days_later = 10.business_days.from_now
  • 現在の日付から10営業日後の日付を計算します
  • 祝日や週末はスキップされます

3.2 過去の営業日・時間計算

現在から1営業日前の日付を計算

@one_business_day_before = 1.business_day.before(Time.current)
  • 現在の日付から1営業日前の日付を計算します
  • 休日は考慮され、スキップされます

現在から2営業時間前の時刻を計算

@two_business_hours_before = 2.business_hours.before(Time.current)
  • 現在の時刻から2営業時間前の時刻を計算します
  • 休日や営業時間外の時間は考慮されます
  • 例:月曜日の午前11時からの2営業時間前は、前週金曜日の午後6時になります
金曜日             週末(非営業日)          月曜日
|-----------------|---------------------|---------------|
10時     18時(6PM)  土・日                10時    11時
          ↑                              ↑      ↑
          |                              |      |
       結果点                        営業開始点  開始点
(2営業時間前)                      (1営業時間前)

3.3 期間内の営業日数計算

現在から1ヶ月後までの営業日数を計算

@days_between = Time.current.business_days_until(1.month.from_now)
  • 現在の日付から1ヶ月後までの営業日数を計算します
  • 休日は除外され、整数値が返されます

去年の営業日数を計算

last_year_start = 1.year.ago.beginning_of_year
last_year_end = 1.year.ago.end_of_year
@business_days_last_year = last_year_start.business_days_until(last_year_end)
  • 去年の営業日数を計算します
  • 日本の祝日と週末が自動的に除外されます

3.4 特定の日付や時刻の計算

特定の日付から3営業日前の日付を計算

specific_date = Date.new(2024, 4, 1)
@three_business_days_before = 3.business_days.before(specific_date)
  • 指定された日付(この例では2024年4月1日)から3営業日前の日付を計算します
  • イニシャライザで設定した休日情報を元に正確に計算されます

次の営業日の開始時刻を計算

@next_business_day_start = Time.current.next_business_day.beginning_of_workday
  • 次の営業日の開始時刻を計算します
  • 現在日が営業日であっても、翌営業日の開始時刻が返されます

2週間後の金曜日の終業時刻を計算

@two_weeks_friday_end = 2.weeks.from_now.end_of_week(:friday).end_of_workday
  • 2週間後の金曜日の終業時刻を計算します
  • Active Supportの日付計算とbusiness_timeの営業時間計算を組み合わせています

今月の最後の営業日を計算

@last_business_day_of_month = Time.current.end_of_month.prev_business_day
  • 今月の最後の営業日を計算します
  • 月末が休日の場合、その前の営業日が返されます

3.5 営業時間状態の判定

現在の日時が営業時間内かどうかを判定

@is_during_business_hours = Time.current.during_business_hours?
  • 現在の日時が設定された営業時間内かどうかを判定します(Boolean)
  • 営業日でも営業時間外の場合はfalseを返します

次の営業時間の開始時刻を計算

@next_business_hour_start = Time.current.during_business_hours? ? Time.current : Time.current.next_business_hour
  • 現在が営業時間内の場合は現在時刻を、営業時間外の場合は次の営業時間の開始時刻を返します
  • 条件分岐を使って柔軟な日時計算を実現しています

3.6 営業日配列の生成と祝日取得

特定の期間内の営業日のみの日付配列を生成

start_date = Date.current
end_date = 1.month.from_now.to_date
@business_days_array = (start_date..end_date).select(&:workday?)
  • 今日から1ヶ月後までの期間内の営業日のみの日付配列を生成します
  • Rubyの範囲とフィルタリングを使った簡潔な実装です

次の祝日を取得

@next_holiday = HolidayJp.between(Date.current, 1.year.from_now).first
  • holiday_jp gemを使用して、今日から1年以内の最初の祝日を取得します
  • 返されるオブジェクトはHoliday型で、datenameプロパティを持ちます

4. エラーハンドリングとパフォーマンス考慮点

4.1 エラーハンドリング

営業日計算を行う際には、以下のようなエッジケースに注意が必要です

# 営業日計算の範囲外の日付を指定した場合のエラーハンドリング
def calculate_business_days_safely(start_date, end_date)
  begin
    start_date.business_days_until(end_date)
  rescue ArgumentError => e
    Rails.logger.error "営業日計算エラー: #{e.message}"
    # 代替ロジックまたはデフォルト値を返す
    return nil
  end
end

# 祝日定義が存在しない可能性がある将来の日付の取り扱い
def is_future_business_day?(date)
  # イニシャライザの日付範囲内かチェック
  max_configured_date = Time.current.next_year.end_of_year.to_date
  
  if date > max_configured_date
    Rails.logger.warn "設定された休日範囲外の日付です: #{date}"
    # 曜日だけでチェックするなど、代替ロジックを実装
    return !date.saturday? && !date.sunday?
  end
  
  # 通常の営業日チェック
  date.workday?
end

4.2 パフォーマンス考慮点

大量の営業日計算や頻繁な計算を行う場合、以下の点に注意してください

# 連続した営業日の計算を効率化する例
def next_n_business_days(n)
  result = []
  date = Date.current
  
  while result.size < n
    date += 1.day
    result << date if date.workday?
  end
  
  result
end

# 計算結果のキャッシュを活用する例
def cached_month_business_days(year, month)
  cache_key = "business_days/#{year}/#{month}"
  
  Rails.cache.fetch(cache_key, expires_in: 1.month) do
    start_date = Date.new(year, month, 1)
    end_date = start_date.end_of_month
    
    (start_date..end_date).select(&:workday?).size
  end
end

5. カスタマイズ例

5.1 特定の曜日を非営業日として設定

# 月曜日から金曜日の10:00〜18:00を営業時間として設定(水曜日休業)
BusinessTime::Config.work_week = %w(monday tuesday thursday friday)
BusinessTime::Config.beginning_of_workday = "10:00 am"
BusinessTime::Config.end_of_workday = "6:00 pm"

5.2 特定の日付を営業日/非営業日として設定

# 特定の日を休業日として追加
BusinessTime::Config.holidays << Date.new(2024, 12, 30)
BusinessTime::Config.holidays << Date.new(2024, 12, 31)

# 通常休業の土曜日を特定の日だけ営業日として設定
date = Date.new(2024, 12, 21) # 特別営業の土曜日
BusinessTime::Config.work_week = %w(monday tuesday wednesday thursday friday saturday)
BusinessTime::Config.holidays.delete(date) if BusinessTime::Config.holidays.include?(date)

5.3 営業時間のカスタマイズ

# 特定の日だけ営業時間を変更する(半日営業など)
BusinessTime::Config.work_hours[Date.new(2024, 12, 24)] = {"10:00" => "15:00"} # クリスマスイブは15時まで

# 曜日ごとに異なる営業時間を設定
BusinessTime::Config.work_hours = {
  mon: {'9:00' => '17:00'},
  tue: {'9:00' => '17:00'},
  wed: {'9:00' => '17:00'},
  thu: {'9:00' => '17:00'},
  fri: {'9:00' => '16:00'}  # 金曜日は16時まで
}

5.4 複数の拠点(営業カレンダー)の管理

# 東京と大阪の営業所で異なる営業日カレンダーを管理する例
module BusinessCalendars
  def self.tokyo_config
    config = BusinessTime::Config.new
    
    # 東京の営業時間設定
    config.work_week = %w(monday tuesday wednesday thursday friday)
    config.beginning_of_workday = "9:00 am"
    config.end_of_workday = "6:00 pm"
    
    # 東京固有の休業日を追加
    config.holidays << Date.new(2024, 7, 23) # 東京のみの休業日
    
    config
  end
  
  def self.osaka_config
    config = BusinessTime::Config.new
    
    # 大阪の営業時間設定
    config.work_week = %w(monday tuesday wednesday thursday friday)
    config.beginning_of_workday = "10:00 am"
    config.end_of_workday = "7:00 pm"
    
    # 大阪固有の休業日を追加
    config.holidays << Date.new(2024, 7, 25) # 大阪のみの休業日
    
    config
  end
  
  # 拠点ごとの営業日計算メソッド
  def self.next_business_day(date, location)
    config = location == :tokyo ? tokyo_config : osaka_config
    BusinessTime::Config.with(config) do
      date.next_business_day
    end
  end
end

# 使用例
tokyo_next_day = BusinessCalendars.next_business_day(Date.current, :tokyo)
osaka_next_day = BusinessCalendars.next_business_day(Date.current, :osaka)

6. まとめ

この記事では、Railsアプリケーションにおいてbusiness_time gemとholiday_jp gemを使用して、日本の祝日と休日を考慮した営業日計算を行う方法を解説しました。

主要なポイントは以下の通りです

  1. business_timeholiday_jp gemの適切な設定と初期化
  2. 動的な日付範囲での祝日と休日の設定
  3. 単数形/複数形を正しく使い分けた営業日・時間計算メソッド
  4. エラーハンドリングとパフォーマンス最適化の考慮点
  5. カスタマイズによる柔軟な営業日管理

これらの知識を活用することで、より柔軟で正確な営業日計算機能をRailsアプリケーションに実装することができます。営業日計算は一見単純に見えますが、祝日や特別な営業日などの例外処理を含めて正確に実装することが重要です。

さらに学ぶための資料

以上、最後までお読みいただきありがとうございました。この記事が営業日計算機能の実装に役立つことを願っています。

ご質問やフィードバックがありましたら、コメントセクションよりお気軽にご連絡ください。

Discussion