csv_plus_plus 0.1.2 → 0.1.3

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +9 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +23 -13
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +32 -7
  9. data/lib/csv_plus_plus/compiler.rb +82 -60
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +27 -43
  11. data/lib/csv_plus_plus/entities/boolean.rb +18 -9
  12. data/lib/csv_plus_plus/entities/builtins.rb +23 -9
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +200 -29
  14. data/lib/csv_plus_plus/entities/date.rb +38 -5
  15. data/lib/csv_plus_plus/entities/entity.rb +27 -61
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +23 -11
  18. data/lib/csv_plus_plus/entities/function_call.rb +24 -9
  19. data/lib/csv_plus_plus/entities/number.rb +24 -10
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +22 -5
  21. data/lib/csv_plus_plus/entities/string.rb +19 -6
  22. data/lib/csv_plus_plus/entities/variable.rb +16 -4
  23. data/lib/csv_plus_plus/entities.rb +20 -13
  24. data/lib/csv_plus_plus/error/error.rb +11 -1
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +1 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +53 -5
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +34 -14
  28. data/lib/csv_plus_plus/error/syntax_error.rb +22 -9
  29. data/lib/csv_plus_plus/error/writer_error.rb +8 -0
  30. data/lib/csv_plus_plus/error.rb +1 -0
  31. data/lib/csv_plus_plus/google_api_client.rb +7 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +8 -4
  34. data/lib/csv_plus_plus/lexer/tokenizer.rb +6 -1
  35. data/lib/csv_plus_plus/lexer.rb +24 -0
  36. data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
  37. data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
  38. data/lib/csv_plus_plus/modifier/expand.rb +61 -0
  39. data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
  40. data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
  41. data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
  42. data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
  43. data/lib/csv_plus_plus/modifier.rb +82 -158
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/parser/cell_value.tab.rb +5 -5
  46. data/lib/csv_plus_plus/parser/code_section.tab.rb +8 -13
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +17 -23
  48. data/lib/csv_plus_plus/row.rb +53 -12
  49. data/lib/csv_plus_plus/runtime/can_define_references.rb +87 -0
  50. data/lib/csv_plus_plus/runtime/can_resolve_references.rb +209 -0
  51. data/lib/csv_plus_plus/runtime/graph.rb +68 -0
  52. data/lib/csv_plus_plus/runtime/position_tracker.rb +231 -0
  53. data/lib/csv_plus_plus/runtime/references.rb +110 -0
  54. data/lib/csv_plus_plus/runtime/runtime.rb +126 -0
  55. data/lib/csv_plus_plus/runtime.rb +34 -191
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +62 -35
  58. data/lib/csv_plus_plus/version.rb +2 -1
  59. data/lib/csv_plus_plus/writer/base_writer.rb +30 -5
  60. data/lib/csv_plus_plus/writer/csv.rb +11 -9
  61. data/lib/csv_plus_plus/writer/excel.rb +9 -2
  62. data/lib/csv_plus_plus/writer/file_backer_upper.rb +1 -0
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +71 -23
  64. data/lib/csv_plus_plus/writer/google_sheets.rb +79 -29
  65. data/lib/csv_plus_plus/writer/open_document.rb +6 -1
  66. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +103 -30
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +29 -12
  69. metadata +18 -14
  70. data/lib/csv_plus_plus/can_define_references.rb +0 -88
  71. data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
  72. data/lib/csv_plus_plus/data_validation.rb +0 -138
  73. data/lib/csv_plus_plus/expand.rb +0 -20
  74. data/lib/csv_plus_plus/graph.rb +0 -62
  75. data/lib/csv_plus_plus/references.rb +0 -68
  76. data/lib/csv_plus_plus/scope.rb +0 -196
  77. data/lib/csv_plus_plus/validated_modifier.rb +0 -164
  78. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
  79. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d0fb79a1f7a00d60f77289518fb9c6ccac6b059024d5cf66a3f083ec92e77def
4
- data.tar.gz: a35796b982b01171636e27b1d1a9d19d6d414249b0e4634947d5a754cb240146
3
+ metadata.gz: abee03db0f21f9d8c5d3cfb4ec81dbd748568f878458f355cab6db5fc458a509
4
+ data.tar.gz: 6c4e95fa1c780a2d009eaeb58fd48b28c2dd96826b22be02f84d5fc8a06ad31e
5
5
  SHA512:
6
- metadata.gz: 45dd804f7889d65ac5f5c63ccc131d774e7ea24097bfe9449af63a48345ca0b9f16ea9e84c17a73151c9ee8d9d90ce4b01d67b7ced817e831e66781d2e7141e0
7
- data.tar.gz: 21cda263af6d05d5ee396a9d00b4c1c78f1a043d91787dcf0d822e1adb6a97a2326da836cdc3bbc3c78e43e5c342c1bdb9da966884f1d9d8df364e574f334ded
6
+ metadata.gz: 59f2cb756bdfd20f95f252aa1c07fb4b994def21f07d8bc4e5a976b22f0b439fb950e1723fdb34753baf23527e5950d244f1386e9231c6318b35de3974305ba4
7
+ data.tar.gz: 38b6d6d960aa17978adf2ee1724333754e7cdc86b8e41fa58522357448db8448d5441fb5961d3758af94d617b94b21b197b5e4d2d4eb2dba0cbc39da586c4413
data/README.md CHANGED
@@ -29,7 +29,6 @@ And can be compiled into a `.xlsx` file by:
29
29
  $ csv++ -n 'My Stock Tracker' -o mystocks.xlsx mystocks.csvpp
30
30
  ```
31
31
 
32
-
33
32
  See the [Language Reference](./docs/LANGUAGE_REFERENCE.md) for a full explanation of features.
34
33
 
35
34
  ## Installing
@@ -38,7 +37,7 @@ Just install it via rubygems (homebrew and debian packages are in the works):
38
37
 
39
38
  `$ gem install csv_plus_plus`
40
39
 
41
- or if you want the very latest changes, clone the repository and run:
40
+ or if you want the very latest changes, clone this repository and run:
42
41
 
43
42
  `$ rake gem:install`
44
43
 
@@ -1,3 +1,12 @@
1
+ ## v0.1.3
2
+
3
+ - Proper scoping of variables defined within an expand modifier
4
+ - Types via Sorbet
5
+ - Fix formula insertion on Excel
6
+ - Fix modifier string quoting
7
+ - Fix broken Yard doc generation
8
+ - Fix: multiple modifiers on the same row weren't being handled
9
+
1
10
  ## v0.1.2
2
11
 
3
12
  - var=... modifier which allows binding a variable to a cell
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -5,57 +6,106 @@ module CSVPlusPlus
5
6
  #
6
7
  # @attr_reader timings [Array<Benchmark::Tms>] +Benchmark+ timings that have been accumulated by each step of
7
8
  # compilation
8
- # @attr_reader benchmark [Benchmark] A +Benchmark+ instance
9
- module BenchmarkedCompiler
10
- attr_reader :benchmark, :timings
9
+ # @attr_reader benchmark [Benchmark::Report] A +Benchmark+ instance
10
+ class BenchmarkedCompiler < ::CSVPlusPlus::Compiler
11
+ extend ::T::Sig
11
12
 
12
- # Wrap a +Compiler+ with our instance methods that add benchmarks
13
- def self.with_benchmarks(compiler, &block)
14
- ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
15
- # compiler = new(options:, runtime:, benchmark: x)
16
- compiler.extend(self)
17
- compiler.benchmark = x
13
+ sig { returns(::Benchmark::Report) }
14
+ attr_reader :benchmark
18
15
 
19
- block.call(compiler)
16
+ sig { returns(::T::Array[::Benchmark::Tms]) }
17
+ attr_reader :timings
20
18
 
19
+ sig do
20
+ params(
21
+ options: ::CSVPlusPlus::Options,
22
+ runtime: ::CSVPlusPlus::Runtime::Runtime,
23
+ block: ::T.proc.params(compiler: ::CSVPlusPlus::Compiler).void
24
+ ).void
25
+ end
26
+ # Instantiate a +::Compiler+ that can benchmark (time) it's stages. For better or worse, the only way that they
27
+ # Benchmark library exposes it's +::Benchmark::Report+ is via a block, so this code also has to wrap with one
28
+ #
29
+ # @param options [Options]
30
+ # @param runtime [Runtime]
31
+ def self.with_benchmarks(options:, runtime:, &block)
32
+ ::Benchmark.benchmark(::Benchmark::CAPTION, 25, ::Benchmark::FORMAT, '> Total') do |x|
33
+ # compiler.extend(self)
34
+ compiler = new(benchmark: x, options:, runtime:)
35
+ block.call(compiler)
21
36
  [compiler.timings.reduce(:+)]
22
37
  end
23
38
  end
24
39
 
25
- # @param benchmark [Benchmark] A +Benchmark+ instance
26
- def benchmark=(benchmark)
27
- @benchmark = benchmark
28
- @timings = []
40
+ sig do
41
+ params(
42
+ benchmark: ::Benchmark::Report,
43
+ options: ::CSVPlusPlus::Options,
44
+ runtime: ::CSVPlusPlus::Runtime::Runtime
45
+ ).void
29
46
  end
47
+ # @param benchmark [::Benchmark::Report]
48
+ def initialize(benchmark:, options:, runtime:)
49
+ super(options:, runtime:)
30
50
 
51
+ @benchmark = ::T.let(benchmark, ::Benchmark::Report)
52
+ @timings = ::T.let([], ::T::Array[::Benchmark::Tms])
53
+ end
54
+
55
+ sig { override.params(block: ::T.proc.params(runtime: ::CSVPlusPlus::Runtime::Runtime).void).void }
31
56
  # Time the Compiler#outputting! stage
32
- def outputting!
33
- time_stage('Writing the spreadsheet') { super }
57
+ # rubocop:disable Naming/BlockForwarding
58
+ def outputting!(&block)
59
+ time_stage('Writing the spreadsheet') { super(&block) }
34
60
  end
61
+ # rubocop:enable Naming/BlockForwarding
35
62
 
36
63
  protected
37
64
 
65
+ sig { override.void }
38
66
  def parse_code_section!
39
67
  time_stage('Parsing code section') { super }
40
68
  end
41
69
 
70
+ sig { override.returns(::T::Array[::CSVPlusPlus::Row]) }
42
71
  def parse_csv_section!
43
72
  time_stage('Parsing CSV section') { super }
44
73
  end
45
74
 
46
- def expanding
47
- time_stage('Expanding rows') { super }
75
+ sig { override.params(block: ::T.proc.void).void }
76
+ # rubocop:disable Naming/BlockForwarding
77
+ def expanding!(&block)
78
+ time_stage('Expanding rows') { super(&block) }
79
+ end
80
+ # rubocop:enable Naming/BlockForwarding
81
+
82
+ sig { override.params(block: ::T.proc.void).void }
83
+ # rubocop:disable Naming/BlockForwarding
84
+ def bind_all_vars!(&block)
85
+ time_stage('Binding [[var=]]') { super(&block) }
48
86
  end
87
+ # rubocop:enable Naming/BlockForwarding
49
88
 
89
+ sig do
90
+ override
91
+ .params(template: ::CSVPlusPlus::Template)
92
+ .returns(::T::Array[::T::Array[::CSVPlusPlus::Entities::Entity]])
93
+ end
50
94
  def resolve_all_cells!(template)
51
95
  time_stage('Resolving each cell') { super(template) }
52
96
  end
53
97
 
54
98
  private
55
99
 
100
+ sig do
101
+ type_parameters(:R).params(
102
+ stage: ::String,
103
+ block: ::T.proc.returns(::T.type_parameter(:R))
104
+ ).returns(::T.nilable(::T.type_parameter(:R)))
105
+ end
56
106
  def time_stage(stage, &block)
57
- ret = nil
58
- @timings << @benchmark.report(stage) { ret = block.call }
107
+ ret = ::T.let(nil, ::T.nilable(::T.type_parameter(:R)))
108
+ @timings << ::T.unsafe(@benchmark.report(stage) { ret = block.call })
59
109
  ret
60
110
  end
61
111
  end
@@ -1,8 +1,6 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative 'modifier'
4
- require_relative 'parser/cell_value.tab'
5
-
6
4
  module CSVPlusPlus
7
5
  # A cell of a template
8
6
  #
@@ -11,9 +9,27 @@ module CSVPlusPlus
11
9
  # @attr_reader index [Integer] The cell's index (starts at 0)
12
10
  # @attr_reader modifier [Modifier] The modifier for this cell
13
11
  class Cell
14
- attr_accessor :ast, :row_index
15
- attr_reader :index, :modifier
12
+ extend ::T::Sig
13
+
14
+ sig { returns(::T.nilable(::CSVPlusPlus::Entities::Entity)) }
15
+ attr_accessor :ast
16
+
17
+ sig { returns(::Integer) }
18
+ attr_accessor :row_index
19
+
20
+ sig { returns(::Integer) }
21
+ attr_reader :index
16
22
 
23
+ sig { returns(::CSVPlusPlus::Modifier::Modifier) }
24
+ attr_reader :modifier
25
+
26
+ sig do
27
+ params(
28
+ value: ::T.nilable(::String),
29
+ runtime: ::CSVPlusPlus::Runtime::Runtime,
30
+ modifier: ::CSVPlusPlus::Modifier::Modifier
31
+ ).returns(::CSVPlusPlus::Cell)
32
+ end
17
33
  # Parse a +value+ into a Cell object.
18
34
  #
19
35
  # @param value [String] A string value which should already have been processed through a CSV parser
@@ -23,45 +39,51 @@ module CSVPlusPlus
23
39
  # @return [Cell]
24
40
  def self.parse(value, runtime:, modifier:)
25
41
  new(value:, row_index: runtime.row_index, index: runtime.cell_index, modifier:).tap do |c|
26
- c.ast = ::CSVPlusPlus::Parser::CellValue.new.parse(value, runtime)
42
+ c.ast = ::T.unsafe(::CSVPlusPlus::Parser::CellValue.new).parse(value, runtime)
27
43
  end
28
44
  end
29
45
 
30
- # @param row_index [Integer] The cell's row index (starts at 0)
46
+ sig do
47
+ params(
48
+ index: ::Integer,
49
+ modifier: ::CSVPlusPlus::Modifier::Modifier,
50
+ row_index: ::Integer,
51
+ value: ::T.nilable(::String)
52
+ ).void
53
+ end
31
54
  # @param index [Integer] The cell's index (starts at 0)
32
- # @param value [String] A string value which should already have been processed through a CSV parser
33
55
  # @param modifier [Modifier] A modifier to apply to this cell
34
- def initialize(row_index:, index:, value:, modifier:)
56
+ # @param row_index [Integer] The cell's row index (starts at 0)
57
+ # @param value [String] A string value which should already have been processed through a CSV parser
58
+ def initialize(index:, modifier:, row_index:, value:)
35
59
  @value = value
36
60
  @modifier = modifier
37
61
  @index = index
38
62
  @row_index = row_index
39
63
  end
40
64
 
65
+ sig { returns(::T.nilable(::String)) }
41
66
  # The +@value+ (cleaned up some)
42
67
  #
43
- # @return [String]
68
+ # @return [::String]
69
+ # TODO: is this used?
44
70
  def value
45
- return if @value.nil? || @value.strip.empty?
71
+ stripped = @value&.strip
46
72
 
47
- @value.strip
73
+ stripped&.empty? ? nil : stripped
48
74
  end
49
75
 
50
- # @return [String]
51
- def to_s
52
- "Cell(index: #{@index}, row_index: #{@row_index}, value: #{@value}, modifier: #{@modifier})"
53
- end
54
-
55
- # A compiled final representation of the cell. This can only happen after all cell have had
56
- # variables and functions resolved.
76
+ sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::String)) }
77
+ # A compiled final representation of the cell. This can only happen after all cell have had variables and functions
78
+ # resolved.
79
+ #
80
+ # @param runtime [Runtime]
57
81
  #
58
- # @return [String]
59
- def to_csv
82
+ # @return [::String]
83
+ def evaluate(runtime)
60
84
  return value unless @ast
61
85
 
62
- # This looks really simple but we're relying on each node of the AST to define #to_s such that calling
63
- # this at the top will recursively print the tree (as a well-formatted spreadsheet formula)
64
- "=#{@ast}"
86
+ "=#{@ast.evaluate(runtime)}"
65
87
  end
66
88
  end
67
89
  end
@@ -1,32 +1,42 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require 'optparse'
4
-
5
4
  module CSVPlusPlus
6
5
  # Handle running the application with the given CLI flags
7
6
  #
8
7
  # @attr options [Options, nil] The parsed CLI options
9
8
  class CLI
9
+ extend ::T::Sig
10
+
11
+ sig { returns(::CSVPlusPlus::Options) }
10
12
  attr_accessor :options
11
13
 
14
+ sig { void }
12
15
  # Handle CLI flags and launch the compiler
13
16
  #
14
17
  # @return [CLI]
15
18
  def self.launch_compiler!
16
19
  cli = new
17
- cli.parse_options!
18
20
  cli.main
19
21
  rescue ::StandardError => e
20
- cli.handle_error(e)
22
+ ::T.must(cli).handle_error(e)
21
23
  exit(1)
22
24
  end
23
25
 
26
+ sig { void }
27
+ # Initialize and parse the CLI flags provided to the program
28
+ def initialize
29
+ @options = ::T.let(::CSVPlusPlus::Options.new, ::CSVPlusPlus::Options)
30
+ parse_options!
31
+ end
32
+
33
+ sig { void }
24
34
  # Compile the given template using the given CLI flags
25
35
  def main
26
- parse_options! unless @options
27
36
  ::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, @options)
28
37
  end
29
38
 
39
+ sig { params(error: ::StandardError).void }
30
40
  # Nicely handle a given error. How it's handled depends on if it's our error and if @options.verbose
31
41
  #
32
42
  # @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
@@ -44,22 +54,18 @@ module CSVPlusPlus
44
54
  end
45
55
  end
46
56
 
57
+ private
58
+
59
+ sig { void }
47
60
  # Handle the supplied command line options, setting +@options+ or throw an error if anything is invalid
48
61
  def parse_options!
49
- @options = ::CSVPlusPlus::Options.new
50
62
  option_parser.parse!
51
63
  validate_options
52
64
  rescue ::OptionParser::InvalidOption => e
53
65
  raise(::CSVPlusPlus::Error::Error, e.message)
54
66
  end
55
67
 
56
- # @return [::String]
57
- def to_s
58
- "CLI(options: #{options})"
59
- end
60
-
61
- private
62
-
68
+ sig { params(error: ::StandardError).void }
63
69
  # An error was thrown that we weren't planning on
64
70
  def unhandled_error(error)
65
71
  warn(
@@ -74,6 +80,7 @@ module CSVPlusPlus
74
80
  warn("Cause: #{error.cause}") if error.cause
75
81
  end
76
82
 
83
+ sig { params(error: ::CSVPlusPlus::Error::Error).void }
77
84
  def handle_internal_error(error)
78
85
  case error
79
86
  when ::CSVPlusPlus::Error::SyntaxError
@@ -83,6 +90,7 @@ module CSVPlusPlus
83
90
  end
84
91
  end
85
92
 
93
+ sig { params(error: ::Google::Apis::ClientError).void }
86
94
  def handle_google_error(error)
87
95
  warn("Error making Google Sheets API request: #{error.message}")
88
96
  return unless @options.verbose
@@ -90,6 +98,7 @@ module CSVPlusPlus
90
98
  warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
91
99
  end
92
100
 
101
+ sig { void }
93
102
  def validate_options
94
103
  error_message = @options.validate
95
104
  return if error_message.nil?
@@ -98,6 +107,7 @@ module CSVPlusPlus
98
107
  raise(::CSVPlusPlus::Error::Error, error_message)
99
108
  end
100
109
 
110
+ sig { returns(::OptionParser) }
101
111
  def option_parser
102
112
  ::OptionParser.new do |parser|
103
113
  parser.on('-h', '--help', 'Show help information') do
@@ -1,7 +1,6 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './google_options'
4
-
5
4
  module CSVPlusPlus
6
5
  # Individual CLI flags that a user can supply
7
6
  #
@@ -1,3 +1,4 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module CSVPlusPlus
@@ -7,25 +8,49 @@ module CSVPlusPlus
7
8
  # attr_reader green_hex [String] The green value in hex ("FF", "00", "AF", etc)
8
9
  # attr_reader red_hex [String] The red value in hex ("FF", "00", "AF", etc)
9
10
  class Color
10
- attr_reader :red_hex, :green_hex, :blue_hex
11
+ extend ::T::Sig
12
+
13
+ sig { returns(::String) }
14
+ attr_reader :red_hex
15
+
16
+ sig { returns(::String) }
17
+ attr_reader :green_hex
18
+
19
+ sig { returns(::String) }
20
+ attr_reader :blue_hex
11
21
 
12
22
  HEX_STRING_REGEXP = /^#?([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i
13
23
  public_constant :HEX_STRING_REGEXP
14
24
 
25
+ sig { params(hex_string: ::String).returns(::T::Boolean) }
26
+ # Is +hex_string+ a valid hexadecimal color code? This function will accept input like the 6-digit format: #FF00FF,
27
+ # 00AABB and the shorter 3-digit format: #FFF, 0FA.
28
+ #
29
+ # @param hex_string [::String] The string to see if it's valid hex string
30
+ #
15
31
  # @return [boolean]
16
32
  def self.valid_hex_string?(hex_string)
17
33
  !(hex_string.strip =~ ::CSVPlusPlus::Color::HEX_STRING_REGEXP).nil?
18
34
  end
19
35
 
36
+ sig { params(hex_string: ::String).void }
20
37
  # Create an instance from a string like "#FFF" or "#FFFFFF"
21
38
  #
22
39
  # @param hex_string [String] The hex string input to parse
40
+ # rubocop:disable Metrics/CyclomaticComplexity
23
41
  def initialize(hex_string)
24
- @red_hex, @green_hex, @blue_hex = hex_string.strip.match(::CSVPlusPlus::Color::HEX_STRING_REGEXP)
42
+ red_hex, green_hex, blue_hex = hex_string.strip.match(::CSVPlusPlus::Color::HEX_STRING_REGEXP)
25
43
  &.captures
26
44
  &.map { |s| s.length == 1 ? s + s : s }
45
+ raise(::CSVPlusPlus::Error::Error, "Invalid color: #{hex_string}") unless red_hex && green_hex && blue_hex
46
+
47
+ @red_hex = ::T.let(red_hex, ::String)
48
+ @green_hex = ::T.let(green_hex, ::String)
49
+ @blue_hex = ::T.let(blue_hex, ::String)
27
50
  end
51
+ # rubocop:enable Metrics/CyclomaticComplexity
28
52
 
53
+ sig { returns(::Float) }
29
54
  # The percent (decimal between 0-1) of red
30
55
  #
31
56
  # @return [Numeric]
@@ -33,6 +58,7 @@ module CSVPlusPlus
33
58
  hex_to_percent(@red_hex)
34
59
  end
35
60
 
61
+ sig { returns(::Float) }
36
62
  # The percent (decimal between 0-1) of green
37
63
  #
38
64
  # @return [Numeric]
@@ -40,6 +66,7 @@ module CSVPlusPlus
40
66
  hex_to_percent(@green_hex)
41
67
  end
42
68
 
69
+ sig { returns(::Float) }
43
70
  # The percent (decimal between 0-1) of blue
44
71
  #
45
72
  # @return [Numeric]
@@ -47,6 +74,7 @@ module CSVPlusPlus
47
74
  hex_to_percent(@blue_hex)
48
75
  end
49
76
 
77
+ sig { returns(::String) }
50
78
  # Create a hex representation of the color (without a '#')
51
79
  #
52
80
  # @return [::String]
@@ -54,11 +82,7 @@ module CSVPlusPlus
54
82
  [@red_hex, @green_hex, @blue_hex].join
55
83
  end
56
84
 
57
- # @return [::String]
58
- def to_s
59
- "Color(r: #{@red_hex}, g: #{@green_hex}, b: #{@blue_hex})"
60
- end
61
-
85
+ sig { params(other: ::Object).returns(::T::Boolean) }
62
86
  # @return [boolean]
63
87
  def ==(other)
64
88
  other.is_a?(self.class) &&
@@ -69,6 +93,7 @@ module CSVPlusPlus
69
93
 
70
94
  private
71
95
 
96
+ sig { params(hex: ::String).returns(::Float) }
72
97
  def hex_to_percent(hex)
73
98
  hex.to_i(16) / 255.0
74
99
  end