csv_plus_plus 0.1.1 → 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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -63
  3. data/{CHANGELOG.md → docs/CHANGELOG.md} +17 -0
  4. data/lib/csv_plus_plus/benchmarked_compiler.rb +112 -0
  5. data/lib/csv_plus_plus/cell.rb +46 -24
  6. data/lib/csv_plus_plus/cli.rb +44 -17
  7. data/lib/csv_plus_plus/cli_flag.rb +1 -2
  8. data/lib/csv_plus_plus/color.rb +42 -11
  9. data/lib/csv_plus_plus/compiler.rb +178 -0
  10. data/lib/csv_plus_plus/entities/ast_builder.rb +50 -0
  11. data/lib/csv_plus_plus/entities/boolean.rb +40 -0
  12. data/lib/csv_plus_plus/entities/builtins.rb +58 -0
  13. data/lib/csv_plus_plus/entities/cell_reference.rb +231 -0
  14. data/lib/csv_plus_plus/entities/date.rb +63 -0
  15. data/lib/csv_plus_plus/entities/entity.rb +50 -0
  16. data/lib/csv_plus_plus/entities/entity_with_arguments.rb +57 -0
  17. data/lib/csv_plus_plus/entities/function.rb +45 -0
  18. data/lib/csv_plus_plus/entities/function_call.rb +50 -0
  19. data/lib/csv_plus_plus/entities/number.rb +48 -0
  20. data/lib/csv_plus_plus/entities/runtime_value.rb +43 -0
  21. data/lib/csv_plus_plus/entities/string.rb +42 -0
  22. data/lib/csv_plus_plus/entities/variable.rb +37 -0
  23. data/lib/csv_plus_plus/entities.rb +40 -0
  24. data/lib/csv_plus_plus/error/error.rb +20 -0
  25. data/lib/csv_plus_plus/error/formula_syntax_error.rb +37 -0
  26. data/lib/csv_plus_plus/error/modifier_syntax_error.rb +75 -0
  27. data/lib/csv_plus_plus/error/modifier_validation_error.rb +69 -0
  28. data/lib/csv_plus_plus/error/syntax_error.rb +71 -0
  29. data/lib/csv_plus_plus/error/writer_error.rb +17 -0
  30. data/lib/csv_plus_plus/error.rb +10 -2
  31. data/lib/csv_plus_plus/google_api_client.rb +11 -2
  32. data/lib/csv_plus_plus/google_options.rb +23 -18
  33. data/lib/csv_plus_plus/lexer/lexer.rb +17 -6
  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 +18 -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 -150
  44. data/lib/csv_plus_plus/options.rb +64 -19
  45. data/lib/csv_plus_plus/{language → parser}/cell_value.tab.rb +25 -25
  46. data/lib/csv_plus_plus/{language → parser}/code_section.tab.rb +86 -95
  47. data/lib/csv_plus_plus/parser/modifier.tab.rb +478 -0
  48. data/lib/csv_plus_plus/row.rb +53 -15
  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 +42 -0
  56. data/lib/csv_plus_plus/source_code.rb +66 -0
  57. data/lib/csv_plus_plus/template.rb +63 -36
  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 +7 -4
  63. data/lib/csv_plus_plus/writer/google_sheet_builder.rb +88 -45
  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 -33
  67. data/lib/csv_plus_plus/writer.rb +39 -9
  68. data/lib/csv_plus_plus.rb +41 -15
  69. metadata +44 -30
  70. data/lib/csv_plus_plus/code_section.rb +0 -101
  71. data/lib/csv_plus_plus/expand.rb +0 -18
  72. data/lib/csv_plus_plus/graph.rb +0 -62
  73. data/lib/csv_plus_plus/language/ast_builder.rb +0 -68
  74. data/lib/csv_plus_plus/language/benchmarked_compiler.rb +0 -65
  75. data/lib/csv_plus_plus/language/builtins.rb +0 -46
  76. data/lib/csv_plus_plus/language/compiler.rb +0 -152
  77. data/lib/csv_plus_plus/language/entities/boolean.rb +0 -33
  78. data/lib/csv_plus_plus/language/entities/cell_reference.rb +0 -33
  79. data/lib/csv_plus_plus/language/entities/entity.rb +0 -86
  80. data/lib/csv_plus_plus/language/entities/function.rb +0 -35
  81. data/lib/csv_plus_plus/language/entities/function_call.rb +0 -37
  82. data/lib/csv_plus_plus/language/entities/number.rb +0 -36
  83. data/lib/csv_plus_plus/language/entities/runtime_value.rb +0 -28
  84. data/lib/csv_plus_plus/language/entities/string.rb +0 -31
  85. data/lib/csv_plus_plus/language/entities/variable.rb +0 -25
  86. data/lib/csv_plus_plus/language/entities.rb +0 -28
  87. data/lib/csv_plus_plus/language/references.rb +0 -70
  88. data/lib/csv_plus_plus/language/runtime.rb +0 -205
  89. data/lib/csv_plus_plus/language/scope.rb +0 -192
  90. data/lib/csv_plus_plus/language/syntax_error.rb +0 -66
  91. data/lib/csv_plus_plus/modifier.tab.rb +0 -907
  92. data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -56
  93. 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: 6ef86258faa82b75114acc715e9cb68bb312dc55194d533270fce0a796e02362
4
- data.tar.gz: 9316f43dd4169a779efc6d8f447002434c4fc5984064b1f22f1448baee0f4522
3
+ metadata.gz: abee03db0f21f9d8c5d3cfb4ec81dbd748568f878458f355cab6db5fc458a509
4
+ data.tar.gz: 6c4e95fa1c780a2d009eaeb58fd48b28c2dd96826b22be02f84d5fc8a06ad31e
5
5
  SHA512:
6
- metadata.gz: 8637e6c3dec41bf747e5fe1f6eb4d3227247e196ef530bbac96e964f6cdbe911d39bed9941ab7e116f00ea2547cf5050a609f078af8dab8f28a6e0f7746e776d
7
- data.tar.gz: 57acb6edef384a31a41ea244841d2977d31817d87735944336688f5961d6eb22c97e5b88683d753c1163239f397936411a7836eafe1aef6e9ed6a612e846fd68
6
+ metadata.gz: 59f2cb756bdfd20f95f252aa1c07fb4b994def21f07d8bc4e5a976b22f0b439fb950e1723fdb34753baf23527e5950d244f1386e9231c6318b35de3974305ba4
7
+ data.tar.gz: 38b6d6d960aa17978adf2ee1724333754e7cdc86b8e41fa58522357448db8448d5441fb5961d3758af94d617b94b21b197b5e4d2d4eb2dba0cbc39da586c4413
data/README.md CHANGED
@@ -9,101 +9,56 @@ A tool that allows you to programatically author spreadsheets in your favorite t
9
9
 
10
10
  A `csvpp` file consists of a (optional) code section and a CSV section separated by `---`. In the code section you can define variables and functions that can be used in the CSV below it. For example:
11
11
 
12
+ ###### **`mystocks.csvpp`**
12
13
  ```
13
14
  fees := 0.50 # my broker charges $0.50 a trade
14
15
 
15
- price := cellref(C)
16
- quantity := cellref(D)
16
+ price := celladjacent(C)
17
+ quantity := celladjacent(D)
17
18
 
18
19
  def profit() (price * quantity) - fees
19
20
 
20
21
  ---
21
22
  ![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
22
- ![[expand]],[[format=bold]],,,"=PROFIT()",$$fees
23
+ ![[expand]],[[format=bold]],,,"=profit()",$$fees
23
24
  ```
24
25
 
25
- ## Variables
26
-
27
- Variables can be defined in the code section by giving a name (a combination of letters, numbers and underscores ) the expression `:=` and followed with a value.
28
-
29
- ### Built-in Variables
30
-
31
- * `$$rownum` - The current row number. The first row of the spreadsheet starts at 1. Can be used anywhere and it's value will evaluate to the current row being processed.
32
-
33
- ## Functions
34
-
35
- ### Built-in Functions
36
- * `cellref(CELL)` - Returns a reference to the `CELL` relative to the current row. If the current `$$rownum` is `2`, then `CELLREF("C")` returns a reference to cell `C2`.
37
-
38
- ## Modifiers
39
-
40
- Modifiers can change the formatting of a cell or row, apply validation, change alignment, etc. All of the normal rules of CSV apply, with the addition that each cell can have modifiers (specified in `[[`/`]]` for cells and `![[`/`]]` for rows):
41
-
42
- ```
43
- foo,[[...]]bar,baz
44
- ```
45
-
46
- specifying formatting or various other modifiers to the cell. Additionally a row can start with:
47
-
48
- ```
49
- ![[...]]foo,bar,baz
50
- ```
51
-
52
- which will apply that modifier to all cells in the row.
53
-
54
- ### Examples
55
-
56
- * Align the second cell left, align the last cell to the center and make it bold and italicized:
26
+ And can be compiled into a `.xlsx` file by:
57
27
 
58
28
  ```
59
- Date,[[align=left]]Amount,Quantity,[[align=center/format=bold italic]]Price
29
+ $ csv++ -n 'My Stock Tracker' -o mystocks.xlsx mystocks.csvpp
60
30
  ```
61
31
 
62
- * Underline and center-align an entire row:
32
+ See the [Language Reference](./docs/LANGUAGE_REFERENCE.md) for a full explanation of features.
63
33
 
64
- ```
65
- ![[align=center/format=underline]]Date,Amount,Quantity,Price
66
- ```
34
+ ## Installing
67
35
 
68
- * A header for the first row, then some formulas that repeat for each row for the rest of the spreadsheet:
36
+ Just install it via rubygems (homebrew and debian packages are in the works):
69
37
 
70
- ```
71
- ![[align=center/format=bold]]Date,Price,Quantity,Profit
72
- ![[expand=1:]],,,"=MULTIPLY(cellref(B), cellref(C))"
73
- ```
38
+ `$ gem install csv_plus_plus`
74
39
 
75
- ## Setup (Google Sheets)
40
+ or if you want the very latest changes, clone this repository and run:
76
41
 
77
- Just install it via rubygems (homebrew and debian packages are in the works):
42
+ `$ rake gem:install`
78
43
 
79
- `$ gem install csv_plus_plus`
44
+ ### [Setting Up Google Sheets](./docs/README_GOOGLE_SHEETS.md)
80
45
 
81
- ### Publishing to Google Sheets
46
+ ## Examples
82
47
 
83
- * Go to the [GCP developers console](https://console.cloud.google.com/projectselector2/apis/credentials?pli=1&supportedpurview=project), create a service account and export keys for it to `~/.config/gcloud/application_default_credentials.json`
84
- * "Share" the spreadsheet with the email associated with the service account
48
+ Take a look at the [examples](./examples/) directory for a bunch of example `.csvpp` files.
85
49
 
86
50
  ## CLI Arguments
87
51
 
88
52
  ```
89
53
  Usage: csv++ [options]
54
+ -h, --help Show help information
90
55
  -b, --backup Create a backup of the spreadsheet before applying changes.
91
- -g, --google-sheet-id SHEET_ID The id of the sheet - you can extract this from the URL: https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0
92
56
  -c, --create Create the sheet if it doesn't exist. It will use --sheet-name if specified
57
+ -g, --google-sheet-id SHEET_ID The id of the sheet - you can extract this from the URL: https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0
93
58
  -k, --key-values KEY_VALUES A comma-separated list of key=values which will be made available to the template
94
59
  -n, --sheet-name SHEET_NAME The name of the sheet to apply the template to
60
+ -o, --output OUTPUT_FILE The file to write to (must be .csv, .ods, .xls)
95
61
  -v, --verbose Enable verbose output
96
62
  -x, --offset-columns OFFSET Apply the template offset by OFFSET cells
97
63
  -y, --offset-rows OFFSET Apply the template offset by OFFSET rows
98
- -h, --help Show help information
99
- ```
100
-
101
- ## Usage Examples
102
-
103
- ```
104
- # apply my_taxes_template.csvpp to an existing Google Sheet with name "Taxes 2022"
105
- $ csv++ --sheet-name "Taxes 2022" --sheet-id "[...]" my_taxes_template.csvpp
106
-
107
- # take input from stdin, supply a variable ($$rate = 1) and apply to the "Stocks" spreadsheet
108
- $ cat stocks.csvpp | csv++ -k "rate=1" -n "Stocks" -i "[...]"
109
64
  ```
@@ -1,3 +1,20 @@
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
+
10
+ ## v0.1.2
11
+
12
+ - var=... modifier which allows binding a variable to a cell
13
+ - Improved error handling and messages
14
+ - Moving in a direction that allows for the context-dependent aspects of modifiers
15
+ - Fixes a bug with creating a new excel spreadsheet
16
+ - Docs & tests
17
+
1
18
  ## v0.1.1
2
19
 
3
20
  - Better support for the various infix operators (+,-,/,*,^,%,=,<,etc)
@@ -0,0 +1,112 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module CSVPlusPlus
5
+ # Extend a +Compiler+ class and add benchmark timings
6
+ #
7
+ # @attr_reader timings [Array<Benchmark::Tms>] +Benchmark+ timings that have been accumulated by each step of
8
+ # compilation
9
+ # @attr_reader benchmark [Benchmark::Report] A +Benchmark+ instance
10
+ class BenchmarkedCompiler < ::CSVPlusPlus::Compiler
11
+ extend ::T::Sig
12
+
13
+ sig { returns(::Benchmark::Report) }
14
+ attr_reader :benchmark
15
+
16
+ sig { returns(::T::Array[::Benchmark::Tms]) }
17
+ attr_reader :timings
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)
36
+ [compiler.timings.reduce(:+)]
37
+ end
38
+ end
39
+
40
+ sig do
41
+ params(
42
+ benchmark: ::Benchmark::Report,
43
+ options: ::CSVPlusPlus::Options,
44
+ runtime: ::CSVPlusPlus::Runtime::Runtime
45
+ ).void
46
+ end
47
+ # @param benchmark [::Benchmark::Report]
48
+ def initialize(benchmark:, options:, runtime:)
49
+ super(options:, runtime:)
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 }
56
+ # Time the Compiler#outputting! stage
57
+ # rubocop:disable Naming/BlockForwarding
58
+ def outputting!(&block)
59
+ time_stage('Writing the spreadsheet') { super(&block) }
60
+ end
61
+ # rubocop:enable Naming/BlockForwarding
62
+
63
+ protected
64
+
65
+ sig { override.void }
66
+ def parse_code_section!
67
+ time_stage('Parsing code section') { super }
68
+ end
69
+
70
+ sig { override.returns(::T::Array[::CSVPlusPlus::Row]) }
71
+ def parse_csv_section!
72
+ time_stage('Parsing CSV section') { super }
73
+ end
74
+
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) }
86
+ end
87
+ # rubocop:enable Naming/BlockForwarding
88
+
89
+ sig do
90
+ override
91
+ .params(template: ::CSVPlusPlus::Template)
92
+ .returns(::T::Array[::T::Array[::CSVPlusPlus::Entities::Entity]])
93
+ end
94
+ def resolve_all_cells!(template)
95
+ time_stage('Resolving each cell') { super(template) }
96
+ end
97
+
98
+ private
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
106
+ def time_stage(stage, &block)
107
+ ret = ::T.let(nil, ::T.nilable(::T.type_parameter(:R)))
108
+ @timings << ::T.unsafe(@benchmark.report(stage) { ret = block.call })
109
+ ret
110
+ end
111
+ end
112
+ end
@@ -1,8 +1,6 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
- require_relative './language/cell_value.tab'
4
- require_relative './modifier'
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::Language::CellValueParser.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,71 +1,96 @@
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]
33
43
  def handle_error(error)
44
+ # make sure that we're on a newline (verbose mode might be in the middle of printing a benchmark)
45
+ puts("\n\n") if @options.verbose
46
+
34
47
  case error
35
- when ::CSVPlusPlus::Error
48
+ when ::CSVPlusPlus::Error::Error
36
49
  handle_internal_error(error)
37
50
  when ::Google::Apis::ClientError
38
51
  handle_google_error(error)
39
52
  else
40
- # TODO: more if verbose?
41
- warn(error.message)
53
+ unhandled_error(error)
42
54
  end
43
55
  end
44
56
 
57
+ private
58
+
59
+ sig { void }
45
60
  # Handle the supplied command line options, setting +@options+ or throw an error if anything is invalid
46
61
  def parse_options!
47
- @options = ::CSVPlusPlus::Options.new
48
62
  option_parser.parse!
49
63
  validate_options
50
64
  rescue ::OptionParser::InvalidOption => e
51
- raise(::CSVPlusPlus::Error, e.message)
65
+ raise(::CSVPlusPlus::Error::Error, e.message)
52
66
  end
53
67
 
54
- # @return [String]
55
- def to_s
56
- "CLI(options: #{options})"
57
- end
68
+ sig { params(error: ::StandardError).void }
69
+ # An error was thrown that we weren't planning on
70
+ def unhandled_error(error)
71
+ warn(
72
+ <<~ERROR_MESSAGE)
73
+ An unexpected error was encountered. Please try running again with --verbose and
74
+ reporting the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
75
+ ERROR_MESSAGE
58
76
 
59
- private
77
+ return unless @options.verbose
60
78
 
79
+ warn(error.full_message)
80
+ warn("Cause: #{error.cause}") if error.cause
81
+ end
82
+
83
+ sig { params(error: ::CSVPlusPlus::Error::Error).void }
61
84
  def handle_internal_error(error)
62
- if error.is_a?(::CSVPlusPlus::Language::SyntaxError)
85
+ case error
86
+ when ::CSVPlusPlus::Error::SyntaxError
63
87
  warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
64
88
  else
65
89
  warn(error.message)
66
90
  end
67
91
  end
68
92
 
93
+ sig { params(error: ::Google::Apis::ClientError).void }
69
94
  def handle_google_error(error)
70
95
  warn("Error making Google Sheets API request: #{error.message}")
71
96
  return unless @options.verbose
@@ -73,14 +98,16 @@ module CSVPlusPlus
73
98
  warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
74
99
  end
75
100
 
101
+ sig { void }
76
102
  def validate_options
77
103
  error_message = @options.validate
78
104
  return if error_message.nil?
79
105
 
80
106
  puts(option_parser)
81
- raise(::CSVPlusPlus::Error, error_message)
107
+ raise(::CSVPlusPlus::Error::Error, error_message)
82
108
  end
83
109
 
110
+ sig { returns(::OptionParser) }
84
111
  def option_parser
85
112
  ::OptionParser.new do |parser|
86
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,19 +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
11
12
 
12
- # create an instance from a string like "#FFF" or "#FFFFFF"
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
21
+
22
+ HEX_STRING_REGEXP = /^#?([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i
23
+ public_constant :HEX_STRING_REGEXP
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
+ #
31
+ # @return [boolean]
32
+ def self.valid_hex_string?(hex_string)
33
+ !(hex_string.strip =~ ::CSVPlusPlus::Color::HEX_STRING_REGEXP).nil?
34
+ end
35
+
36
+ sig { params(hex_string: ::String).void }
37
+ # Create an instance from a string like "#FFF" or "#FFFFFF"
13
38
  #
14
39
  # @param hex_string [String] The hex string input to parse
40
+ # rubocop:disable Metrics/CyclomaticComplexity
15
41
  def initialize(hex_string)
16
- @red_hex, @green_hex, @blue_hex = hex_string
17
- .gsub(/^#?/, '')
18
- .match(/([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i)
42
+ red_hex, green_hex, blue_hex = hex_string.strip.match(::CSVPlusPlus::Color::HEX_STRING_REGEXP)
19
43
  &.captures
20
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)
21
50
  end
51
+ # rubocop:enable Metrics/CyclomaticComplexity
22
52
 
53
+ sig { returns(::Float) }
23
54
  # The percent (decimal between 0-1) of red
24
55
  #
25
56
  # @return [Numeric]
@@ -27,6 +58,7 @@ module CSVPlusPlus
27
58
  hex_to_percent(@red_hex)
28
59
  end
29
60
 
61
+ sig { returns(::Float) }
30
62
  # The percent (decimal between 0-1) of green
31
63
  #
32
64
  # @return [Numeric]
@@ -34,6 +66,7 @@ module CSVPlusPlus
34
66
  hex_to_percent(@green_hex)
35
67
  end
36
68
 
69
+ sig { returns(::Float) }
37
70
  # The percent (decimal between 0-1) of blue
38
71
  #
39
72
  # @return [Numeric]
@@ -41,18 +74,15 @@ module CSVPlusPlus
41
74
  hex_to_percent(@blue_hex)
42
75
  end
43
76
 
77
+ sig { returns(::String) }
44
78
  # Create a hex representation of the color (without a '#')
45
79
  #
46
- # @return [String]
80
+ # @return [::String]
47
81
  def to_hex
48
82
  [@red_hex, @green_hex, @blue_hex].join
49
83
  end
50
84
 
51
- # @return [String]
52
- def to_s
53
- "Color(r: #{@red_hex}, g: #{@green_hex}, b: #{@blue_hex})"
54
- end
55
-
85
+ sig { params(other: ::Object).returns(::T::Boolean) }
56
86
  # @return [boolean]
57
87
  def ==(other)
58
88
  other.is_a?(self.class) &&
@@ -63,6 +93,7 @@ module CSVPlusPlus
63
93
 
64
94
  private
65
95
 
96
+ sig { params(hex: ::String).returns(::Float) }
66
97
  def hex_to_percent(hex)
67
98
  hex.to_i(16) / 255.0
68
99
  end