csv_plus_plus 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +9 -3
  4. data/bin/csv++ +1 -37
  5. data/bin/csvpp +6 -0
  6. data/lib/csv_plus_plus/cell.rb +24 -8
  7. data/lib/csv_plus_plus/cli.rb +97 -0
  8. data/lib/csv_plus_plus/cli_flag.rb +10 -2
  9. data/lib/csv_plus_plus/code_section.rb +22 -3
  10. data/lib/csv_plus_plus/color.rb +19 -5
  11. data/lib/csv_plus_plus/google_api_client.rb +20 -0
  12. data/lib/csv_plus_plus/google_options.rb +6 -2
  13. data/lib/csv_plus_plus/graph.rb +0 -1
  14. data/lib/csv_plus_plus/language/ast_builder.rb +68 -0
  15. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +65 -0
  16. data/lib/csv_plus_plus/language/builtins.rb +46 -0
  17. data/lib/csv_plus_plus/language/cell_value.tab.rb +1 -2
  18. data/lib/csv_plus_plus/language/code_section.tab.rb +1 -2
  19. data/lib/csv_plus_plus/language/compiler.rb +74 -86
  20. data/lib/csv_plus_plus/language/entities/boolean.rb +5 -4
  21. data/lib/csv_plus_plus/language/entities/cell_reference.rb +10 -3
  22. data/lib/csv_plus_plus/language/entities/entity.rb +22 -6
  23. data/lib/csv_plus_plus/language/entities/function.rb +6 -4
  24. data/lib/csv_plus_plus/language/entities/function_call.rb +4 -3
  25. data/lib/csv_plus_plus/language/entities/number.rb +6 -4
  26. data/lib/csv_plus_plus/language/entities/runtime_value.rb +9 -8
  27. data/lib/csv_plus_plus/language/entities/string.rb +6 -4
  28. data/lib/csv_plus_plus/language/references.rb +22 -5
  29. data/lib/csv_plus_plus/language/runtime.rb +80 -22
  30. data/lib/csv_plus_plus/language/scope.rb +29 -38
  31. data/lib/csv_plus_plus/language/syntax_error.rb +10 -5
  32. data/lib/csv_plus_plus/lexer/lexer.rb +25 -12
  33. data/lib/csv_plus_plus/lexer/tokenizer.rb +35 -11
  34. data/lib/csv_plus_plus/modifier.rb +71 -8
  35. data/lib/csv_plus_plus/modifier.tab.rb +2 -2
  36. data/lib/csv_plus_plus/options.rb +17 -3
  37. data/lib/csv_plus_plus/row.rb +15 -4
  38. data/lib/csv_plus_plus/template.rb +10 -6
  39. data/lib/csv_plus_plus/version.rb +1 -1
  40. data/lib/csv_plus_plus/writer/base_writer.rb +0 -1
  41. data/lib/csv_plus_plus/writer/csv.rb +4 -1
  42. data/lib/csv_plus_plus/writer/excel.rb +5 -9
  43. data/lib/csv_plus_plus/writer/file_backer_upper.rb +58 -0
  44. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +8 -10
  45. data/lib/csv_plus_plus/writer/google_sheets.rb +22 -41
  46. data/lib/csv_plus_plus/writer/rubyxl_builder.rb +23 -15
  47. data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +15 -8
  48. data/lib/csv_plus_plus.rb +26 -4
  49. metadata +29 -7
@@ -4,31 +4,44 @@ require_relative 'entities'
4
4
  require_relative 'syntax_error'
5
5
  require 'tempfile'
6
6
 
7
- ENTITIES = ::CSVPlusPlus::Language::Entities
8
-
9
- RUNTIME_VARIABLES = {
10
- rownum: ::ENTITIES::RuntimeValue.new(->(r) { ::ENTITIES::Number.new(r.row_index + 1) }),
11
- cellnum: ::ENTITIES::RuntimeValue.new(->(r) { ::ENTITIES::Number.new(r.cell_index + 1) })
12
- }.freeze
13
-
14
7
  module CSVPlusPlus
15
8
  module Language
16
- ##
17
- # The runtime state of the compiler (the current linenumber/row, cell, etc)
9
+ # The runtime state of the compiler (the current +line_number+/+row_index+, +cell+ being processed, etc). We take
10
+ # multiple runs through the input file for parsing so it's really convenient to have a central place for these
11
+ # things to be managed.
12
+ #
13
+ # @attr_reader filename [String, nil] The filename that the input came from (mostly used for debugging since
14
+ # +filename+ can be +nil+ if it's read from stdin.
15
+ # @attr_reader length_of_code_section [Integer] The length (count of lines) of the code section part of the original
16
+ # input.
17
+ # @attr_reader length_of_csv_section [Integer] The length (count of lines) of the CSV part of the original csvpp
18
+ # input.
19
+ # @attr_reader length_of_original_file [Integer] The length (count of lines) of the original csvpp input.
20
+ #
21
+ # @attr cell [Cell] The current cell being processed
22
+ # @attr cell_index [Integer] The index of the current cell being processed (starts at 0)
23
+ # @attr row_index [Integer] The index of the current row being processed (starts at 0)
24
+ # @attr line_number [Integer] The line number of the original csvpp template (starts at 1)
18
25
  class Runtime
19
26
  attr_reader :filename, :length_of_code_section, :length_of_csv_section, :length_of_original_file
20
27
 
21
28
  attr_accessor :cell, :cell_index, :row_index, :line_number
22
29
 
23
- # initialize
30
+ # @param input [String] The input to be parsed
31
+ # @param filename [String, nil] The filename that the input came from (mostly used for debugging since +filename+
32
+ # can be +nil+ if it's read from stdin
24
33
  def initialize(input:, filename:)
25
34
  @filename = filename || 'stdin'
26
35
 
27
36
  init_input!(input)
28
- init!(1)
37
+ start!
29
38
  end
30
39
 
31
- # map over an unparsed file and keep track of line_number and row_index
40
+ # Map over an a csvpp file and keep track of line_number and row_index
41
+ #
42
+ # @param lines [Array]
43
+ #
44
+ # @return [Array]
32
45
  def map_lines(lines, &block)
33
46
  @line_number = 1
34
47
  lines.map do |line|
@@ -36,7 +49,11 @@ module CSVPlusPlus
36
49
  end
37
50
  end
38
51
 
39
- # map over a single row and keep track of the cell and it's index
52
+ # Map over a single row and keep track of the cell and it's index
53
+ #
54
+ # @param row [Array<Cell>] The row to map each cell over
55
+ #
56
+ # @return [Array]
40
57
  def map_row(row, &block)
41
58
  @cell_index = 0
42
59
  row.map.with_index do |cell, index|
@@ -45,7 +62,12 @@ module CSVPlusPlus
45
62
  end
46
63
  end
47
64
 
48
- # map over all rows and keep track of row and line numbers
65
+ # Map over all rows and keep track of row and line numbers
66
+ #
67
+ # @param rows [Array<Row>] The rows to map over (and keep track of indexes)
68
+ # @param cells_too [boolean] If the cells of each +row+ should be iterated over also.
69
+ #
70
+ # @return [Array]
49
71
  def map_rows(rows, cells_too: false, &block)
50
72
  @row_index = 0
51
73
  map_lines(rows) do |row|
@@ -59,56 +81,92 @@ module CSVPlusPlus
59
81
  end
60
82
 
61
83
  # Increment state to the next line
84
+ #
85
+ # @return [Integer]
62
86
  def next_line!
63
87
  @row_index += 1 unless @row_index.nil?
64
88
  @line_number += 1
65
89
  end
66
90
 
91
+ # Return the current spreadsheet row number. It parallels +@row_index+ but starts at 1.
92
+ #
93
+ # @return [Integer, nil]
94
+ def rownum
95
+ return if @row_index.nil?
96
+
97
+ @row_index + 1
98
+ end
99
+
67
100
  # Set the current cell and index
101
+ #
102
+ # @param cell [Cell] The current cell
103
+ # @param cell_index [Integer] The index of the cell
68
104
  def set_cell!(cell, cell_index)
69
105
  @cell = cell
70
106
  @cell_index = cell_index
71
107
  end
72
108
 
73
- # Each time we run a parse on the input, call this so that the runtime state
74
- # is set to it's default values
75
- def init!(start_line_number_at)
109
+ # Each time we run a parse on the input, reset the runtime state starting at the beginning of the file
110
+ def start!
76
111
  @row_index = @cell_index = nil
77
- @line_number = start_line_number_at
112
+ @line_number = 1
113
+ end
114
+
115
+ # Reset the runtime state starting at the CSV section
116
+ def start_at_csv!
117
+ # TODO: isn't the input re-written anyway without the code section? why do we need this?
118
+ start!
119
+ @line_number = @length_of_code_section || 1
78
120
  end
79
121
 
80
- # to_s
122
+ # @return [String]
81
123
  def to_s
82
124
  "Runtime(cell: #{@cell}, row_index: #{@row_index}, cell_index: #{@cell_index})"
83
125
  end
84
126
 
85
- # get the current (entity) value of a runtime value
127
+ # Get the current (entity) value of a runtime value
128
+ #
129
+ # @param var_id [String, Symbol] The Variable#id of the variable being resolved.
130
+ #
131
+ # @return [Entity]
86
132
  def runtime_value(var_id)
87
133
  if runtime_variable?(var_id)
88
- ::RUNTIME_VARIABLES[var_id.to_sym].resolve_fn.call(self)
134
+ ::CSVPlusPlus::Language::Builtins::VARIABLES[var_id.to_sym].resolve_fn.call(self)
89
135
  else
90
136
  raise_syntax_error('Undefined variable', var_id)
91
137
  end
92
138
  end
93
139
 
94
140
  # Is +var_id+ a runtime variable? (it's a static variable otherwise)
141
+ #
142
+ # @param var_id [String, Symbol] The Variable#id to check if it's a runtime variable
143
+ #
144
+ # @return [boolean]
95
145
  def runtime_variable?(var_id)
96
- ::RUNTIME_VARIABLES.key?(var_id.to_sym)
146
+ ::CSVPlusPlus::Language::Builtins::VARIABLES.key?(var_id.to_sym)
97
147
  end
98
148
 
99
149
  # Called when an error is encoutered during parsing. It will construct a useful
100
150
  # error with the current +@row/@cell_index+, +@line_number+ and +@filename+
151
+ #
152
+ # @param message [String] A message relevant to why this error is being raised.
153
+ # @param bad_input [String] The offending input that caused this error to be thrown.
154
+ # @param wrapped_error [StandardError, nil] The underlying error that was raised (if it's not from our own logic)
101
155
  def raise_syntax_error(message, bad_input, wrapped_error: nil)
102
156
  raise(::CSVPlusPlus::Language::SyntaxError.new(message, bad_input, self, wrapped_error:))
103
157
  end
104
158
 
105
159
  # The currently available input for parsing. The tmp state will be re-written
106
160
  # between parsing the code section and the CSV section
161
+ #
162
+ # @return [String]
107
163
  def input
108
164
  @tmp
109
165
  end
110
166
 
111
167
  # We mutate the input over and over. It's ok because it's just a Tempfile
168
+ #
169
+ # @param data [String] The data to rewrite our input file to
112
170
  def rewrite_input!(data)
113
171
  @tmp.truncate(0)
114
172
  @tmp.write(data)
@@ -6,40 +6,29 @@ require_relative './entities'
6
6
  require_relative './references'
7
7
  require_relative './syntax_error'
8
8
 
9
- BUILTIN_FUNCTIONS = {
10
- # =CELLREF(C) === =INDIRECT(CONCAT($$C, $$rownum))
11
- cellref: ::CSVPlusPlus::Language::Entities::Function.new(
12
- :cellref,
13
- [:cell],
14
- ::CSVPlusPlus::Language::Entities::FunctionCall.new(
15
- :indirect,
16
- [
17
- ::CSVPlusPlus::Language::Entities::FunctionCall.new(
18
- :concat,
19
- [
20
- ::CSVPlusPlus::Language::Entities::Variable.new(:cell),
21
- ::CSVPlusPlus::Language::Entities::Variable.new(:rownum)
22
- ]
23
- )
24
- ]
25
- )
26
- )
27
- }.freeze
28
-
29
9
  module CSVPlusPlus
30
10
  module Language
31
11
  # A class representing the scope of the current Template and responsible for resolving variables
12
+ #
13
+ # @attr_reader code_section [CodeSection] The CodeSection containing variables and functions to be resolved
14
+ # @attr_reader runtime [Runtime] The compiler's current runtime
15
+ #
32
16
  # rubocop:disable Metrics/ClassLength
33
17
  class Scope
34
18
  attr_reader :code_section, :runtime
35
19
 
36
20
  # initialize with a +Runtime+ and optional +CodeSection+
21
+ #
22
+ # @param runtime [Runtime]
23
+ # @param code_section [Runtime, nil]
37
24
  def initialize(runtime:, code_section: nil)
38
25
  @code_section = code_section if code_section
39
26
  @runtime = runtime
40
27
  end
41
28
 
42
29
  # Resolve all values in the ast of the current cell being processed
30
+ #
31
+ # @return [Entity]
43
32
  def resolve_cell_value
44
33
  return unless (ast = @runtime.cell&.ast)
45
34
 
@@ -56,14 +45,14 @@ module CSVPlusPlus
56
45
  end
57
46
 
58
47
  # Set the +code_section+ and resolve all inner dependencies in it's variables and functions.
48
+ #
49
+ # @param code_section [CodeSection] The code_section to be resolved
59
50
  def code_section=(code_section)
60
51
  @code_section = code_section
61
-
62
52
  resolve_static_variables!
63
- resolve_static_functions!
64
53
  end
65
54
 
66
- # to_s
55
+ # @return [String]
67
56
  def to_s
68
57
  "Scope(code_section: #{@code_section}, runtime: #{@runtime})"
69
58
  end
@@ -71,10 +60,10 @@ module CSVPlusPlus
71
60
  private
72
61
 
73
62
  # Resolve all variable references defined statically in the code section
63
+ # TODO: experiment with getting rid of this - does it even play correctly with runtime vars?
74
64
  def resolve_static_variables!
75
65
  variables = @code_section.variables
76
66
  last_var_dependencies = {}
77
- # TODO: might not need the infinite loop wrap
78
67
  loop do
79
68
  var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
80
69
  return if var_dependencies == last_var_dependencies
@@ -89,14 +78,6 @@ module CSVPlusPlus
89
78
  var_dependencies.reject { |k| @runtime.runtime_variable?(k) }
90
79
  end
91
80
 
92
- # Resolve all functions defined statically in the code section
93
- def resolve_static_functions!
94
- # TODO: I'm still torn if it's worth replacing function references
95
- #
96
- # my current theory is that if we resolve static functions befor processing each cell,
97
- # overall compile time will be improved because there will be less to do for each cell
98
- end
99
-
100
81
  def resolve_functions(ast, refs)
101
82
  refs.reduce(ast.dup) do |acc, elem|
102
83
  function_replace(acc, elem.id, resolve_function(elem.id))
@@ -112,10 +93,13 @@ module CSVPlusPlus
112
93
  # Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
113
94
  def function_replace(node, fn_id, replacement)
114
95
  if node.function_call? && node.id == fn_id
115
- apply_arguments(replacement, node)
96
+ call_function_or_runtime_value(replacement, node)
116
97
  elsif node.function_call?
117
- arguments = node.arguments.map { |n| function_replace(n, fn_id, replacement) }
118
- ::CSVPlusPlus::Language::Entities::FunctionCall.new(node.id, arguments)
98
+ # not our function, but continue our depth first search on it
99
+ ::CSVPlusPlus::Language::Entities::FunctionCall.new(
100
+ node.id,
101
+ node.arguments.map { |n| function_replace(n, fn_id, replacement) }
102
+ )
119
103
  else
120
104
  node
121
105
  end
@@ -125,11 +109,18 @@ module CSVPlusPlus
125
109
  id = fn_id.to_sym
126
110
  return @code_section.functions[id] if @code_section.defined_function?(id)
127
111
 
128
- # this will throw a syntax error if it doesn't exist (which is what we want)
129
- return ::BUILTIN_FUNCTIONS[id] if ::BUILTIN_FUNCTIONS.key?(id)
112
+ ::CSVPlusPlus::Language::Builtins::FUNCTIONS[id]
113
+ end
114
+
115
+ def call_function_or_runtime_value(function_or_runtime_value, function_call)
116
+ if function_or_runtime_value.function?
117
+ call_function(function_or_runtime_value, function_call)
118
+ else
119
+ function_or_runtime_value.resolve_fn.call(@runtime, function_call.arguments)
120
+ end
130
121
  end
131
122
 
132
- def apply_arguments(function, function_call)
123
+ def call_function(function, function_call)
133
124
  i = 0
134
125
  function.arguments.reduce(function.body.dup) do |ast, argument|
135
126
  variable_replace(ast, argument, function_call.arguments[i]).tap do
@@ -2,10 +2,13 @@
2
2
 
3
3
  module CSVPlusPlus
4
4
  module Language
5
- ##
6
5
  # An error that can be thrown for various syntax errors
7
6
  class SyntaxError < ::CSVPlusPlus::Error
8
- # initialize
7
+ # @param message [String] The primary message to be shown to the user
8
+ # @param bad_input [String] The offending input that caused the error to be thrown
9
+ # @param runtime [Runtime] The current runtime
10
+ # @param wrapped_error [StandardError] The underlying error that caused the syntax error. For example a
11
+ # Racc::ParseError that was thrown
9
12
  def initialize(message, bad_input, runtime, wrapped_error: nil)
10
13
  @bad_input = bad_input.to_s
11
14
  @runtime = runtime
@@ -15,19 +18,21 @@ module CSVPlusPlus
15
18
  super(message)
16
19
  end
17
20
 
18
- # to_s
21
+ # @return [String]
19
22
  def to_s
20
23
  to_trace
21
24
  end
22
25
 
23
26
  # Output a verbose user-helpful string that references the current runtime
24
27
  def to_verbose_trace
25
- warn(@wrapped_error.full_message)
26
- warn(@wrapped_error.backtrace)
28
+ warn(@wrapped_error.full_message) if @wrapped_error
29
+ warn(@wrapped_error.backtrace) if @wrapped_error
27
30
  to_trace
28
31
  end
29
32
 
30
33
  # Output a user-helpful string that references the runtime state
34
+ #
35
+ # @return [String]
31
36
  def to_trace
32
37
  "#{message_prefix}#{cell_index} #{message_postfix}"
33
38
  end
@@ -1,19 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CSVPlusPlus
4
- # Common methods to be mixed into our Racc parsers
4
+ # Common methods to be mixed into the Racc parsers
5
+ #
6
+ # @attr_reader tokens [Array]
5
7
  module Lexer
6
- # initialize
7
- def initialize
8
- @tokens = []
8
+ attr_reader :tokens
9
+
10
+ # Initialize a lexer instance with an empty +@tokens+
11
+ def initialize(tokens: [])
12
+ @tokens = tokens
9
13
  end
10
14
 
11
15
  # Used by racc to iterate each token
16
+ #
17
+ # @return [Array<(String, String)>]
12
18
  def next_token
13
19
  @tokens.shift
14
20
  end
15
21
 
16
- # parse
22
+ # Orchestate the tokenizing, parsing and error handling of parsing input. Each instance will implement their own
23
+ # #tokenizer method
24
+ #
25
+ # @return [Lexer#return_value] Each instance will define it's own +return_value+ with the result of parsing
17
26
  def parse(input, runtime)
18
27
  return if input.nil?
19
28
 
@@ -28,10 +37,20 @@ module CSVPlusPlus
28
37
 
29
38
  protected
30
39
 
40
+ # Given a +type+, instantiate the proper instance with the given +entity_args+
41
+ #
42
+ # @param type [Symbol]
43
+ # @param entity_args
44
+ def e(type, *entity_args)
45
+ ::CSVPlusPlus::Language::TYPES[type].new(*entity_args)
46
+ end
47
+
48
+ private
49
+
31
50
  def tokenize(input, runtime)
32
51
  return if input.nil?
33
52
 
34
- t = tokenizer(input)
53
+ t = tokenizer.scan(input)
35
54
 
36
55
  until t.scanner.empty?
37
56
  next if t.matches_ignore?
@@ -45,12 +64,6 @@ module CSVPlusPlus
45
64
  @tokens << %i[EOL EOL]
46
65
  end
47
66
 
48
- def e(type, *entity_args)
49
- ::CSVPlusPlus::Language::TYPES[type].new(*entity_args)
50
- end
51
-
52
- private
53
-
54
67
  def consume_token(tokenizer, runtime)
55
68
  if tokenizer.last_token
56
69
  @tokens << [tokenizer.last_token, tokenizer.last_match]
@@ -5,13 +5,14 @@ require 'strscan'
5
5
  module CSVPlusPlus
6
6
  module Lexer
7
7
  # A class that contains the use-case-specific regexes for parsing
8
+ #
9
+ # @attr_reader last_token [String] The last token that's been matched.
10
+ # @attr_reader scanner [StringScanner] The StringScanner instance that's parsing the input.
8
11
  class Tokenizer
9
12
  attr_reader :last_token, :scanner
10
13
 
11
- # initialize
12
- # rubocop:disable Metrics/ParameterLists
13
- def initialize(input:, tokens:, catchall: nil, ignore: nil, alter_matches: {}, stop_fn: nil)
14
- @scanner = ::StringScanner.new(input.strip)
14
+ # @param input [String]
15
+ def initialize(tokens:, catchall: nil, ignore: nil, alter_matches: {}, stop_fn: nil)
15
16
  @last_token = nil
16
17
 
17
18
  @catchall = catchall
@@ -20,43 +21,66 @@ module CSVPlusPlus
20
21
  @stop_fn = stop_fn
21
22
  @alter_matches = alter_matches
22
23
  end
23
- # rubocop:enable Metrics/ParameterLists
24
24
 
25
- # Scan tokens and see if any match
25
+ # Initializers a scanner for the given input to be parsed
26
+ #
27
+ # @param input The input to be tokenized
28
+ # @return [Tokenizer]
29
+ def scan(input)
30
+ @scanner = ::StringScanner.new(input.strip)
31
+ self
32
+ end
33
+
34
+ # Scan tokens and set +@last_token+ if any match
35
+ #
36
+ # @return [String, nil]
26
37
  def scan_tokens!
27
38
  m = @tokens.find { |t| @scanner.scan(t.first) }
28
39
  @last_token = m ? m[1] : nil
29
40
  end
30
41
 
31
42
  # Scan input against the catchall pattern
43
+ #
44
+ # @return [String, nil]
32
45
  def scan_catchall
33
46
  @scanner.scan(@catchall) if @catchall
34
47
  end
35
48
 
36
49
  # Scan input against the ignore pattern
50
+ #
51
+ # @return [boolean]
37
52
  def matches_ignore?
38
53
  @scanner.scan(@ignore) if @ignore
39
54
  end
40
55
 
41
56
  # The value of the last token matched
57
+ #
58
+ # @return [String, nil]
42
59
  def last_match
43
60
  return @alter_matches[@last_token].call(@scanner.matched) if @alter_matches.key?(@last_token)
44
61
 
45
62
  @scanner.matched
46
63
  end
47
64
 
48
- # Peek the input
49
- def peek
50
- @scanner.peek(100)
65
+ # Read the input but don't consume it
66
+ #
67
+ # @param peek_characters [Integer]
68
+ #
69
+ # @return [String]
70
+ def peek(peek_characters: 100)
71
+ @scanner.peek(peek_characters)
51
72
  end
52
73
 
53
74
  # Scan for our stop token (if there is one - some parsers stop early and some don't)
75
+ #
76
+ # @return [boolean]
54
77
  def stop?
55
78
  @stop_fn ? @stop_fn.call(@scanner) : false
56
79
  end
57
80
 
58
- # The rest of the un-parsed input. The tokenizer might not need to
59
- # parse the entire input
81
+ # The rest of the un-parsed input. The tokenizer might not need to parse the entire input
82
+ #
83
+ # @return [String]
60
84
  def rest
61
85
  @scanner.rest
62
86
  end
@@ -7,12 +7,33 @@ require_relative './language/syntax_error'
7
7
 
8
8
  module CSVPlusPlus
9
9
  # A container representing the operations that can be applied to a cell or row
10
+ #
11
+ # @attr borders [Array<String>] The borders that will be set
12
+ # @attr expand [Expand] Whether this row expands into multiple rows
13
+ # @attr fontfamily [String] The font family
14
+ # @attr fontsize [Number] The font size
15
+ # @attr halign ['left', 'center', 'right'] Horizontal alignment
16
+ # @attr note [String] A note/comment on the cell
17
+ # @attr numberformat [String] A number format to apply to the value in the cell
18
+ # @attr row_level [Boolean] Is this a row modifier? If so it's values will apply to all cells in the row
19
+ # (unless overridden by the cell modifier)
20
+ # @attr validation [Object]
21
+ # @attr valign ['top', 'center', 'bottom'] Vertical alignment
22
+ #
23
+ # @attr_writer borderstyle ['dashed', 'dotted', 'double', 'solid', 'solid_medium', 'solid_thick']
24
+ # The style of border on the cell
25
+ #
26
+ # @attr_reader bordercolor [String]
27
+ # @attr_reader borders [Array<String>]
28
+ # @attr_reader color [Color] The background color of the cell
29
+ # @attr_reader fontcolor [Color] The font color of the cell
30
+ # @attr_reader formats [Array<String>] Bold/italics/underline/strikethrough formatting
10
31
  class Modifier
11
32
  attr_reader :bordercolor, :borders, :color, :fontcolor, :formats
12
33
  attr_writer :borderstyle
13
34
  attr_accessor :expand, :fontfamily, :fontsize, :halign, :valign, :note, :numberformat, :row_level, :validation
14
35
 
15
- # initialize
36
+ # @param row_level [Boolean] Whether or not this modifier applies to the entire row
16
37
  def initialize(row_level: false)
17
38
  @row_level = row_level
18
39
  @freeze = false
@@ -20,82 +41,119 @@ module CSVPlusPlus
20
41
  @formats = ::Set.new
21
42
  end
22
43
 
23
- # Set the color. hex_value is a String
44
+ # Set the color
45
+ #
46
+ # @param hex_value [String]
47
+ #
48
+ # @return [Color]
24
49
  def color=(hex_value)
25
50
  @color = ::CSVPlusPlus::Color.new(hex_value)
26
51
  end
27
52
 
28
- # Assign a border. +side+ must be 'top', 'left', 'bottom', 'right' or 'all'
53
+ # Assign a border
54
+ #
55
+ # @param side ['top', 'left', 'bottom', 'right', 'all']
29
56
  def border=(side)
30
57
  @borders << side
31
58
  end
32
59
 
33
60
  # Does this have a border along +side+?
61
+ #
62
+ # @param side ['top', 'left', 'bottom', 'right', 'all']
63
+ #
64
+ # @return [Boolean]
34
65
  def border_along?(side)
35
- border_all? || @borders.include?(side)
66
+ @borders.include?('all') || @borders.include?(side)
36
67
  end
37
68
 
38
69
  # Does this have a border along all sides?
70
+ #
71
+ # @return [Boolean]
39
72
  def border_all?
40
- @borders.include?('all')
73
+ @borders.include?('all') \
74
+ || (border_along?('top') && border_along?('bottom') && border_along?('left') && border_along?('right'))
41
75
  end
42
76
 
43
77
  # Set the bordercolor
78
+ #
79
+ # @param hex_value [String] formatted as '#000000', '#000' or '000000'
44
80
  def bordercolor=(hex_value)
45
81
  @bordercolor = ::CSVPlusPlus::Color.new(hex_value)
46
82
  end
47
83
 
48
84
  # Are there any borders set?
85
+ #
86
+ # @return [Boolean]
49
87
  def any_border?
50
88
  !@borders.empty?
51
89
  end
52
90
 
53
91
  # Set the fontcolor
92
+ #
93
+ # @param hex_value [String] formatted as '#000000', '#000' or '000000'
54
94
  def fontcolor=(hex_value)
55
95
  @fontcolor = ::CSVPlusPlus::Color.new(hex_value)
56
96
  end
57
97
 
58
- # Set a format. +type+ must be 'bold', 'italic', 'underline' or 'strikethrough'
98
+ # Set a text format (bolid, italic, underline or strikethrough)
99
+ #
100
+ # @param value ['bold', 'italic', 'underline', 'strikethrough']
59
101
  def format=(value)
60
102
  @formats << value
61
103
  end
62
104
 
63
105
  # Is the given format set?
106
+ #
107
+ # @param type ['bold', 'italic', 'underline', 'strikethrough']
108
+ #
109
+ # @return [Boolean]
64
110
  def formatted?(type)
65
111
  @formats.include?(type)
66
112
  end
67
113
 
68
114
  # Freeze the row from edits
115
+ #
116
+ # @return [true]
69
117
  def freeze!
70
118
  @frozen = true
71
119
  end
72
120
 
73
- # Is the row forzen?
121
+ # Is the row frozen?
122
+ #
123
+ # @return [Boolean]
74
124
  def frozen?
75
125
  @frozen
76
126
  end
77
127
 
78
128
  # Mark this modifer as row-level
129
+ #
130
+ # @return [true]
79
131
  def row_level!
80
132
  @row_level = true
81
133
  end
82
134
 
83
135
  # Is this a row-level modifier?
136
+ #
137
+ # @return [Boolean]
84
138
  def row_level?
85
139
  @row_level
86
140
  end
87
141
 
88
142
  # Is this a cell-level modifier?
143
+ #
144
+ # @return [Boolean]
89
145
  def cell_level?
90
146
  !@row_level
91
147
  end
92
148
 
93
149
  # Style of border
150
+ #
151
+ # @return [String]
94
152
  def borderstyle
95
153
  @borderstyle || 'solid'
96
154
  end
97
155
 
98
- # to_s
156
+ # @return [String]
99
157
  def to_s
100
158
  # TODO... I dunno, not sure how to manage this
101
159
  "Modifier(row_level: #{@row_level} halign: #{@halign} valign: #{@valign} format: #{@formats} " \
@@ -103,8 +161,13 @@ module CSVPlusPlus
103
161
  end
104
162
 
105
163
  # Create a new modifier instance, with all values defaulted from +other+
164
+ #
165
+ # @param other [Modifier]
106
166
  def take_defaults_from!(other)
107
167
  other.instance_variables.each do |property|
168
+ # don't propagate row-specific values
169
+ next if property == :@row_level
170
+
108
171
  value = other.instance_variable_get(property)
109
172
  instance_variable_set(property, value.clone)
110
173
  end