csv_plus_plus 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -5
- data/{CHANGELOG.md → docs/CHANGELOG.md} +25 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +70 -20
- data/lib/csv_plus_plus/cell.rb +29 -41
- data/lib/csv_plus_plus/cli.rb +53 -80
- data/lib/csv_plus_plus/cli_flag.rb +71 -71
- data/lib/csv_plus_plus/color.rb +32 -7
- data/lib/csv_plus_plus/compiler.rb +98 -66
- data/lib/csv_plus_plus/entities/ast_builder.rb +30 -39
- data/lib/csv_plus_plus/entities/boolean.rb +26 -10
- data/lib/csv_plus_plus/entities/builtins.rb +66 -24
- data/lib/csv_plus_plus/entities/date.rb +42 -6
- data/lib/csv_plus_plus/entities/entity.rb +17 -69
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +44 -0
- data/lib/csv_plus_plus/entities/function.rb +34 -11
- data/lib/csv_plus_plus/entities/function_call.rb +49 -10
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +30 -11
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +43 -13
- data/lib/csv_plus_plus/entities/string.rb +23 -7
- data/lib/csv_plus_plus/entities.rb +7 -16
- data/lib/csv_plus_plus/error/cli_error.rb +17 -0
- data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
- data/lib/csv_plus_plus/error/error.rb +25 -2
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -12
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +34 -12
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +21 -27
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +8 -0
- data/lib/csv_plus_plus/error.rb +5 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +25 -10
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +58 -17
- data/lib/csv_plus_plus/lexer.rb +64 -1
- data/lib/csv_plus_plus/modifier/conditional_formatting.rb +1 -0
- data/lib/csv_plus_plus/modifier/data_validation.rb +138 -0
- data/lib/csv_plus_plus/modifier/expand.rb +78 -0
- data/lib/csv_plus_plus/modifier/google_sheet_modifier.rb +133 -0
- data/lib/csv_plus_plus/modifier/modifier.rb +222 -0
- data/lib/csv_plus_plus/modifier/modifier_validator.rb +243 -0
- data/lib/csv_plus_plus/modifier/rubyxl_modifier.rb +84 -0
- data/lib/csv_plus_plus/modifier.rb +89 -160
- data/lib/csv_plus_plus/options/file_options.rb +49 -0
- data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
- data/lib/csv_plus_plus/options/options.rb +97 -0
- data/lib/csv_plus_plus/options.rb +34 -77
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +66 -67
- data/lib/csv_plus_plus/parser/code_section.tab.rb +86 -83
- data/lib/csv_plus_plus/parser/modifier.tab.rb +57 -53
- data/lib/csv_plus_plus/reader/csv.rb +50 -0
- data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
- data/lib/csv_plus_plus/reader/reader.rb +27 -0
- data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
- data/lib/csv_plus_plus/reader.rb +14 -0
- data/lib/csv_plus_plus/row.rb +53 -12
- data/lib/csv_plus_plus/runtime/graph.rb +68 -0
- data/lib/csv_plus_plus/runtime/position.rb +242 -0
- data/lib/csv_plus_plus/runtime/references.rb +115 -0
- data/lib/csv_plus_plus/runtime/runtime.rb +132 -0
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +34 -191
- data/lib/csv_plus_plus/source_code.rb +71 -0
- data/lib/csv_plus_plus/template.rb +71 -39
- data/lib/csv_plus_plus/version.rb +2 -1
- data/lib/csv_plus_plus/writer/csv.rb +37 -8
- data/lib/csv_plus_plus/writer/excel.rb +25 -5
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -13
- data/lib/csv_plus_plus/writer/google_sheets.rb +29 -85
- data/lib/csv_plus_plus/writer/google_sheets_builder.rb +179 -0
- data/lib/csv_plus_plus/writer/merger.rb +31 -0
- data/lib/csv_plus_plus/writer/open_document.rb +21 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +140 -42
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +79 -10
- data/lib/csv_plus_plus.rb +47 -18
- metadata +50 -21
- data/lib/csv_plus_plus/can_define_references.rb +0 -88
- data/lib/csv_plus_plus/can_resolve_references.rb +0 -8
- data/lib/csv_plus_plus/data_validation.rb +0 -138
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -60
- data/lib/csv_plus_plus/entities/variable.rb +0 -25
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -58
- data/lib/csv_plus_plus/expand.rb +0 -20
- data/lib/csv_plus_plus/google_options.rb +0 -27
- data/lib/csv_plus_plus/graph.rb +0 -62
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -85
- data/lib/csv_plus_plus/references.rb +0 -68
- data/lib/csv_plus_plus/scope.rb +0 -196
- data/lib/csv_plus_plus/validated_modifier.rb +0 -164
- data/lib/csv_plus_plus/writer/base_writer.rb +0 -20
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +0 -147
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +0 -77
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +0 -59
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module CSVPlusPlus
|
4
|
-
# The Google-specific options a user can supply
|
5
|
-
#
|
6
|
-
# attr sheet_id [String] The ID of the Google Sheet to write to
|
7
|
-
GoogleOptions =
|
8
|
-
::Struct.new(:sheet_id) do
|
9
|
-
# Format a string with a verbose description of what we're doing with the options
|
10
|
-
#
|
11
|
-
# @return [String]
|
12
|
-
def verbose_summary
|
13
|
-
<<~SUMMARY
|
14
|
-
## Google Sheets Options
|
15
|
-
|
16
|
-
> Sheet ID | #{sheet_id}
|
17
|
-
SUMMARY
|
18
|
-
end
|
19
|
-
|
20
|
-
# @return [String]
|
21
|
-
def to_s
|
22
|
-
"GoogleOptions(sheet_id: #{sheet_id})"
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
public_constant :GoogleOptions
|
27
|
-
end
|
data/lib/csv_plus_plus/graph.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'tsort'
|
4
|
-
|
5
|
-
module CSVPlusPlus
|
6
|
-
# Graph ordering and searching functions
|
7
|
-
module Graph
|
8
|
-
# Get a list of all variables references in a given +ast+
|
9
|
-
# TODO: this is only used in one place - refactor it
|
10
|
-
def self.variable_references(ast, runtime, include_runtime_variables: false)
|
11
|
-
depth_first_search(ast) do |node|
|
12
|
-
next unless node.variable?
|
13
|
-
|
14
|
-
node.id if !runtime.runtime_variable?(node.id) || include_runtime_variables
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# Create a dependency graph of +variables+
|
19
|
-
def self.dependency_graph(variables, runtime)
|
20
|
-
::CSVPlusPlus::Graph::DependencyGraph[
|
21
|
-
variables.map { |var_id, ast| [var_id, variable_references(ast, runtime)] }
|
22
|
-
]
|
23
|
-
end
|
24
|
-
|
25
|
-
# Perform a topological sort on a +DependencyGraph+. A toplogical sort is noteworthy
|
26
|
-
# because it will give us the order in which we need to resolve our variable dependencies.
|
27
|
-
#
|
28
|
-
# Given this dependency graph:
|
29
|
-
#
|
30
|
-
# { a: [b c], b: [c], c: [d], d: [] }
|
31
|
-
#
|
32
|
-
# it will return:
|
33
|
-
#
|
34
|
-
# [d, c, b, a]
|
35
|
-
#
|
36
|
-
def self.topological_sort(dependencies)
|
37
|
-
dependencies.tsort
|
38
|
-
end
|
39
|
-
|
40
|
-
# Do a DFS on an AST starting at +node+
|
41
|
-
def self.depth_first_search(node, accum = [], &)
|
42
|
-
ret = yield(node)
|
43
|
-
accum << ret unless ret.nil?
|
44
|
-
|
45
|
-
return accum unless node.function_call?
|
46
|
-
|
47
|
-
node.arguments.each { |n| depth_first_search(n, accum, &) }
|
48
|
-
accum
|
49
|
-
end
|
50
|
-
|
51
|
-
# A dependency graph represented as a +Hash+ which will be used by our +topological_sort+ function
|
52
|
-
class DependencyGraph < Hash
|
53
|
-
include ::TSort
|
54
|
-
alias tsort_each_node each_key
|
55
|
-
|
56
|
-
# sort each child
|
57
|
-
def tsort_each_child(node, &)
|
58
|
-
fetch(node).each(&)
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
@@ -1,85 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../color'
|
4
|
-
|
5
|
-
module CSVPlusPlus
|
6
|
-
# Common methods to be mixed into the Racc parsers
|
7
|
-
#
|
8
|
-
# @attr_reader tokens [Array]
|
9
|
-
module Lexer
|
10
|
-
attr_reader :tokens
|
11
|
-
|
12
|
-
# Initialize a lexer instance with an empty +@tokens+
|
13
|
-
def initialize(tokens: [])
|
14
|
-
@tokens = tokens
|
15
|
-
end
|
16
|
-
|
17
|
-
# Used by racc to iterate each token
|
18
|
-
#
|
19
|
-
# @return [Array<(String, String)>]
|
20
|
-
def next_token
|
21
|
-
@tokens.shift
|
22
|
-
end
|
23
|
-
|
24
|
-
# Orchestate the tokenizing, parsing and error handling of parsing input. Each instance will implement their own
|
25
|
-
# #tokenizer method
|
26
|
-
#
|
27
|
-
# @return [Lexer#return_value] Each instance will define it's own +return_value+ with the result of parsing
|
28
|
-
def parse(input, runtime)
|
29
|
-
return if input.nil?
|
30
|
-
|
31
|
-
return return_value unless anything_to_parse?(input)
|
32
|
-
|
33
|
-
tokenize(input, runtime)
|
34
|
-
do_parse
|
35
|
-
return_value
|
36
|
-
rescue ::Racc::ParseError => e
|
37
|
-
runtime.raise_formula_syntax_error("Error parsing #{parse_subject}", e.message, wrapped_error: e)
|
38
|
-
rescue ::CSVPlusPlus::Error::ModifierValidationError => e
|
39
|
-
raise(::CSVPlusPlus::Error::ModifierSyntaxError.new(runtime, wrapped_error: e))
|
40
|
-
end
|
41
|
-
|
42
|
-
TOKEN_LIBRARY = {
|
43
|
-
A1_NOTATION: [::CSVPlusPlus::Entities::CellReference::A1_NOTATION_REGEXP, :A1_NOTATION],
|
44
|
-
FALSE: [/false/i, :FALSE],
|
45
|
-
HEX_COLOR: [::CSVPlusPlus::Color::HEX_STRING_REGEXP, :HEX_COLOR],
|
46
|
-
ID: [/[$!\w:]+/, :ID],
|
47
|
-
INFIX_OP: [%r{\^|\+|-|\*|/|&|<|>|<=|>=|<>}, :INFIX_OP],
|
48
|
-
NUMBER: [/-?[\d.]+/, :NUMBER],
|
49
|
-
STRING: [%r{"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"}, :STRING],
|
50
|
-
TRUE: [/true/i, :TRUE],
|
51
|
-
VAR_REF: [/\$\$/, :VAR_REF]
|
52
|
-
}.freeze
|
53
|
-
public_constant :TOKEN_LIBRARY
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def tokenize(input, runtime)
|
58
|
-
return if input.nil?
|
59
|
-
|
60
|
-
t = tokenizer.scan(input)
|
61
|
-
|
62
|
-
until t.scanner.empty?
|
63
|
-
next if t.matches_ignore?
|
64
|
-
|
65
|
-
return if t.stop?
|
66
|
-
|
67
|
-
t.scan_tokens!
|
68
|
-
consume_token(t, runtime)
|
69
|
-
end
|
70
|
-
|
71
|
-
@tokens << %i[EOL EOL]
|
72
|
-
end
|
73
|
-
|
74
|
-
def consume_token(tokenizer, runtime)
|
75
|
-
if tokenizer.last_token
|
76
|
-
@tokens << [tokenizer.last_token, tokenizer.last_match]
|
77
|
-
elsif tokenizer.scan_catchall
|
78
|
-
@tokens << [tokenizer.last_match, tokenizer.last_match]
|
79
|
-
else
|
80
|
-
# TODO: this should raise a modifier_syntax_error if we're on the modifier parser
|
81
|
-
runtime.raise_formula_syntax_error("Unable to parse #{parse_subject} starting at", tokenizer.peek)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'graph'
|
4
|
-
require_relative 'scope'
|
5
|
-
|
6
|
-
module CSVPlusPlus
|
7
|
-
# References in an AST that need to be resolved
|
8
|
-
#
|
9
|
-
# @attr functions [Array<Entities::Function>] Functions references
|
10
|
-
# @attr variables [Array<Entities::Variable>] Variable references
|
11
|
-
class References
|
12
|
-
attr_accessor :functions, :variables
|
13
|
-
|
14
|
-
# Extract references from an AST and return them in a new +References+ object
|
15
|
-
#
|
16
|
-
# @param ast [Entity] An +Entity+ to do a depth first search on for references. Entities can be
|
17
|
-
# infinitely deep because they can contain other function calls as params to a function call
|
18
|
-
# @param scope [Scope] The +CodeSection+ containing all currently defined functions & variables
|
19
|
-
#
|
20
|
-
# @return [References]
|
21
|
-
def self.extract(ast, scope)
|
22
|
-
new.tap do |refs|
|
23
|
-
::CSVPlusPlus::Graph.depth_first_search(ast) do |node|
|
24
|
-
next unless node.function_call? || node.variable?
|
25
|
-
|
26
|
-
refs.functions << node if function_reference?(node, scope)
|
27
|
-
refs.variables << node if node.variable?
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Is the node a resolvable reference?
|
33
|
-
#
|
34
|
-
# @param node [Entity] The node to check if it's resolvable
|
35
|
-
#
|
36
|
-
# @return [boolean]
|
37
|
-
# TODO: move this into the Entity subclasses
|
38
|
-
def self.function_reference?(node, scope)
|
39
|
-
node.function_call? && (scope.defined_function?(node.id) \
|
40
|
-
|| ::CSVPlusPlus::Entities::Builtins::FUNCTIONS.key?(node.id))
|
41
|
-
end
|
42
|
-
|
43
|
-
private_class_method :function_reference?
|
44
|
-
|
45
|
-
# Create an object with empty references. The caller will build them up as it depth-first-searches
|
46
|
-
def initialize
|
47
|
-
@functions = []
|
48
|
-
@variables = []
|
49
|
-
end
|
50
|
-
|
51
|
-
# Are there any references to be resolved?
|
52
|
-
#
|
53
|
-
# @return [boolean]
|
54
|
-
def empty?
|
55
|
-
@functions.empty? && @variables.empty?
|
56
|
-
end
|
57
|
-
|
58
|
-
# @return [String]
|
59
|
-
def to_s
|
60
|
-
"References(functions: #{@functions}, variables: #{@variables})"
|
61
|
-
end
|
62
|
-
|
63
|
-
# @return [boolean]
|
64
|
-
def ==(other)
|
65
|
-
@functions == other.functions && @variables == other.variables
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
data/lib/csv_plus_plus/scope.rb
DELETED
@@ -1,196 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'can_define_references'
|
4
|
-
require_relative 'entities'
|
5
|
-
require_relative 'graph'
|
6
|
-
require_relative 'references'
|
7
|
-
|
8
|
-
module CSVPlusPlus
|
9
|
-
# A class representing the scope of the current Template and responsible for resolving variables
|
10
|
-
#
|
11
|
-
# @attr_reader functions [Hash<Symbol, Function>] The currently functions defined
|
12
|
-
# @attr_reader runtime [Runtime] The compiler's current runtime
|
13
|
-
# @attr_reader variables [Hash<Symbol, Entity>] The currently defined variables
|
14
|
-
#
|
15
|
-
# rubocop:disable Metrics/ClassLength
|
16
|
-
class Scope
|
17
|
-
include ::CSVPlusPlus::CanDefineReferences
|
18
|
-
# TODO: split out a CanResolveReferences
|
19
|
-
|
20
|
-
attr_reader :functions, :runtime, :variables
|
21
|
-
|
22
|
-
# @param runtime [Runtime]
|
23
|
-
def initialize(runtime:, functions: {}, variables: {})
|
24
|
-
@runtime = runtime
|
25
|
-
@functions = functions
|
26
|
-
@variables = variables
|
27
|
-
end
|
28
|
-
|
29
|
-
# Resolve all values in the ast of the current cell being processed
|
30
|
-
#
|
31
|
-
# @return [Entity]
|
32
|
-
def resolve_cell_value
|
33
|
-
return unless (ast = @runtime.cell&.ast)
|
34
|
-
|
35
|
-
last_round = nil
|
36
|
-
loop do
|
37
|
-
refs = ::CSVPlusPlus::References.extract(ast, self)
|
38
|
-
return ast if refs.empty?
|
39
|
-
|
40
|
-
# TODO: throw an error here instead I think - basically we did a round and didn't make progress
|
41
|
-
return ast if last_round == refs
|
42
|
-
|
43
|
-
ast = resolve_functions(resolve_variables(ast, refs.variables), refs.functions)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# Bind +var_id+ to the current cell (based on where +@runtime+ is currently pointing).
|
48
|
-
#
|
49
|
-
# @param var_id [Symbol] The name of the variable to bind the cell reference to
|
50
|
-
#
|
51
|
-
# @return [CellReference]
|
52
|
-
def bind_variable_to_cell(var_id)
|
53
|
-
::CSVPlusPlus::Entities::CellReference.from_index(
|
54
|
-
cell_index: runtime.cell_index,
|
55
|
-
row_index: runtime.row_index
|
56
|
-
).tap do |cell_reference|
|
57
|
-
def_variable(var_id, cell_reference)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# @return [String]
|
62
|
-
def to_s
|
63
|
-
"Scope(functions: #{@functions}, runtime: #{@runtime}, variables: #{@variables})"
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
# Resolve all variable references defined statically in the code section
|
69
|
-
# TODO: experiment with getting rid of this - does it even play correctly with runtime vars?
|
70
|
-
def resolve_static_variables!
|
71
|
-
last_var_dependencies = {}
|
72
|
-
loop do
|
73
|
-
var_dependencies, resolution_order = variable_resolution_order(only_static_vars(variables))
|
74
|
-
return if var_dependencies == last_var_dependencies
|
75
|
-
|
76
|
-
# TODO: make the contract better here
|
77
|
-
@variables = resolve_dependencies(var_dependencies, resolution_order, variables)
|
78
|
-
last_var_dependencies = var_dependencies.clone
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def only_static_vars(var_dependencies)
|
83
|
-
var_dependencies.reject { |k| @runtime.runtime_variable?(k) }
|
84
|
-
end
|
85
|
-
|
86
|
-
def resolve_functions(ast, refs)
|
87
|
-
refs.reduce(ast.dup) do |acc, elem|
|
88
|
-
function_replace(acc, elem.id, resolve_function(elem.id))
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def resolve_variables(ast, refs)
|
93
|
-
refs.reduce(ast.dup) do |acc, elem|
|
94
|
-
variable_replace(acc, elem.id, resolve_variable(elem.id))
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
# Make a copy of the AST represented by +node+ and replace +fn_id+ with +replacement+ throughout
|
99
|
-
# rubocop:disable Metrics/MethodLength
|
100
|
-
def function_replace(node, fn_id, replacement)
|
101
|
-
if node.function_call? && node.id == fn_id
|
102
|
-
call_function_or_runtime_value(replacement, node)
|
103
|
-
elsif node.function_call?
|
104
|
-
# not our function, but continue our depth first search on it
|
105
|
-
::CSVPlusPlus::Entities::FunctionCall.new(
|
106
|
-
node.id,
|
107
|
-
node.arguments.map { |n| function_replace(n, fn_id, replacement) },
|
108
|
-
infix: node.infix
|
109
|
-
)
|
110
|
-
else
|
111
|
-
node
|
112
|
-
end
|
113
|
-
end
|
114
|
-
# rubocop:enable Metrics/MethodLength
|
115
|
-
|
116
|
-
def resolve_function(fn_id)
|
117
|
-
id = fn_id.to_sym
|
118
|
-
return functions[id] if defined_function?(id)
|
119
|
-
|
120
|
-
::CSVPlusPlus::Entities::Builtins::FUNCTIONS[id]
|
121
|
-
end
|
122
|
-
|
123
|
-
def call_function_or_runtime_value(function_or_runtime_value, function_call)
|
124
|
-
if function_or_runtime_value.function?
|
125
|
-
call_function(function_or_runtime_value, function_call)
|
126
|
-
else
|
127
|
-
function_or_runtime_value.resolve_fn.call(@runtime, function_call.arguments)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def call_function(function, function_call)
|
132
|
-
i = 0
|
133
|
-
function.arguments.reduce(function.body.dup) do |ast, argument|
|
134
|
-
variable_replace(ast, argument, function_call.arguments[i]).tap do
|
135
|
-
i += 1
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
# Make a copy of the AST represented by +node+ and replace +var_id+ with +replacement+ throughout
|
141
|
-
def variable_replace(node, var_id, replacement)
|
142
|
-
if node.function_call?
|
143
|
-
arguments = node.arguments.map { |n| variable_replace(n, var_id, replacement) }
|
144
|
-
# TODO: refactor these places where we copy functions... it's brittle with the kwargs
|
145
|
-
::CSVPlusPlus::Entities::FunctionCall.new(node.id, arguments, infix: node.infix)
|
146
|
-
elsif node.variable? && node.id == var_id
|
147
|
-
replacement
|
148
|
-
else
|
149
|
-
node
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def resolve_variable(var_id)
|
154
|
-
id = var_id.to_sym
|
155
|
-
return variables[id] if defined_variable?(id)
|
156
|
-
|
157
|
-
# this will throw a syntax error if it doesn't exist (which is what we want)
|
158
|
-
@runtime.runtime_value(id)
|
159
|
-
end
|
160
|
-
|
161
|
-
def check_unbound_vars(dependencies, variables)
|
162
|
-
unbound_vars = dependencies.values.flatten - variables.keys
|
163
|
-
return if unbound_vars.empty?
|
164
|
-
|
165
|
-
@runtime.raise_formula_syntax_error('Undefined variables', unbound_vars.map(&:to_s).join(', '))
|
166
|
-
end
|
167
|
-
|
168
|
-
def variable_resolution_order(variables)
|
169
|
-
# we have a hash of variables => ASTs but they might have references to each other, so
|
170
|
-
# we need to interpolate them first (before interpolating the cell values)
|
171
|
-
var_dependencies = ::CSVPlusPlus::Graph.dependency_graph(variables, @runtime)
|
172
|
-
# are there any references that we don't have variables for? (undefined variable)
|
173
|
-
check_unbound_vars(var_dependencies, variables)
|
174
|
-
|
175
|
-
# a topological sort will give us the order of dependencies
|
176
|
-
[var_dependencies, ::CSVPlusPlus::Graph.topological_sort(var_dependencies)]
|
177
|
-
# TODO: don't expose this exception directly to the caller
|
178
|
-
rescue ::TSort::Cyclic
|
179
|
-
@runtime.raise_formula_syntax_error('Cyclic variable dependency detected', var_refs.keys)
|
180
|
-
end
|
181
|
-
|
182
|
-
def resolve_dependencies(var_dependencies, resolution_order, variables)
|
183
|
-
{}.tap do |resolved_vars|
|
184
|
-
# for each var and each dependency it has, build up and mutate resolved_vars
|
185
|
-
resolution_order.each do |var|
|
186
|
-
resolved_vars[var] = variables[var].dup
|
187
|
-
|
188
|
-
var_dependencies[var].each do |dependency|
|
189
|
-
resolved_vars[var] = variable_replace(resolved_vars[var], dependency, variables[dependency])
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|
195
|
-
# rubocop:enable Metrics/ClassLength
|
196
|
-
end
|
@@ -1,164 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'data_validation'
|
4
|
-
require_relative 'modifier'
|
5
|
-
|
6
|
-
module CSVPlusPlus
|
7
|
-
# Validates and coerces modifier values as they are parsed.
|
8
|
-
#
|
9
|
-
# Previously this logic was handled in the parser's grammar, but with the introduction of variable binding, the
|
10
|
-
# grammar is no longer context free so we need the parser to be a little looser on what it accepts and validate it
|
11
|
-
# here. Having this layer is also nice because we can provide better error messages to the user for what went
|
12
|
-
# wrong during the parse.
|
13
|
-
class ValidatedModifier < ::CSVPlusPlus::Modifier
|
14
|
-
# Validates that +border+ is 'all', 'top', 'bottom', 'left' or 'right'.
|
15
|
-
#
|
16
|
-
# @param value [String] The unvalidated user input
|
17
|
-
def border=(value)
|
18
|
-
super(one_of(:border, value, %i[all top bottom left right]))
|
19
|
-
end
|
20
|
-
|
21
|
-
# Validates that +bordercolor+ is a hex color.
|
22
|
-
#
|
23
|
-
# @param value [String] The unvalidated user input
|
24
|
-
def bordercolor=(value)
|
25
|
-
super(color_value(:bordercolor, value))
|
26
|
-
end
|
27
|
-
|
28
|
-
# Validates that +borderstyle+ is 'dashed', 'dotted', 'double', 'solid', 'solid_medium' or 'solid_thick'.
|
29
|
-
#
|
30
|
-
# @param value [String] The unvalidated user input
|
31
|
-
def borderstyle=(value)
|
32
|
-
super(one_of(:borderstyle, value, %i[dashed dotted double solid solid_medium solid_thick]))
|
33
|
-
end
|
34
|
-
|
35
|
-
# Validates that +color+ is a hex color.
|
36
|
-
#
|
37
|
-
# @param value [String] The unvalidated user input
|
38
|
-
def color=(value)
|
39
|
-
super(color_value(:color, value))
|
40
|
-
end
|
41
|
-
|
42
|
-
# Validates that +expand+ is a positive integer.
|
43
|
-
#
|
44
|
-
# @param value [String] The unvalidated user input
|
45
|
-
def expand=(value)
|
46
|
-
super(::CSVPlusPlus::Expand.new(positive_integer(:expand, value)))
|
47
|
-
end
|
48
|
-
|
49
|
-
# Validates that +fontcolor+ is a hex color.
|
50
|
-
#
|
51
|
-
# @param value [String] The unvalidated user input
|
52
|
-
def fontcolor=(value)
|
53
|
-
super(color_value(:fontcolor, value))
|
54
|
-
end
|
55
|
-
|
56
|
-
# Validates that +fontcolor+ is a hex color.
|
57
|
-
def fontfamily=(value)
|
58
|
-
super(matches_regexp(:fontfamily, unquote(value), /^[\w\s]+$/, 'It is not a valid font family.'))
|
59
|
-
end
|
60
|
-
|
61
|
-
# Validates that +fontsize+ is a positive integer
|
62
|
-
#
|
63
|
-
# @param value [String] The unvalidated user input
|
64
|
-
def fontsize=(value)
|
65
|
-
super(positive_integer(:fontsize, value))
|
66
|
-
end
|
67
|
-
|
68
|
-
# Validates that +format+ is 'bold', 'italic', 'strikethrough' or 'underline'.
|
69
|
-
#
|
70
|
-
# @param value [String] The unvalidated user input
|
71
|
-
def format=(value)
|
72
|
-
super(one_of(:format, value, %i[bold italic strikethrough underline]))
|
73
|
-
end
|
74
|
-
|
75
|
-
# Validates that +halign+ is 'left', 'center' or 'right'.
|
76
|
-
#
|
77
|
-
# @param value [String] The unvalidated user input
|
78
|
-
def halign=(value)
|
79
|
-
super(one_of(:halign, value, %i[left center right]))
|
80
|
-
end
|
81
|
-
|
82
|
-
# Validates that +note+ is a quoted string.
|
83
|
-
#
|
84
|
-
# @param value [String] The unvalidated user input
|
85
|
-
|
86
|
-
# Validates that +numberformat+ is 'currency', 'date', 'date_time', 'number', 'percent', 'text', 'time' or
|
87
|
-
# 'scientific'.
|
88
|
-
#
|
89
|
-
# @param value [String] The unvalidated user input
|
90
|
-
def numberformat=(value)
|
91
|
-
super(one_of(:nubmerformat, value, %i[currency date date_time number percent text time scientific]))
|
92
|
-
end
|
93
|
-
|
94
|
-
# Validates that +valign+ is 'top', 'center' or 'bottom'.
|
95
|
-
#
|
96
|
-
# @param value [String] The unvalidated user input
|
97
|
-
def valign=(value)
|
98
|
-
super(one_of(:valign, value, %i[top center bottom]))
|
99
|
-
end
|
100
|
-
|
101
|
-
# Validates that the conditional validating rules are well-formed.
|
102
|
-
#
|
103
|
-
# Pretty much based off of the Google Sheets API spec here:
|
104
|
-
#
|
105
|
-
# @param value [String] The unvalidated user input
|
106
|
-
def validation=(value)
|
107
|
-
super(a_data_validation(:validation, value))
|
108
|
-
end
|
109
|
-
|
110
|
-
# Validates +variable+ is a valid variable identifier.
|
111
|
-
#
|
112
|
-
# @param value [String] The unvalidated user input
|
113
|
-
def var=(value)
|
114
|
-
# TODO: I need a shared definition of what a variable can be (I guess the :ID token)
|
115
|
-
super(matches_regexp(:var, value, /^\w+$/, 'It must be a sequence of letters, numbers and _.').to_sym)
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
# XXX centralize this :(((
|
121
|
-
def unquote(str)
|
122
|
-
# TODO: I'm pretty sure this isn't sufficient and we need to deal with the backslashes
|
123
|
-
str.gsub(/^['\s]*|['\s]*$/, '')
|
124
|
-
end
|
125
|
-
|
126
|
-
def a_data_validation(modifier, value)
|
127
|
-
data_validation = ::CSVPlusPlus::DataValidation.new(value)
|
128
|
-
return data_validation unless data_validation.valid?
|
129
|
-
|
130
|
-
raise_error(modifier, value, message: data_validation.invalid_reason)
|
131
|
-
end
|
132
|
-
|
133
|
-
def color_value(modifier, value)
|
134
|
-
unless ::CSVPlusPlus::Color.valid_hex_string?(value)
|
135
|
-
raise_error(modifier, value, message: 'It must be a 3 or 6 digit hex code.')
|
136
|
-
end
|
137
|
-
|
138
|
-
::CSVPlusPlus::Color.new(value)
|
139
|
-
end
|
140
|
-
|
141
|
-
def matches_regexp(modifier, value, regexp, message)
|
142
|
-
raise_error(modifier, value, message:) unless value =~ regexp
|
143
|
-
value
|
144
|
-
end
|
145
|
-
|
146
|
-
def one_of(modifier, value, choices)
|
147
|
-
value.downcase.to_sym.tap do |v|
|
148
|
-
raise_error(modifier, value, choices:) unless choices.include?(v)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def positive_integer(modifier, value)
|
153
|
-
Integer(value, 10).tap do |i|
|
154
|
-
raise_error(modifier, value, message: 'It must be positive and greater than 0.') unless i.positive?
|
155
|
-
end
|
156
|
-
rescue ::ArgumentError
|
157
|
-
raise_error(modifier, value, message: 'It must be a valid (whole) number.')
|
158
|
-
end
|
159
|
-
|
160
|
-
def raise_error(modifier, bad_input, choices: nil, message: nil)
|
161
|
-
raise(::CSVPlusPlus::Error::ModifierValidationError.new(modifier, bad_input, choices:, message:))
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module CSVPlusPlus
|
4
|
-
module Writer
|
5
|
-
# Some shared functionality that all Writers should build on
|
6
|
-
class BaseWriter
|
7
|
-
attr_accessor :options
|
8
|
-
|
9
|
-
protected
|
10
|
-
|
11
|
-
# Open a CSV outputter to +filename+
|
12
|
-
def initialize(options)
|
13
|
-
@options = options
|
14
|
-
load_requires
|
15
|
-
end
|
16
|
-
|
17
|
-
def load_requires; end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|