dsu 1.2.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -21
  3. data/Gemfile.lock +7 -7
  4. data/README.md +28 -35
  5. data/bin/console +23 -1
  6. data/bin/dsu +3 -0
  7. data/bin/setup +14 -3
  8. data/exe/dsu +23 -1
  9. data/exe/dsu_migrate.rb +43 -0
  10. data/lib/core/ruby/color_theme_colors.rb +16 -0
  11. data/lib/core/ruby/color_theme_mode.rb +42 -0
  12. data/lib/core/ruby/not_today.rb +7 -0
  13. data/lib/core/ruby/wrap_and_join.rb +31 -0
  14. data/lib/dsu/base_cli.rb +19 -23
  15. data/lib/dsu/cli.rb +47 -37
  16. data/lib/dsu/command_services/add_entry_service.rb +10 -21
  17. data/lib/dsu/crud/json_file.rb +139 -0
  18. data/lib/dsu/crud/raw_json_file.rb +51 -0
  19. data/lib/dsu/env.rb +21 -0
  20. data/lib/dsu/migration/service.rb +196 -0
  21. data/lib/dsu/migration/version.rb +7 -0
  22. data/lib/dsu/models/color_theme.rb +270 -0
  23. data/lib/dsu/models/configuration.rb +160 -0
  24. data/lib/dsu/models/entry.rb +6 -2
  25. data/lib/dsu/models/entry_group.rb +143 -42
  26. data/lib/dsu/models/migration_version.rb +48 -0
  27. data/lib/dsu/presenters/base_presenter.rb +32 -0
  28. data/lib/dsu/presenters/color_theme_presenter.rb +50 -0
  29. data/lib/dsu/presenters/color_theme_show_presenter.rb +49 -0
  30. data/lib/dsu/presenters/configuration_presenter.rb +45 -0
  31. data/lib/dsu/presenters/entry_group_presenter.rb +35 -0
  32. data/lib/dsu/presenters/entry_presenter.rb +25 -0
  33. data/lib/dsu/services/color_theme/hydrator_service.rb +42 -0
  34. data/lib/dsu/services/configuration/hydrator_service.rb +42 -0
  35. data/lib/dsu/services/entry/hydrator_service.rb +33 -0
  36. data/lib/dsu/services/entry_group/editor_service.rb +107 -0
  37. data/lib/dsu/services/entry_group/hydrator_service.rb +37 -0
  38. data/lib/dsu/services/migration_version/hydrator_service.rb +36 -0
  39. data/lib/dsu/services/stderr_redirector_service.rb +27 -0
  40. data/lib/dsu/services/temp_file/reader_service.rb +33 -0
  41. data/lib/dsu/services/temp_file/writer_service.rb +35 -0
  42. data/lib/dsu/subcommands/base_subcommand.rb +14 -0
  43. data/lib/dsu/subcommands/config.rb +92 -32
  44. data/lib/dsu/subcommands/edit.rb +3 -3
  45. data/lib/dsu/subcommands/list.rb +70 -93
  46. data/lib/dsu/subcommands/theme.rb +159 -0
  47. data/lib/dsu/support/ask.rb +14 -19
  48. data/lib/dsu/support/color_themable.rb +34 -0
  49. data/lib/dsu/support/command_help_colorizeable.rb +27 -0
  50. data/lib/dsu/support/command_hookable.rb +60 -0
  51. data/lib/dsu/support/command_options/dsu_times.rb +32 -21
  52. data/lib/dsu/support/command_options/time.rb +7 -1
  53. data/lib/dsu/support/command_options/time_mneumonic.rb +7 -1
  54. data/lib/dsu/support/descriptable.rb +6 -4
  55. data/lib/dsu/support/entry_group_viewable.rb +28 -4
  56. data/lib/dsu/support/fileable.rb +94 -0
  57. data/lib/dsu/support/presentable.rb +11 -0
  58. data/lib/dsu/support/subcommand_help_colorizeable.rb +27 -0
  59. data/lib/dsu/support/time_comparable.rb +19 -0
  60. data/lib/dsu/support/time_formatable.rb +12 -0
  61. data/lib/dsu/support/times_sortable.rb +48 -14
  62. data/lib/dsu/support/utils.rb +11 -0
  63. data/lib/dsu/validators/color_theme_validator.rb +74 -0
  64. data/lib/dsu/validators/entries_validator.rb +4 -8
  65. data/lib/dsu/validators/version_validator.rb +29 -0
  66. data/lib/dsu/version.rb +2 -1
  67. data/lib/dsu/views/color_theme/index.rb +62 -0
  68. data/lib/dsu/views/color_theme/show.rb +106 -0
  69. data/lib/dsu/views/configuration/show.rb +41 -0
  70. data/lib/dsu/views/entry_group/edit.rb +3 -5
  71. data/lib/dsu/views/entry_group/shared/no_entries_to_display.rb +41 -0
  72. data/lib/dsu/views/entry_group/show.rb +16 -15
  73. data/lib/dsu/views/shared/error.rb +17 -0
  74. data/lib/dsu/views/shared/info.rb +17 -0
  75. data/lib/dsu/views/shared/message.rb +85 -0
  76. data/lib/dsu/views/shared/model_errors.rb +31 -0
  77. data/lib/dsu/views/shared/success.rb +17 -0
  78. data/lib/dsu/views/shared/warning.rb +17 -0
  79. data/lib/dsu.rb +22 -1
  80. data/lib/seed_data/themes/cherry.json +79 -0
  81. data/lib/seed_data/themes/default.json +79 -0
  82. data/lib/seed_data/themes/lemon.json +79 -0
  83. data/lib/seed_data/themes/matrix.json +79 -0
  84. data/lib/seed_data/themes/whiteout.json +79 -0
  85. metadata +68 -23
  86. data/lib/dsu/core/ruby/not_today.rb +0 -11
  87. data/lib/dsu/services/ai/tense_translator_service.rb +0 -63
  88. data/lib/dsu/services/configuration_loader_service.rb +0 -55
  89. data/lib/dsu/services/entry_group_deleter_service.rb +0 -31
  90. data/lib/dsu/services/entry_group_editor_service.rb +0 -96
  91. data/lib/dsu/services/entry_group_hydrator_service.rb +0 -43
  92. data/lib/dsu/services/entry_group_reader_service.rb +0 -36
  93. data/lib/dsu/services/entry_group_writer_service.rb +0 -46
  94. data/lib/dsu/services/entry_hydrator_service.rb +0 -35
  95. data/lib/dsu/services/temp_file_reader_service.rb +0 -31
  96. data/lib/dsu/services/temp_file_writer_service.rb +0 -33
  97. data/lib/dsu/support/colorable.rb +0 -14
  98. data/lib/dsu/support/configurable.rb +0 -15
  99. data/lib/dsu/support/configuration.rb +0 -112
  100. data/lib/dsu/support/entry_group_fileable.rb +0 -49
  101. data/lib/dsu/support/entry_group_loadable.rb +0 -49
  102. data/lib/dsu/support/folder_locations.rb +0 -21
  103. data/lib/dsu/support/say.rb +0 -40
  104. data/lib/dsu/views/edited_entries/shared/errors.rb +0 -39
  105. data/lib/dsu/views/shared/messages.rb +0 -56
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorized_string'
4
+
5
+ module Dsu
6
+ module Support
7
+ module ColorThemable
8
+ def prompt_with_options(prompt:, options:)
9
+ options = "[#{options.join('/')}]"
10
+ "#{apply_theme(prompt, theme_color: self.prompt)} " \
11
+ "#{apply_theme(options, theme_color: prompt_options)}" \
12
+ "#{apply_theme('>', theme_color: self.prompt)}"
13
+ end
14
+
15
+ module_function
16
+
17
+ def apply_theme(input, theme_color:)
18
+ if input.is_a?(Array)
19
+ return input.map do |string|
20
+ colorize_string(string, theme_color: theme_color)
21
+ end.join("\n")
22
+ end
23
+
24
+ colorize_string(input, theme_color: theme_color)
25
+ end
26
+
27
+ private
28
+
29
+ def colorize_string(input, theme_color:)
30
+ ColorizedString[input].colorize(**theme_color)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/color_theme'
4
+ require_relative '../support/color_themable'
5
+
6
+ module Dsu
7
+ module Support
8
+ module CommandHelpColorizable
9
+ class << self
10
+ def included(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def help(shell, subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
16
+ help_text = Services::StdoutRedirectorService.call { super }
17
+ puts apply_theme(help_text, theme_color: color_theme.help)
18
+ end
19
+
20
+ def color_theme
21
+ @color_theme ||= Models::ColorTheme.current_or_default
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/color_theme'
4
+ require_relative '../services/stderr_redirector_service'
5
+ require_relative '../views/shared/error'
6
+ require_relative 'color_themable'
7
+
8
+ module Dsu
9
+ module Support
10
+ module CommandHookable
11
+ class << self
12
+ def included(base)
13
+ base.extend(ColorThemable)
14
+ base.extend(ClassMethods)
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def start(args = ARGV, options = {})
20
+ display_dsu_header unless suspend_header?(args, options)
21
+ stderror = Services::StderrRedirectorService.call do
22
+ super
23
+ end
24
+ display_errors_if(stderror)
25
+ display_dsu_footer
26
+ end
27
+
28
+ def display_dsu_header
29
+ puts apply_theme("Dsu v#{Dsu::VERSION}", theme_color: color_theme.dsu_header)
30
+ puts
31
+ end
32
+
33
+ private
34
+
35
+ def suspend_header?(args, _options)
36
+ return unless args.count > 1
37
+ return true if args[0] == 'theme' && %w[use delete].include?(args[1])
38
+ end
39
+
40
+ def display_dsu_footer
41
+ puts apply_theme('_' * 35, theme_color: color_theme.dsu_footer)
42
+ footer = apply_theme("Theme: #{color_theme.theme_name}", theme_color: color_theme.dsu_footer)
43
+ puts footer
44
+ end
45
+
46
+ def display_errors_if(stderror_string)
47
+ stderror_string = stderror_string.strip
48
+ return unless stderror_string.present?
49
+
50
+ errors = stderror_string.split("\n").map(&:strip)
51
+ Views::Shared::Error.new(messages: errors, options: options.merge({ ordered_list: false })).render
52
+ end
53
+
54
+ def color_theme
55
+ Models::ColorTheme.current_or_default
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -2,34 +2,45 @@
2
2
 
3
3
  require_relative 'time'
4
4
  require_relative 'time_mneumonic'
5
- require_relative 'time_mneumonics'
6
5
 
7
6
  module Dsu
8
7
  module Support
9
8
  module CommandOptions
10
9
  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 = if relative_time_mneumonic?(to_command_option)
23
- time_from_mneumonic(command_option: to_command_option, relative_time: from_time)
24
- elsif time_mneumonic?(to_command_option)
25
- time_from_mneumonic(command_option: to_command_option)
26
- end
27
- to_time ||= time_from_date_string(command_option: to_command_option)
28
-
29
- [from_time, to_time].sort
10
+ module_function
11
+
12
+ # Returns an array of Time objects. The first element is the "from" time.
13
+ # The second element is the "to" time. Both arguments are expected to be
14
+ # command options that are time strings, time or relative time mneumonics.
15
+ def dsu_times_for(from_option:, to_option:)
16
+ from_time = dsu_from_time_for(from_option: from_option)
17
+ to_time = dsu_to_time_for(to_option: to_option, from_time: from_time)
18
+
19
+ errors = []
20
+ errors << "Option -f, [--from=DATE|MNEMONIC] value is invalid [\"#{from_option}\"]" if from_time.nil?
21
+ errors << "Option -t, [--to=DATE|MNEMONIC] value is invalid [\"#{to_option}\"]" if to_time.nil?
22
+ return [[], errors] if errors.any?
23
+
24
+ min_time, max_time = [from_time, to_time].sort
25
+ [(min_time.to_date..max_time.to_date).map(&:to_time), []]
26
+ end
27
+
28
+ def dsu_from_time_for(from_option:)
29
+ return if from_option.nil?
30
+
31
+ from_time = if TimeMneumonic.time_mneumonic?(from_option)
32
+ TimeMneumonic.time_from_mneumonic(command_option: from_option)
30
33
  end
34
+ from_time || Time.time_from_date_string(command_option: from_option)
35
+ end
31
36
 
32
- (times.min.to_date..times.max.to_date).map(&:to_time)
37
+ def dsu_to_time_for(to_option:, from_time:)
38
+ to_time = if TimeMneumonic.relative_time_mneumonic?(to_option)
39
+ TimeMneumonic.time_from_mneumonic(command_option: to_option, relative_time: from_time)
40
+ elsif TimeMneumonic.time_mneumonic?(to_option)
41
+ TimeMneumonic.time_from_mneumonic(command_option: to_option)
42
+ end
43
+ to_time || Time.time_from_date_string(command_option: to_option)
33
44
  end
34
45
  end
35
46
  end
@@ -3,6 +3,8 @@
3
3
  module Dsu
4
4
  module Support
5
5
  module CommandOptions
6
+ # TODO: Make this into an ActiveModel class that uses validations.
7
+ #
6
8
  # The purpose of this module is to take a command option that is a string and return a Time object.
7
9
  # The command option is expected to be a date in the format of [M]M/[D]D[/YYYY]. MM and DD with
8
10
  # leading zeroes is optional (i.e. only M and D are required), YYYY is optionl and will be replaced
@@ -10,6 +12,8 @@ module Dsu
10
12
  module Time
11
13
  DATE_CAPTURE_REGEX = %r{\A(?<month>0?[1-9]|1[0-2])/(?<day>0?[1-9]|1\d|2\d|3[01])(?:/(?<year>\d{4}))?\z}
12
14
 
15
+ module_function
16
+
13
17
  def time_from_date_string!(command_option:)
14
18
  raise ArgumentError, 'command_option is nil.' if command_option.nil?
15
19
  raise ArgumentError, 'command_option is blank.' if command_option.blank?
@@ -35,7 +39,7 @@ module Dsu
35
39
  nil
36
40
  end
37
41
 
38
- private
42
+ # private_class_methods go here.
39
43
 
40
44
  # This method returns the time parts for the given time string in a hash
41
45
  # (i.e. month, day, year) IF the time string matches the DATE_CAPTURE_REGEX
@@ -71,6 +75,8 @@ module Dsu
71
75
  time_parts[:year] = ::Time.now.year if time_parts[:year].nil?
72
76
  "#{time_parts[:year]}/#{time_parts[:month]}/#{time_parts[:day]}"
73
77
  end
78
+
79
+ private_class_method :time_parts_for, :time_parts?, :valid_time!, :time_string_for
74
80
  end
75
81
  end
76
82
  end
@@ -10,6 +10,8 @@ module Dsu
10
10
  module TimeMneumonic
11
11
  include TimeMneumonics
12
12
 
13
+ module_function
14
+
13
15
  def time_from_mneumonic(command_option:, relative_time: nil)
14
16
  time_from_mneumonic!(command_option: command_option, relative_time: relative_time)
15
17
  rescue ArgumentError
@@ -45,7 +47,7 @@ module Dsu
45
47
  mneumonic.match?(RELATIVE_REGEX)
46
48
  end
47
49
 
48
- private
50
+ # Add private_class_methods here.
49
51
 
50
52
  # Returns a Time object from a mneumonic.
51
53
  def time_for_mneumonic(mneumonic:, relative_time:)
@@ -96,6 +98,10 @@ module Dsu
96
98
  raise ArgumentError, "#{command_option_name} is an invalid mneumonic: \"#{command_option}\"."
97
99
  end
98
100
  end
101
+
102
+ private_class_method :time_for_mneumonic, :relative_time_for,
103
+ :mneumonic?, :today_mneumonic?, :tomorrow_mneumonic?,
104
+ :yesterday_mneumonic?, :validate_argument!
99
105
  end
100
106
  end
101
107
  end
@@ -3,9 +3,11 @@
3
3
  module Dsu
4
4
  module Support
5
5
  module Descriptable
6
+ DESCRIPTION_MAX_COUNT = 25
7
+
6
8
  class << self
7
- def included(mod)
8
- mod.extend(ClassMethods)
9
+ def included(base)
10
+ base.extend(ClassMethods)
9
11
  end
10
12
  end
11
13
 
@@ -16,7 +18,7 @@ module Dsu
16
18
  end
17
19
 
18
20
  module ClassMethods
19
- def short_description(string:, count: 25, elipsis: '...')
21
+ def short_description(string:, count: DESCRIPTION_MAX_COUNT, elipsis: '...')
20
22
  return elipsis unless string.is_a?(String)
21
23
 
22
24
  elipsis_length = elipsis.length
@@ -27,7 +29,7 @@ module Dsu
27
29
  tokens = string.split
28
30
  string = ''
29
31
 
30
- return "#{tokens.first[0..(count - elipsis_length)]}#{elipsis}" if tokens.count == 1
32
+ return "#{tokens.first[0...(count - elipsis_length)]}#{elipsis}" if tokens.count == 1
31
33
 
32
34
  tokens.each do |token|
33
35
  break if string.length + token.length + elipsis_length > count
@@ -4,6 +4,9 @@ module Dsu
4
4
  module Support
5
5
  module EntryGroupViewable
6
6
  def view_entry_groups(times:, options: {})
7
+ raise ArgumentError, 'times must be an Array' unless times.is_a?(Array)
8
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
9
+
7
10
  total_viewable_entry_groups = 0
8
11
 
9
12
  times.each do |time|
@@ -13,25 +16,46 @@ module Dsu
13
16
  end
14
17
  end
15
18
 
16
- yield total_viewable_entry_groups if block_given?
19
+ total_unviewable_entry_groups = times.size - total_viewable_entry_groups
20
+ yield total_viewable_entry_groups, total_unviewable_entry_groups if block_given?
17
21
  end
18
22
 
19
23
  def view_entry_group(time:, options: {})
24
+ raise ArgumentError, 'time must be a Time object' unless time.is_a?(Time)
25
+ raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash)
26
+
20
27
  return unless show_entry_group?(time: time, options: options)
21
28
 
22
- entry_group = Models::EntryGroup.load(time: time)
29
+ entry_group = Models::EntryGroup.find_or_initialize(time: time)
23
30
  Views::EntryGroup::Show.new(entry_group: entry_group).render
24
31
 
25
32
  yield if block_given?
26
33
  end
27
34
 
35
+ # This method will unconditionally display the FIRST and LAST entry groups
36
+ # associated with the times provided by the <times> argument. All other
37
+ # entry groups will be conditionally displayed based on the :include_all
38
+ # value in the <options> argument.
39
+ def view_list_for(times:, options:)
40
+ configuration = Models::Configuration.new unless defined?(configuration) && configuration
41
+ options = configuration.to_h.merge(options).with_indifferent_access
42
+ times_first_and_last = [times.first, times.last]
43
+ times.each do |time|
44
+ view_options = options.dup
45
+ view_options[:include_all] = true if times_first_and_last.include?(time)
46
+ view_entry_group(time: time, options: view_options) do
47
+ puts
48
+ end
49
+ end
50
+ end
51
+
28
52
  private
29
53
 
30
54
  def show_entry_group?(time:, options:)
31
- Models::EntryGroup.exists?(time: time) || options[:include_all]
55
+ Models::EntryGroup.exist?(time: time) || options[:include_all]
32
56
  end
33
57
 
34
- module_function :view_entry_group, :view_entry_groups, :show_entry_group?
58
+ module_function :view_entry_group, :view_entry_groups, :view_list_for, :show_entry_group?
35
59
  end
36
60
  end
37
61
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module Fileable
6
+ MIGRATION_VERSION_FILE_NAME = 'migration_version.json'
7
+
8
+ def dsu_folder
9
+ File.join(root_folder, 'dsu')
10
+ end
11
+
12
+ # Configuration
13
+
14
+ def config_folder
15
+ root_folder
16
+ end
17
+
18
+ def config_file_name
19
+ '.dsu'
20
+ end
21
+
22
+ def config_path
23
+ File.join(config_folder, config_file_name)
24
+ end
25
+
26
+ # Entries
27
+
28
+ def entries_folder
29
+ File.join(dsu_folder, 'entries')
30
+ end
31
+
32
+ def entries_file_name(time:, file_name_format: nil)
33
+ file_name_format ||= '%Y-%m-%d.json'
34
+ time.strftime(file_name_format)
35
+ end
36
+
37
+ def entries_path(time:, file_name_format: nil)
38
+ File.join(entries_folder, entries_file_name(time: time, file_name_format: file_name_format))
39
+ end
40
+
41
+ # Themes
42
+
43
+ def themes_folder
44
+ File.join(dsu_folder, 'themes')
45
+ end
46
+
47
+ def themes_path(theme_name:)
48
+ File.join(themes_folder, theme_file_name(theme_name: theme_name))
49
+ end
50
+
51
+ def theme_file_name(theme_name:)
52
+ "#{theme_name}.json"
53
+ end
54
+
55
+ # Migration
56
+
57
+ def migration_version_folder
58
+ File.join(dsu_folder)
59
+ end
60
+
61
+ def migration_version_path
62
+ File.join(migration_version_folder, MIGRATION_VERSION_FILE_NAME)
63
+ end
64
+
65
+ # Base folders
66
+
67
+ def root_folder
68
+ Dir.home
69
+ end
70
+
71
+ def temp_folder
72
+ Dir.tmpdir
73
+ end
74
+
75
+ def gem_dir
76
+ Gem.loaded_specs['dsu'].gem_dir
77
+ end
78
+
79
+ # Back up folder
80
+
81
+ def backup_folder(version:)
82
+ File.join(dsu_folder, 'backup', version.to_s)
83
+ end
84
+
85
+ # Seed data folders
86
+
87
+ def seed_data_folder
88
+ File.join(gem_dir, 'lib/seed_data')
89
+ end
90
+
91
+ extend self # rubocop:disable Style/ModuleFunction
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module Presentable
6
+ def presenter
7
+ "Dsu::Presenters::#{self.class.name.demodulize}Presenter".constantize.new(self, options: options)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/color_theme'
4
+ require_relative '../support/color_themable'
5
+
6
+ module Dsu
7
+ module Support
8
+ module SubcommandHelpColorizable
9
+ class << self
10
+ def included(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def command_help(shell, subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
16
+ help_text = Services::StdoutRedirectorService.call { super }
17
+ puts apply_theme(help_text, theme_color: color_theme.help)
18
+ end
19
+
20
+ def color_theme
21
+ @color_theme ||= Models::ColorTheme.current_or_default
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module TimeComparable
6
+ TIME_COMPARABLE_FORMAT_SPECIFIER = '%Y%m%d'
7
+
8
+ def time_equal?(other_time:)
9
+ time_equal_compare_string_for(time: time) == time_equal_compare_string_for(time: other_time)
10
+ end
11
+
12
+ def time_equal_compare_string_for(time:)
13
+ time = time.localtime if time.utc?
14
+
15
+ time.strftime(TIME_COMPARABLE_FORMAT_SPECIFIER)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -28,9 +28,21 @@ module Dsu
28
28
  time.strftime("%A, (#{today_yesterday_or_tomorrow}) %Y-%m-%d #{time_zone}")
29
29
  end
30
30
 
31
+ def mm_dd(time:, separator: '/')
32
+ time.strftime("%m#{separator}%d")
33
+ end
34
+
35
+ def mm_dd_yyyy(time:, separator: '/')
36
+ time.strftime("%m#{separator}%d#{separator}%Y")
37
+ end
38
+
31
39
  def timezone_for(time:)
32
40
  time.zone
33
41
  end
42
+
43
+ def yyyy_mm_dd(time:, separator: '-')
44
+ time.strftime("%Y#{separator}%m#{separator}%d")
45
+ end
34
46
  end
35
47
  end
36
48
  end
@@ -3,29 +3,41 @@
3
3
  module Dsu
4
4
  module Support
5
5
  module TimesSortable
6
- module_function
6
+ def sorted_dsu_times_for(times:)
7
+ configuration = Models::Configuration.new unless defined?(configuration) && configuration
8
+ entries_display_order = configuration.entries_display_order
9
+ times_sort(times: times_for(times: times), entries_display_order: entries_display_order)
10
+ end
7
11
 
8
12
  def times_sort(times:, entries_display_order: nil)
9
- entries_display_order ||= 'asc'
10
- unless %w[asc desc].include? entries_display_order
11
- raise "Invalid entries_display_order: #{entries_display_order}"
12
- end
13
+ times = times.dup
14
+ entries_display_order ||= :asc
13
15
 
14
- if entries_display_order == 'asc'
15
- times.sort # sort ascending
16
- elsif entries_display_order == 'desc'
17
- times.sort.reverse # sort descending
18
- end
16
+ validate_times_argument!(times: times)
17
+ validate_entries_display_order_argument!(entries_display_order: entries_display_order)
18
+
19
+ return times if times.one?
20
+
21
+ # NOTE: The times array needs to be sorted unconditionally because if
22
+ # the sort is ascending, then the times array needs to be returned
23
+ # in ascending order. If the sort is descending, then in order to
24
+ # properly reverse the times array, it needs to first be sorted in
25
+ # ascending order before being reversed.
26
+ times.sort!
27
+ times.reverse! if entries_display_order == :desc
28
+
29
+ times
19
30
  end
20
31
 
21
- # TODO: Do we have something else we can use here?
22
32
  def times_for(times:)
33
+ times = times.dup
34
+ validate_times_argument!(times: times)
35
+
23
36
  start_date = times.max
24
37
  return times unless start_date.monday? || start_date.on_weekend?
25
38
 
26
- # If the start_date is a weekend or a Monday, then we need to include
27
- # start_date along with all the dates up to and including the previous
28
- # Monday.
39
+ # If the start date is a weekend or a Monday then we need to look back
40
+ # to include the preceeding Friday upto and including the start date.
29
41
  (0..3).filter_map do |num|
30
42
  time = start_date - num.days
31
43
  next unless time == start_date || time.on_weekend? || time.friday?
@@ -33,6 +45,28 @@ module Dsu
33
45
  time
34
46
  end
35
47
  end
48
+
49
+ private
50
+
51
+ def validate_times_argument!(times:)
52
+ raise ArgumentError, "times is the wrong object type: \"#{times.class}\"" unless times.is_a?(Array)
53
+ raise ArgumentError, 'times is empty' if times.empty?
54
+ end
55
+
56
+ def validate_entries_display_order_argument!(entries_display_order:)
57
+ unless entries_display_order.nil? || entries_display_order.is_a?(Symbol)
58
+ raise ArgumentError, "entries_display_order is the wrong object type: \"#{entries_display_order.class}\""
59
+ end
60
+
61
+ unless %i[asc desc].include?(entries_display_order)
62
+ raise ArgumentError, "entries_display_order is invalid: \":#{entries_display_order}\""
63
+ end
64
+ end
65
+
66
+ # NOTE: This, as opposed to using module_function, so that we can
67
+ # invoke .validate_times_sort_arguments! from the .times_sort
68
+ # method with module as the receiver AND when included as a mixin.
69
+ extend self
36
70
  end
37
71
  end
38
72
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Utils
5
+ class << self
6
+ def strip_escapes(escaped_string)
7
+ escaped_string.gsub(/\e\[[0-9;]*[a-zA-Z]/, '')
8
+ end
9
+ end
10
+ end
11
+ end