Class: Agentilda::Ordinal

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/agentilda/ordinal.rb

Overview

A plan's number, and therefore its identity. Set once when the folder is created and never changed: branch names, pull request titles and every pull-requests.md join on it, and renumbering breaks all of them silently.

Always rendered NNN.MM. 002.00 is an ordinary plan, specified before it was built. 002.01 is retroactive — work that shipped between 002 and 003 and was documented afterwards. The decimal marks a sibling, not containment: 002.01 is not part of 002.

000 is where the sequence starts, so the first plan of a project is 000.00. It also absorbs the older meaning — work that predates the plan discipline — since either way it sorts first, which is where it belongs.

Constant Summary collapse

PATTERN =

The canonical form, plus the bare NNN that trees written before this rule still use and must remain readable.

/\A(\d{1,3})(?:\.(\d{1,2}))?\z/
MAX_MINOR =

The most retroactive slots a single gap can hold.

99

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#majorInteger (readonly)

Returns 0..999.

Returns:

  • (Integer)

    0..999



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/agentilda/ordinal.rb', line 21

class Ordinal < Data.define(:major, :minor)
  include Comparable

  # The canonical form, plus the bare `NNN` that trees written before this
  # rule still use and must remain readable.
  PATTERN = /\A(\d{1,3})(?:\.(\d{1,2}))?\z/

  # The most retroactive slots a single gap can hold.
  MAX_MINOR = 99

  # @param text [String, nil] e.g. "3", "003", "003.00", "018.01"
  # @return [Agentilda::Ordinal, nil] nil when it is not a plan number
  def self.parse(text)
    m = PATTERN.match(text.to_s.strip)
    m && new(major: m[1].to_i, minor: m[2].to_i)
  end

  # Pull the number off the front of a folder name.
  #
  # @param dirname [String] e.g. "018.01-✅--verify-against-filed-returns"
  # @return [Agentilda::Ordinal, nil]
  def self.from_dirname(dirname) = parse(dirname.to_s[/\A[\d.]+/])

  # The next ordinary plan after everything in +existing+.
  #
  # The first plan in an empty tree is `000.00`, not `001.00`: the sequence
  # counts from zero so that the very first specification — usually the
  # project's own — sorts above everything and needs no gap reserved for it.
  #
  # @param existing [Array<Agentilda::Ordinal>]
  # @return [Agentilda::Ordinal]
  def self.next_major(existing)
    return new(major: 0, minor: 0) if existing.empty?

    new(major: existing.map(&:major).max + 1, minor: 0)
  end

  # The next retroactive slot in the gap after +major+.
  #
  # @param existing [Array<Agentilda::Ordinal>]
  # @param major [Integer] the plan the work landed after
  # @return [Agentilda::Ordinal]
  # @raise [Agentilda::Error] when the gap is full
  def self.next_minor(existing, major:)
    taken = existing.select { |o| o.major == major }.map(&:minor).max || 0
    if taken >= MAX_MINOR
      raise Error, "all #{MAX_MINOR} retroactive slots after #{format("%03d", major)} are taken"
    end

    new(major:, minor: taken + 1)
  end

  # @return [Boolean] whether this plan was documented after the fact
  def retroactive? = minor.positive?

  # @return [String] the canonical `NNN.MM`
  def to_s = format("%03d.%02d", major, minor)

  # @return [String] what a pull request title carries
  def to_prefix = "[#{self}]"

  # @param other [Object]
  # @return [Integer, nil]
  def <=>(other)
    return nil unless other.is_a?(self.class)

    [major, minor] <=> [other.major, other.minor]
  end
end

#minorInteger (readonly)

Returns 0..99; zero for an ordinary plan.

Returns:

  • (Integer)

    0..99; zero for an ordinary plan



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/agentilda/ordinal.rb', line 21

class Ordinal < Data.define(:major, :minor)
  include Comparable

  # The canonical form, plus the bare `NNN` that trees written before this
  # rule still use and must remain readable.
  PATTERN = /\A(\d{1,3})(?:\.(\d{1,2}))?\z/

  # The most retroactive slots a single gap can hold.
  MAX_MINOR = 99

  # @param text [String, nil] e.g. "3", "003", "003.00", "018.01"
  # @return [Agentilda::Ordinal, nil] nil when it is not a plan number
  def self.parse(text)
    m = PATTERN.match(text.to_s.strip)
    m && new(major: m[1].to_i, minor: m[2].to_i)
  end

  # Pull the number off the front of a folder name.
  #
  # @param dirname [String] e.g. "018.01-✅--verify-against-filed-returns"
  # @return [Agentilda::Ordinal, nil]
  def self.from_dirname(dirname) = parse(dirname.to_s[/\A[\d.]+/])

  # The next ordinary plan after everything in +existing+.
  #
  # The first plan in an empty tree is `000.00`, not `001.00`: the sequence
  # counts from zero so that the very first specification — usually the
  # project's own — sorts above everything and needs no gap reserved for it.
  #
  # @param existing [Array<Agentilda::Ordinal>]
  # @return [Agentilda::Ordinal]
  def self.next_major(existing)
    return new(major: 0, minor: 0) if existing.empty?

    new(major: existing.map(&:major).max + 1, minor: 0)
  end

  # The next retroactive slot in the gap after +major+.
  #
  # @param existing [Array<Agentilda::Ordinal>]
  # @param major [Integer] the plan the work landed after
  # @return [Agentilda::Ordinal]
  # @raise [Agentilda::Error] when the gap is full
  def self.next_minor(existing, major:)
    taken = existing.select { |o| o.major == major }.map(&:minor).max || 0
    if taken >= MAX_MINOR
      raise Error, "all #{MAX_MINOR} retroactive slots after #{format("%03d", major)} are taken"
    end

    new(major:, minor: taken + 1)
  end

  # @return [Boolean] whether this plan was documented after the fact
  def retroactive? = minor.positive?

  # @return [String] the canonical `NNN.MM`
  def to_s = format("%03d.%02d", major, minor)

  # @return [String] what a pull request title carries
  def to_prefix = "[#{self}]"

  # @param other [Object]
  # @return [Integer, nil]
  def <=>(other)
    return nil unless other.is_a?(self.class)

    [major, minor] <=> [other.major, other.minor]
  end
end

Class Method Details

.from_dirname(dirname) ⇒ Agentilda::Ordinal?

Pull the number off the front of a folder name.

Parameters:

  • dirname (String)

    e.g. "018.01-✅--verify-against-filed-returns"

Returns:



42
# File 'lib/agentilda/ordinal.rb', line 42

def self.from_dirname(dirname) = parse(dirname.to_s[/\A[\d.]+/])

.next_major(existing) ⇒ Agentilda::Ordinal

The next ordinary plan after everything in existing.

The first plan in an empty tree is 000.00, not 001.00: the sequence counts from zero so that the very first specification — usually the project's own — sorts above everything and needs no gap reserved for it.

Parameters:

Returns:



52
53
54
55
56
# File 'lib/agentilda/ordinal.rb', line 52

def self.next_major(existing)
  return new(major: 0, minor: 0) if existing.empty?

  new(major: existing.map(&:major).max + 1, minor: 0)
end

.next_minor(existing, major:) ⇒ Agentilda::Ordinal

The next retroactive slot in the gap after major.

Parameters:

Returns:

Raises:



64
65
66
67
68
69
70
71
# File 'lib/agentilda/ordinal.rb', line 64

def self.next_minor(existing, major:)
  taken = existing.select { |o| o.major == major }.map(&:minor).max || 0
  if taken >= MAX_MINOR
    raise Error, "all #{MAX_MINOR} retroactive slots after #{format("%03d", major)} are taken"
  end

  new(major:, minor: taken + 1)
end

.parse(text) ⇒ Agentilda::Ordinal?

Returns nil when it is not a plan number.

Parameters:

  • text (String, nil)

    e.g. "3", "003", "003.00", "018.01"

Returns:



33
34
35
36
# File 'lib/agentilda/ordinal.rb', line 33

def self.parse(text)
  m = PATTERN.match(text.to_s.strip)
  m && new(major: m[1].to_i, minor: m[2].to_i)
end

Instance Method Details

#<=>(other) ⇒ Integer?

Parameters:

  • other (Object)

Returns:

  • (Integer, nil)


84
85
86
87
88
# File 'lib/agentilda/ordinal.rb', line 84

def <=>(other)
  return nil unless other.is_a?(self.class)

  [major, minor] <=> [other.major, other.minor]
end

#retroactive?Boolean

Returns whether this plan was documented after the fact.

Returns:

  • (Boolean)

    whether this plan was documented after the fact



74
# File 'lib/agentilda/ordinal.rb', line 74

def retroactive? = minor.positive?

#to_prefixString

Returns what a pull request title carries.

Returns:

  • (String)

    what a pull request title carries



80
# File 'lib/agentilda/ordinal.rb', line 80

def to_prefix = "[#{self}]"

#to_sString

Returns the canonical NNN.MM.

Returns:

  • (String)

    the canonical NNN.MM



77
# File 'lib/agentilda/ordinal.rb', line 77

def to_s = format("%03d.%02d", major, minor)