dsu 0.1.0.alpha.5 → 1.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile.lock +6 -15
  5. data/README.md +33 -47
  6. data/lib/dsu/base_cli.rb +13 -6
  7. data/lib/dsu/cli.rb +46 -55
  8. data/lib/dsu/command_services/add_entry_service.rb +21 -21
  9. data/lib/dsu/core/ruby/not_today.rb +11 -0
  10. data/lib/dsu/models/entry.rb +32 -21
  11. data/lib/dsu/models/entry_group.rb +41 -105
  12. data/lib/dsu/services/configuration_loader_service.rb +19 -2
  13. data/lib/dsu/services/entry_group_editor_service.rb +37 -89
  14. data/lib/dsu/services/stdout_redirector_service.rb +27 -0
  15. data/lib/dsu/subcommands/list.rb +83 -15
  16. data/lib/dsu/support/colorable.rb +1 -0
  17. data/lib/dsu/support/command_options/dsu_times.rb +33 -0
  18. data/lib/dsu/support/command_options/time.rb +77 -0
  19. data/lib/dsu/support/command_options/time_mneumonic.rb +127 -0
  20. data/lib/dsu/support/command_options/time_mneumonics.rb +15 -0
  21. data/lib/dsu/support/configurable.rb +15 -0
  22. data/lib/dsu/support/configuration.rb +13 -1
  23. data/lib/dsu/support/entry_group_fileable.rb +31 -6
  24. data/lib/dsu/support/entry_group_loadable.rb +13 -16
  25. data/lib/dsu/support/entry_group_viewable.rb +26 -8
  26. data/lib/dsu/support/times_sortable.rb +1 -3
  27. data/lib/dsu/validators/description_validator.rb +38 -0
  28. data/lib/dsu/validators/entries_validator.rb +43 -32
  29. data/lib/dsu/validators/time_validator.rb +11 -20
  30. data/lib/dsu/version.rb +1 -1
  31. data/lib/dsu/views/edited_entries/shared/errors.rb +39 -0
  32. data/lib/dsu/views/entry_group/edit.rb +89 -39
  33. data/lib/dsu/views/entry_group/show.rb +10 -4
  34. data/lib/dsu/views/shared/messages.rb +56 -0
  35. data/lib/dsu.rb +8 -2
  36. metadata +24 -12
  37. data/lib/dsu/support/commander/command.rb +0 -130
  38. data/lib/dsu/support/commander/command_help.rb +0 -62
  39. data/lib/dsu/support/commander/subcommand.rb +0 -45
  40. data/lib/dsu/support/interactive/cli.rb +0 -161
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'deco_lite'
3
+ require 'active_model'
4
4
  require_relative '../services/entry_group_editor_service'
5
5
  require_relative '../services/entry_group_deleter_service'
6
6
  require_relative '../services/entry_group_reader_service'
@@ -13,30 +13,28 @@ require_relative 'entry'
13
13
 
14
14
  module Dsu
15
15
  module Models
16
- class EntryGroup < DecoLite::Model
16
+ # This class represents a group of entries for a given day. IOW,
17
+ # things someone might want to share at their daily standup (DSU).
18
+ class EntryGroup
19
+ include ActiveModel::Model
17
20
  extend Support::EntryGroupLoadable
18
21
  include Support::TimeFormatable
19
22
 
20
- validates_with Validators::EntriesValidator, fields: [:entries]
21
- validates_with Validators::TimeValidator, fields: [:time]
23
+ attr_accessor :time
24
+ attr_reader :entries
25
+
26
+ validates_with Validators::EntriesValidator
27
+ validates_with Validators::TimeValidator
22
28
 
23
29
  def initialize(time: nil, entries: [])
24
30
  raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
25
- raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array) || entries.nil?
26
-
27
- time ||= Time.now
28
- time = time.localtime if time.utc?
29
31
 
30
- entries ||= []
31
-
32
- super(hash: {
33
- time: time,
34
- entries: entries
35
- })
32
+ @time = ensure_local_time(time)
33
+ self.entries = entries || []
36
34
  end
37
35
 
38
36
  class << self
39
- def delete(time:, options: {})
37
+ def delete!(time:, options: {})
40
38
  Services::EntryGroupDeleterService.new(time: time, options: options).call
41
39
  end
42
40
 
@@ -46,127 +44,65 @@ module Dsu
46
44
  # return new(time: time) unless exists?(time: time)
47
45
 
48
46
  load(time: time).tap do |entry_group|
49
- entry_group.edit(options: options)
47
+ Services::EntryGroupEditorService.new(entry_group: entry_group, options: options).call
50
48
  end
51
49
  end
52
50
 
53
51
  def exists?(time:)
54
52
  Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
55
53
  end
54
+ end
56
55
 
57
- # Loads the EntryGroup from the file system and returns an
58
- # instantiated EntryGroup object.
59
- def load(time: nil)
60
- new(**hydrated_entry_group_hash_for(time: time))
61
- end
62
-
63
- # This function returns a hash whose :time and :entries
64
- # key values are hydrated with instantiated Time and Entry
65
- # objects.
66
- def hydrated_entry_group_hash_for(time:)
67
- entry_group_hash = entry_group_hash_for(time: time)
68
- hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
69
- end
56
+ def valid_unique_entries
57
+ entries&.select(&:valid?)&.uniq(&:description)
58
+ end
70
59
 
71
- def unique?(entry:)
60
+ def clone
61
+ clone = super
72
62
 
73
- end
63
+ clone.entries = clone.entries.map(&:clone)
64
+ clone
74
65
  end
75
66
 
76
- def required_fields
77
- %i[time entries]
78
- end
67
+ def entries=(entries)
68
+ entries ||= []
79
69
 
80
- def edit(options: {})
81
- Services::EntryGroupEditorService.new(entry_group: self, options: options).call
82
- self
70
+ raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array)
71
+ raise ArgumentError, 'entries contains the wrong object type' unless entries.all?(Entry)
72
+
73
+ @entries = entries.map(&:clone)
83
74
  end
84
75
 
85
76
  # Deletes the entry group file from the file system.
86
- def delete
87
- self.class.delete(time: time)
77
+ def delete!
78
+ self.class.delete!(time: time)
79
+ self.entries = []
88
80
  self
89
81
  end
90
82
 
91
- def entries?
92
- entries.any?
83
+ def time_formatted
84
+ formatted_time(time: time)
93
85
  end
94
86
 
95
87
  def save!
88
+ delete! and return if entries.empty?
89
+
96
90
  validate!
97
91
  Services::EntryGroupWriterService.new(entry_group: self).call
92
+ self
98
93
  end
99
94
 
100
95
  def to_h
101
- super.tap do |hash|
102
- hash[:entries] = hash[:entries].dup
103
- hash[:entries].each_with_index do |entry, index|
104
- hash[:entries][index] = entry.to_h
105
- end
106
- end
107
- end
108
-
109
- def check_unique(sha_or_editor_command:, description:)
110
- raise ArgumentError, 'sha_or_editor_command is nil' if sha_or_editor_command.nil?
111
- raise ArgumentError, 'description is nil' if description.nil?
112
- raise ArgumentError, 'sha_or_editor_command is the wrong object type' unless sha_or_editor_command.is_a?(String)
113
- raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
114
-
115
- if entries.blank?
116
- entry_unique_hash = entry_unique_hash_for(uuid_unique: true, description_unique: true)
117
- return entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
118
- end
119
-
120
- entry_hash = entries.each_with_object({}) do |entry_group_entry, hash|
121
- hash[entry_group_entry.uuid] = entry_group_entry.description
122
- end
123
-
124
- # It is possible that sha_or_editor_command may have an editor command (e.g. +|a|add). If this
125
- # is the case, just treat it as unique because when the entry is added, it will get a unique uuid.
126
- uuid_unique = !sha_or_editor_command.match?(Entry::ENTRY_UUID_REGEX) || !entry_hash.key?(sha_or_editor_command)
127
- entry_unique_hash = entry_unique_hash_for(
128
- uuid: sha_or_editor_command,
129
- uuid_unique: uuid_unique,
130
- description: description,
131
- description_unique: !entry_hash.value?(description)
132
- )
133
- entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
134
- end
135
-
136
- def entry_unique_hash_for(uuid_unique:, description_unique:, uuid: nil, description: nil)
137
96
  {
138
- uuid: uuid,
139
- uuid_unique: uuid_unique,
140
- description: description,
141
- description_unique: description_unique,
142
- formatted_time: Support::TimeFormatable.formatted_time(time: time)
97
+ time: time.dup,
98
+ entries: entries.map(&:to_h)
143
99
  }
144
100
  end
145
101
 
146
- def entry_unique_struct_from(entry_unique_hash:)
147
- Struct.new(*entry_unique_hash.keys, keyword_init: true) do
148
- def unique?
149
- uuid_unique? && description_unique?
150
- end
151
-
152
- def uuid_unique?
153
- uuid_unique
154
- end
155
-
156
- def description_unique?
157
- description_unique
158
- end
159
-
160
- def messages
161
- return [] if unique?
102
+ private
162
103
 
163
- short_description = Models::Entry.short_description(string: description)
164
-
165
- messages = []
166
- messages << "#uuid is not unique: \"#{uuid} #{short_description}\"" unless uuid_unique?
167
- messages << "#description is not unique: \"#{uuid} #{short_description}\""
168
- end
169
- end.new(**entry_unique_hash)
104
+ def ensure_local_time(time)
105
+ time.nil? ? Time.now : time.dup.localtime
170
106
  end
171
107
  end
172
108
  end
@@ -6,6 +6,7 @@ require_relative '../support/configuration'
6
6
 
7
7
  module Dsu
8
8
  module Services
9
+ # This class loads an entry group file.
9
10
  class ConfigurationLoaderService
10
11
  include Dsu::Support::Configuration
11
12
 
@@ -29,9 +30,25 @@ module Dsu
29
30
  attr_reader :default_options
30
31
 
31
32
  def config_options
32
- return Support::Configuration::DEFAULT_DSU_OPTIONS unless config_file?
33
+ return default_config unless config_file?
33
34
 
34
- @config_options ||= YAML.safe_load(ERB.new(File.read(config_file)).result)
35
+ @config_options ||= begin
36
+ loaded_config = YAML.safe_load(ERB.new(File.read(config_file)).result)
37
+ loaded_config = update_and_write_config_file!(loaded_config) unless loaded_config.keys == default_config.keys
38
+ loaded_config
39
+ end
40
+ end
41
+
42
+ def update_and_write_config_file!(loaded_config)
43
+ loaded_config = default_config.merge(loaded_config)
44
+ # TODO: Make this into a configuration writer service.
45
+ # TODO: Test this
46
+ File.write(config_file, loaded_config.to_yaml)
47
+ loaded_config
48
+ end
49
+
50
+ def default_config
51
+ Support::Configuration::DEFAULT_DSU_OPTIONS
35
52
  end
36
53
  end
37
54
  end
@@ -2,14 +2,18 @@
2
2
 
3
3
  require_relative '../models/entry'
4
4
  require_relative '../support/colorable'
5
+ require_relative '../support/configurable'
5
6
  require_relative '../support/say'
6
7
  require_relative '../support/time_formatable'
7
- require_relative 'configuration_loader_service'
8
+ require_relative '../views/edited_entries/shared/errors'
9
+ require_relative '../views/shared/messages'
10
+ require_relative 'stdout_redirector_service'
8
11
 
9
12
  module Dsu
10
13
  module Services
11
14
  class EntryGroupEditorService
12
15
  include Support::Colorable
16
+ include Support::Configurable
13
17
  include Support::Say
14
18
  include Support::TimeFormatable
15
19
 
@@ -24,7 +28,7 @@ module Dsu
24
28
 
25
29
  def call
26
30
  edit_view = render_edit_view
27
- edit!(edit_view: edit_view)
31
+ edit edit_view
28
32
  # NOTE: Return the original entry group object as any permanent changes
29
33
  # will have been applied to it.
30
34
  entry_group
@@ -38,110 +42,54 @@ module Dsu
38
42
  # and edit it. The edits will be used to update the entry group.
39
43
  def render_edit_view
40
44
  say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
41
- capture_stdxxx { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
45
+ StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
42
46
  end
43
47
 
44
- # Writes the temporary file contents to disk and opens it in the editor.
45
- def edit!(edit_view:)
48
+ # Writes the temporary file contents to disk and opens it in the editor
49
+ # for editing. It then copies the changes to the entry group and writes
50
+ # the changes to the entry group file.
51
+ def edit(edit_view)
52
+ entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
53
+
46
54
  Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
47
- unless Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
48
- say "Failed to open temporary file in editor '#{configuration[:editor]}';" \
55
+ if Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
56
+ Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
57
+ next unless process_description?(editor_line)
58
+
59
+ entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
60
+ end
61
+
62
+ process_entry_group!(entry_group_with_edits)
63
+ else
64
+ say "Failed to open temporary file in editor '#{configuration[:editor]}'; " \
49
65
  "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
50
66
  say 'Either set the EDITOR environment variable ' \
51
67
  'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
52
- say 'Run `$ dsu help config` for more information.', ERROR
53
-
54
- system('dsu help config')
55
-
56
- return # rubocop:disable Lint/NonLocalExitFromIterator: This is not an iterator.
68
+ say 'Run `$ dsu help config` for more information:', ERROR
57
69
  end
58
-
59
- update_entry_group!(tmp_file_path: tmp_file_path)
60
70
  end
61
71
  end
62
72
 
63
- # TODO: Clean this up
64
- def update_entry_group!(tmp_file_path:)
65
- errors = []
66
- entry_group.entries = entries = []
67
- Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |tmp_file_line|
68
- next if comment_or_empty?(tmp_file_line: tmp_file_line)
69
-
70
- entry_info = editor_entry_info_from(tmp_file_line: tmp_file_line)
71
- next if entry_info.empty?
72
- next if delete_entry_cmd?(sha: entry_info[:sha])
73
- next unless add_entry_cmd?(sha: entry_info[:sha]) || sha?(sha: entry_info[:sha])
74
-
75
- entry_info[:sha_or_editor_command] = entry_info[:sha]
76
- entry_info[:sha] = nil if add_entry_cmd?(sha: entry_info[:sha])
77
-
78
- entry = Models::Entry.new(uuid: entry_info[:sha], description: entry_info[:description])
79
- entry_group.check_unique(sha_or_editor_command: entry_info[:sha_or_editor_command],
80
- description: entry_info[:description]).tap do |status|
81
- entries << entry and next if status.unique?
82
-
83
- errors << status.messages
84
- end
73
+ def process_entry_group!(entry_group_with_edits)
74
+ if entry_group_with_edits.entries.empty?
75
+ entry_group.delete!
76
+ return
85
77
  end
86
78
 
87
- # Display any errors encountered.
88
- if errors.any?
89
- say 'Error: one or more entry values were not unique within the entry group entries:', ERROR
90
- errors.flatten.each { |message| say "Error: #{message}", ERROR }
79
+ if entry_group_with_edits.invalid?
80
+ header = 'The following ERRORS were encountered; these changes were not saved:'
81
+ messages = entry_group_with_edits.errors.full_messages
82
+ Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
91
83
  end
92
84
 
93
- # Save or delete any entries.
94
- entry_group.entries = entries
95
- entry_group.delete and return unless entry_group.entries?
96
-
85
+ # Make sure we're saving only valid, unique entries.
86
+ entry_group.entries = entry_group_with_edits.valid_unique_entries
97
87
  entry_group.save!
98
88
  end
99
89
 
100
- def sha?(sha:)
101
- sha.match?(Models::Entry::ENTRY_UUID_REGEX)
102
- end
103
-
104
- def delete_entry_cmd?(sha:)
105
- %w[- d delete].include?(sha)
106
- end
107
-
108
- def add_entry_cmd?(sha:)
109
- %w[+ a add].include?(sha)
110
- end
111
-
112
- def comment_or_empty?(tmp_file_line:)
113
- ['#', nil].include? tmp_file_line[0]
114
- end
115
-
116
- def editor_entry_info_from(tmp_file_line:)
117
- match_data = tmp_file_line.match(/(\S+)\s(.+)/)
118
- {
119
- sha: match_data[1]&.strip,
120
- description: match_data[2]&.strip
121
- }
122
- rescue StandardError
123
- {}
124
- end
125
-
126
- # TODO: Add this to a module.
127
- # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
128
- def capture_stdxxx
129
- # The output stream must be an IO-like object. In this case we capture it in
130
- # an in-memory IO object so we can return the string value. You can assign any
131
- # IO object here.
132
- string_io = StringIO.new
133
- prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
134
- prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
135
- yield
136
- string_io.string
137
- ensure
138
- # Restore the previous value of stderr and stdout (typically equal to STDERR).
139
- $stdout = prev_stdout
140
- $stderr = prev_stderr
141
- end
142
-
143
- def configuration
144
- @configuration ||= ConfigurationLoaderService.new.call
90
+ def process_description?(description)
91
+ description = Models::Entry.clean_description(description)
92
+ !(description.blank? || description[0] == '#')
145
93
  end
146
94
  end
147
95
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Services
5
+ # This service captures $stdout, resirects it to a StringIO object,
6
+ # and returns the string value.
7
+ # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
8
+ module StdoutRedirectorService
9
+ class << self
10
+ def call
11
+ raise ArgumentError, 'no block was provided' unless block_given?
12
+
13
+ # The output stream must be an IO-like object. In this case we capture it in
14
+ # an in-memory IO object so we can return the string value. Any IO object can
15
+ # be used here.
16
+ string_io = StringIO.new
17
+ original_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
18
+ yield
19
+ string_io.string
20
+ ensure
21
+ # Restore the original $stdout.
22
+ $stdout = original_stdout
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../base_cli'
4
+ require_relative '../support/command_options/dsu_times'
5
+ require_relative '../support/time_formatable'
4
6
 
5
7
  module Dsu
6
8
  module Subcommands
7
9
  class List < Dsu::BaseCLI
10
+ include Support::CommandOptions::DsuTimes
11
+ include Support::TimeFormatable
12
+
8
13
  map %w[d] => :date
14
+ map %w[dd] => :dates
9
15
  map %w[n] => :today
10
16
  map %w[t] => :tomorrow
11
17
  map %w[y] => :yesterday
@@ -17,10 +23,8 @@ module Dsu
17
23
  LONG_DESC
18
24
  def today
19
25
  time = Time.now
20
- sorted_dsu_times_for(times: [time, time.yesterday]).each do |t|
21
- view_entry_group(time: t)
22
- puts
23
- end
26
+ times = sorted_dsu_times_for(times: [time, time.yesterday])
27
+ view_list_for(times: times)
24
28
  end
25
29
 
26
30
  desc 'tomorrow, t',
@@ -30,10 +34,8 @@ module Dsu
30
34
  LONG_DESC
31
35
  def tomorrow
32
36
  time = Time.now
33
- sorted_dsu_times_for(times: [time.tomorrow, time]).each do |t|
34
- view_entry_group(time: t)
35
- puts
36
- end
37
+ times = sorted_dsu_times_for(times: [time.tomorrow, time])
38
+ view_list_for(times: times)
37
39
  end
38
40
 
39
41
  desc 'yesterday, y',
@@ -43,10 +45,8 @@ module Dsu
43
45
  LONG_DESC
44
46
  def yesterday
45
47
  time = Time.now
46
- sorted_dsu_times_for(times: [time.yesterday, time.yesterday.yesterday]).each do |t|
47
- view_entry_group(time: t)
48
- puts
49
- end
48
+ times = sorted_dsu_times_for(times: [time.yesterday, time.yesterday.yesterday])
49
+ view_list_for(times: times)
50
50
  end
51
51
 
52
52
  desc 'date, d DATE',
@@ -57,14 +57,82 @@ module Dsu
57
57
  LONG_DESC
58
58
  def date(date)
59
59
  time = Time.parse(date)
60
- sorted_dsu_times_for(times: [time, time.yesterday]).each do |t|
61
- view_entry_group(time: t)
62
- puts
60
+ times = sorted_dsu_times_for(times: [time, time.yesterday])
61
+ view_list_for(times: times)
62
+ rescue ArgumentError => e
63
+ say "Error: #{e.message}", ERROR
64
+ exit 1
65
+ end
66
+
67
+ desc 'dates, dd OPTIONS',
68
+ 'Displays the DSU entries for the OPTIONS provided'
69
+ long_desc <<~LONG_DESC
70
+ NAME
71
+ \x5
72
+ `dsu dates|dd OPTIONS` -- will display the DSU entries for the OPTIONS provided.
73
+
74
+ SYNOPSIS
75
+ \x5
76
+ `dsu dates|dd OPTIONS`
77
+
78
+ OPTIONS:
79
+ \x5
80
+ -f|--from DATE|MNEMONIC: ?.
81
+
82
+ \x5
83
+ -t|--to DATE|MNEMONIC: ?.
84
+
85
+ \x5
86
+ #{date_option_description}
87
+
88
+ \x5
89
+ #{mneumonic_option_description}
90
+ LONG_DESC
91
+ # -f, --from FROM [DATE|MNEMONIC] (e.g. -f, --from 1/1[/yyy]|n|t|y|today|tomorrow|yesterday)
92
+ option :from, type: :string, aliases: '-f', banner: 'DATE|MNEMONIC'
93
+ # -t, --to TO [DATE|MNEMONIC] (e.g. -t, --to 1/1[/yyy]|n|t|y|today|tomorrow|yesterday)
94
+ option :to, type: :string, aliases: '-t', banner: 'DATE|MNEMONIC'
95
+
96
+ # Include dates that have no DSU entries.
97
+ option :include_all, type: :boolean, aliases: '-a'
98
+ def dates
99
+ options = configuration.merge(self.options)
100
+ times = dsu_times_from!(from_command_option: options[:from], to_command_option: options[:to])
101
+ # Note special sort here, unlike the other commands where rules for
102
+ # displaying DSU entries are applied; this is more of a list command.
103
+ times = times_sort(times: times, entries_display_order: entries_display_order)
104
+ view_entry_groups(times: times, options: options) do |total_entry_groups|
105
+ nothing_to_display_banner_for(times) if total_entry_groups.zero?
63
106
  end
64
107
  rescue ArgumentError => e
65
108
  say "Error: #{e.message}", ERROR
66
109
  exit 1
67
110
  end
111
+
112
+ private
113
+
114
+ def nothing_to_display_banner_for(entry_group_times)
115
+ entry_group_times.sort!
116
+ time_range = "#{formatted_time(time: entry_group_times.first)} " \
117
+ "through #{formatted_time(time: entry_group_times.last)}"
118
+ say "(nothing to display for #{time_range})", INFO
119
+ end
120
+
121
+ # This method will unconditionally display the FIRST and LAST entry groups
122
+ # associated with the times provided by the <times> argument. All other
123
+ # entry groups will be conditionally displayed based on the :include_all
124
+ # value in the <options> argument.
125
+ def view_list_for(times:)
126
+ options = configuration.merge(self.options)
127
+ times_first_and_last = [times.first, times.last]
128
+ times.each do |time|
129
+ view_options = options.dup
130
+ view_options[:include_all] = true if times_first_and_last.include?(time)
131
+ view_entry_group(time: time, options: view_options) do
132
+ puts
133
+ end
134
+ end
135
+ end
68
136
  end
69
137
  end
70
138
  end
@@ -6,6 +6,7 @@ module Dsu
6
6
  ABORTED = :red
7
7
  ERROR = :red
8
8
  HIGHLIGHT = :cyan
9
+ INFO = HIGHLIGHT
9
10
  SUCCESS = :green
10
11
  WARNING = :yellow
11
12
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time'
4
+ require_relative 'time_mneumonic'
5
+ require_relative 'time_mneumonics'
6
+
7
+ module Dsu
8
+ module Support
9
+ module CommandOptions
10
+ module DsuTimes
11
+ include Time
12
+ include TimeMneumonic
13
+ include TimeMneumonics
14
+
15
+ # Returns an array of Time objects. The first element is the from time. The second element is the to time.
16
+ # Both arguments are expected to be command options that are time strings, time or relative time mneumonics.
17
+ def dsu_times_from!(from_command_option:, to_command_option:)
18
+ times = begin
19
+ from_time = time_from_mneumonic(command_option: from_command_option) if time_mneumonic?(from_command_option)
20
+ from_time ||= time_from_date_string(command_option: from_command_option)
21
+
22
+ to_time = time_from_mneumonic(command_option: to_command_option) if time_mneumonic?(to_command_option)
23
+ to_time ||= time_from_date_string(command_option: to_command_option)
24
+
25
+ [from_time, to_time].sort
26
+ end
27
+
28
+ (times.min.to_date..times.max.to_date).map(&:to_time)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end