Class: Philiprehberger::CronParser::Expression

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/cron_parser/expression.rb

Overview

Represents a parsed 5-field cron expression

Constant Summary collapse

FIELD_RANGES =
{
  minute: { min: 0, max: 59 },
  hour: { min: 0, max: 23 },
  day: { min: 1, max: 31 },
  month: { min: 1, max: 12 },
  weekday: { min: 0, max: 6 }
}.freeze
FIELD_NAMES_MAP =
{
  minute: nil,
  hour: nil,
  day: nil,
  month: Field::MONTH_NAMES,
  weekday: Field::WEEKDAY_NAMES
}.freeze
HUMAN_LABELS =
{
  minute: 'minute',
  hour: 'hour',
  day: 'day of month',
  month: 'month',
  weekday: 'day of week'
}.freeze
ALIASES =
{
  '@yearly' => '0 0 1 1 *',
  '@annually' => '0 0 1 1 *',
  '@monthly' => '0 0 1 * *',
  '@weekly' => '0 0 * * 0',
  '@daily' => '0 0 * * *',
  '@midnight' => '0 0 * * *',
  '@hourly' => '0 * * * *'
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(expr) ⇒ Expression

Returns a new instance of Expression.

Parameters:

  • expr (String)

    a 5-field cron expression or named alias (e.g. “@daily”)

Raises:

  • (Error)

    if the expression is invalid



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/philiprehberger/cron_parser/expression.rb', line 48

def initialize(expr)
  stripped = expr.strip
  stripped = ALIASES[stripped.downcase] if stripped.start_with?('@')
  raise Error, "Unknown cron alias: #{expr.strip}" if stripped.nil?

  @expression = stripped
  parts = @expression.split(/\s+/)
  raise Error, "Expected 5 fields, got #{parts.size}: #{@expression}" unless parts.size == 5

  @fields = {}
  FIELD_RANGES.each_with_index do |(name, range), index|
    @fields[name] = Field.new(parts[index], min: range[:min], max: range[:max], names: FIELD_NAMES_MAP[name])
  end
end

Instance Attribute Details

#expressionString (readonly)

Returns the original (post-alias expansion) expression.

Returns:

  • (String)

    the original (post-alias expansion) expression



44
45
46
# File 'lib/philiprehberger/cron_parser/expression.rb', line 44

def expression
  @expression
end

Instance Method Details

#count_in(from:, to:) ⇒ Integer

Count occurrences within a time window [from, to].

The window is inclusive on both ends; ‘from` is treated as the cursor’s starting point so an exact match at ‘from` counts only if it lies on a minute boundary that the schedule fires on.

Parameters:

  • from (Time)

    start of the window

  • to (Time)

    end of the window

Returns:

  • (Integer)

    the count of occurrences in the window

Raises:

  • (Error)

    if ‘to` is earlier than `from`



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/philiprehberger/cron_parser/expression.rb', line 119

def count_in(from:, to:)
  raise Error, '`to` must be greater than or equal to `from`' if to < from

  count = 0
  cursor = round_to_minute(from) - 60
  loop do
    cursor = self.next(from: cursor)
    break if cursor > to

    count += 1
  end
  count
end

#human_readableString Also known as: description

Return a human-readable description of the expression

Returns:

  • (String)


136
137
138
139
140
141
142
143
144
# File 'lib/philiprehberger/cron_parser/expression.rb', line 136

def human_readable
  parts = []
  parts << minute_description
  parts << hour_description
  parts << day_description
  parts << month_description
  parts << weekday_description
  parts.compact.join(', ')
end

#matches?(time) ⇒ Boolean

Check if a given time matches this cron expression

Parameters:

  • time (Time)

    the time to check

Returns:

  • (Boolean)


67
68
69
70
71
72
73
# File 'lib/philiprehberger/cron_parser/expression.rb', line 67

def matches?(time)
  @fields[:minute].matches?(time.min) &&
    @fields[:hour].matches?(time.hour) &&
    @fields[:day].matches?(time.day) &&
    @fields[:month].matches?(time.month) &&
    @fields[:weekday].matches?(time.wday)
end

#next(from: Time.now) ⇒ Time

Calculate the next occurrence after the given time

Parameters:

  • from (Time) (defaults to: Time.now)

    the starting time

Returns:

  • (Time)

    the next matching time

Raises:

  • (Error)

    if no match found within 4 years



80
81
82
# File 'lib/philiprehberger/cron_parser/expression.rb', line 80

def next(from: Time.now)
  find_occurrence(from, direction: :forward)
end

#next_n(count, from: Time.now) ⇒ Array<Time>

Calculate the next N occurrences after the given time

Parameters:

  • count (Integer)

    the number of occurrences to find

  • from (Time) (defaults to: Time.now)

    the starting time

Returns:

  • (Array<Time>)

    the next N matching times



98
99
100
101
102
103
104
105
106
107
# File 'lib/philiprehberger/cron_parser/expression.rb', line 98

def next_n(count, from: Time.now)
  results = []
  current = from
  count.times do
    current = self.next(from: current)
    results << current
    current += 60
  end
  results
end

#prev(from: Time.now) ⇒ Time

Calculate the previous occurrence before the given time

Parameters:

  • from (Time) (defaults to: Time.now)

    the starting time

Returns:

  • (Time)

    the previous matching time

Raises:

  • (Error)

    if no match found within 4 years



89
90
91
# File 'lib/philiprehberger/cron_parser/expression.rb', line 89

def prev(from: Time.now)
  find_occurrence(from, direction: :backward)
end