csv_plus_plus 0.1.2 → 0.2.0
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.
- 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
|