csv_plus_plus 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11364af3824cbfff63df10d7b932a6ad2c7a222e1a40b39f151af82a150d5dc4
4
- data.tar.gz: 2d6fe31ae0e6a7dee0c19f31b0328e1b20973541c6e2c12cb64f3fbf96d8fa02
3
+ metadata.gz: c4f5c9b5a342102fa8e1c31b319fb05f19ad55278bc633435129254b5b2511a8
4
+ data.tar.gz: 0c2656d4d7b22d0b3b3311745ed5a70b789277c622f0f2d09d2b8a144de71a8d
5
5
  SHA512:
6
- metadata.gz: 623953f6ff728e1dfc1f7f329dbc89e5ae7b7687cebd6da387ea36f0b0659020142e5f2029533e46964b8bf51c2f78b79634b9bb346cad6e6c96ae69d6737fb1
7
- data.tar.gz: 0604bc094e8d287bcec1390ff884786fe8dbf12b5e6e4ec4785cc9cef2297f92360a9a60809c855630edd5f633b2bc16bf378cb0c47a1be7eb2a7f70ae6beb7a
6
+ metadata.gz: 90965e2275cc7988f4054b59e30523cd645b21c1adb89797828e86a767217b09ac174bbc17094604dd608b93defd79f4c9a74ccf7e2f20dd17995f8c1b5dadac
7
+ data.tar.gz: 291011896232e88a7bd0e252887459986812d2877c1d0383458250e1c30cf735d8b2bdaf5dd759f44fa5b8b33b08f74d780df7960622ab774e9f6d856628508f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## v0.0.5
2
+
3
+ - Support the --backup/-b option
4
+ - bin/csvpp (which does the same thing as bin/csv++ but will work better on other filesystems)
5
+ - Fix links in gemspec (which end up on rubygems.org)
6
+ - docs & tests
7
+
8
+ ## v0.0.4
9
+
10
+ - Excel support
11
+
1
12
  ## v0.0.3
2
13
 
3
14
  - Fix the gem package to include the bin/ file
data/README.md CHANGED
@@ -9,7 +9,7 @@ A tool that allows you to programatically author spreadsheets in your favorite t
9
9
  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:
10
10
 
11
11
  ```
12
- fees := 0.65 # my broker charges $0.65 a trade
12
+ fees := 0.50 # my broker charges $0.50 a trade
13
13
 
14
14
  price := cellref(C)
15
15
  quantity := cellref(D)
@@ -21,12 +21,17 @@ def profit() (price * quantity) - fees
21
21
  ![[expand]],[[format=bold]],,,"=PROFIT()",$$fees
22
22
  ```
23
23
 
24
- ## Predefined Variables
24
+ ## Variables
25
25
 
26
- * `$$rownum` - The current row number. The first row of the spreadsheet starts at 1
26
+ 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.
27
27
 
28
- ## Predefined Functions
28
+ ### Built-in Variables
29
29
 
30
+ * `$$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.
31
+
32
+ ## Functions
33
+
34
+ ### Built-in Functions
30
35
  * `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`.
31
36
 
32
37
  ## Modifiers
data/bin/csv++ CHANGED
@@ -1,83 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'optparse'
5
4
  require_relative '../lib/csv_plus_plus'
6
5
 
7
- options = ::CSVPlusPlus::Options.new
8
-
9
- option_parser =
10
- # rubocop:disable Metrics/BlockLength
11
- ::OptionParser.new do |parser|
12
- parser.on('-b', '--backup', 'Create a backup of the spreadsheet before applying changes.') do
13
- options.backup = true
14
- end
15
-
16
- parser.on(
17
- '-g SHEET_ID',
18
- '--google-sheet-id SHEET_ID',
19
- 'The id of the sheet - you can extract this from the URL: ' \
20
- 'https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0'
21
- ) do |v|
22
- options.google_sheet_id = v
23
- end
24
-
25
- parser.on('-c', '--create', "Create the sheet if it doesn't exist. It will use --sheet-name if specified") do
26
- options.create_if_not_exists = true
27
- end
28
-
29
- parser.on(
30
- '-k KEY_VALUES',
31
- '--key-values KEY_VALUES',
32
- 'A comma-separated list of key=values which will be made available to the template'
33
- ) do |v|
34
- options.key_values =
35
- begin
36
- [v.split('=')].to_h
37
- rescue ::StandardError
38
- {}
39
- end
40
- end
41
-
42
- parser.on('-n SHEET_NAME', '--sheet-name SHEET_NAME', 'The name of the sheet to apply the template to') do |v|
43
- options.sheet_name = v
44
- end
45
-
46
- parser.on('-o OUTPUT_FILE', '--output OUTPUT_FILE', 'The file to write to (must be .csv, .ods, .xls)') do |v|
47
- options.output_filename = v
48
- end
49
-
50
- parser.on('-v', '--verbose', 'Enable verbose output') do
51
- options.verbose = true
52
- end
53
-
54
- parser.on('-x OFFSET', '--offset-columns OFFSET', 'Apply the template offset by OFFSET cells') do |v|
55
- options.offset[0] = v
56
- end
57
-
58
- parser.on('-y OFFSET', '--offset-rows OFFSET', 'Apply the template offset by OFFSET rows') do |v|
59
- options.offset[1] = v
60
- end
61
-
62
- parser.on('-h', '--help', 'Show help information') do
63
- puts(parser)
64
- exit
65
- end
66
- end
67
- # rubocop:enable Metrics/BlockLength
68
-
69
- option_parser.parse!
70
-
71
- error_message = options.validate
72
- unless error_message.nil?
73
- warn(error_message)
74
- puts(option_parser)
75
- exit(1)
76
- end
77
-
78
- begin
79
- ::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, options)
80
- rescue ::CSVPlusPlus::Language::SyntaxError => e
81
- warn(options.verbose ? e.to_verbose_trace : e.to_trace)
82
- exit(1)
83
- end
6
+ ::CSVPlusPlus::CLI.launch_compiler!
data/bin/csvpp ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/csv_plus_plus'
5
+
6
+ ::CSVPlusPlus::CLI.launch_compiler!
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module CSVPlusPlus
6
+ # Handle running the application with the given CLI flags
7
+ class CLI
8
+ # handle any CLI flags and launch the compiler
9
+ def self.launch_compiler!
10
+ cli = new
11
+ cli.compile!
12
+ rescue ::StandardError => e
13
+ cli.handle_error(e)
14
+ exit(1)
15
+ end
16
+
17
+ # initialize
18
+ def initialize
19
+ parse_options!
20
+ end
21
+
22
+ # compile the given template, using the given CLI flags
23
+ def compile!
24
+ ::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, @options)
25
+ end
26
+
27
+ # (nicely) handle a given error. how it's handled depends on if it's our error and if @options.verbose
28
+ def handle_error(error)
29
+ case error
30
+ when ::CSVPlusPlus::Error
31
+ handle_internal_error(error)
32
+ when ::Google::Apis::ClientError
33
+ handle_google_error(error)
34
+ else
35
+ # TODO: more if verbose?
36
+ warn(error.message)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def handle_internal_error(error)
43
+ if error.is_a?(::CSVPlusPlus::Language::SyntaxError)
44
+ warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
45
+ else
46
+ warn(error.message)
47
+ end
48
+ end
49
+
50
+ def handle_google_error(error)
51
+ warn("Error making Google Sheets API request: #{error.message}")
52
+ return unless @options.verbose
53
+
54
+ warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
55
+ end
56
+
57
+ def parse_options!
58
+ @options = ::CSVPlusPlus::Options.new
59
+ option_parser.parse!
60
+ validate_options
61
+ end
62
+
63
+ def validate_options
64
+ error_message = @options.validate
65
+ return if error_message.nil?
66
+
67
+ puts(option_parser)
68
+ raise(::CSVPlusPlus::Error, error_message)
69
+ end
70
+
71
+ def option_parser
72
+ ::OptionParser.new do |parser|
73
+ parser.on('-h', '--help', 'Show help information') do
74
+ puts(parser)
75
+ exit
76
+ end
77
+
78
+ ::SUPPORTED_CSVPP_FLAGS.each do |f|
79
+ parser.on(f.short_flag, f.long_flag, f.description) { |v| f.handler.call(@options, v) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './google_options'
4
+
5
+ module CSVPlusPlus
6
+ # Individual CLI flags that a user can supply
7
+ class CliFlag
8
+ attr_reader :short_flag, :long_flag, :description, :handler
9
+
10
+ # initialize
11
+ def initialize(short_flag, long_flag, description, handler)
12
+ @short_flag = short_flag
13
+ @long_flag = long_flag
14
+ @description = description
15
+ @handler = handler
16
+ end
17
+
18
+ # to_s
19
+ def to_s
20
+ "#{@short_flag}, #{@long_flag} #{@description}"
21
+ end
22
+ end
23
+ end
24
+
25
+ SUPPORTED_CSVPP_FLAGS = [
26
+ ::CSVPlusPlus::CliFlag.new(
27
+ '-b',
28
+ '--backup',
29
+ 'Create a backup of the spreadsheet before applying changes.',
30
+ ->(options, _v) { options.backup = true }
31
+ ),
32
+ ::CSVPlusPlus::CliFlag.new(
33
+ '-c',
34
+ '--create',
35
+ "Create the sheet if it doesn't exist. It will use --sheet-name if specified",
36
+ ->(options, _v) { options.create_if_not_exists = true }
37
+ ),
38
+ ::CSVPlusPlus::CliFlag.new(
39
+ '-g SHEET_ID',
40
+ '--google-sheet-id SHEET_ID',
41
+ 'The id of the sheet - you can extract this from the URL: ' \
42
+ 'https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0',
43
+ ->(options, v) { options.google_sheet_id = v }
44
+ ),
45
+ ::CSVPlusPlus::CliFlag.new(
46
+ '-k',
47
+ '--key-values KEY_VALUES',
48
+ 'A comma-separated list of key=values which will be made available to the template',
49
+ lambda do |options, v|
50
+ options.key_values =
51
+ begin
52
+ [v.split('=')].to_h
53
+ rescue ::StandardError
54
+ {}
55
+ end
56
+ end
57
+ ),
58
+ ::CSVPlusPlus::CliFlag.new(
59
+ '-n SHEET_NAME',
60
+ '--sheet-name SHEET_NAME',
61
+ 'The name of the sheet to apply the template to',
62
+ ->(options, v) { options.sheet_name = v }
63
+ ),
64
+ ::CSVPlusPlus::CliFlag.new(
65
+ '-o OUTPUT_FILE',
66
+ '--output OUTPUT_FILE',
67
+ 'The file to write to (must be .csv, .ods, .xls)',
68
+ ->(options, v) { options.output_filename = v }
69
+ ),
70
+ ::CSVPlusPlus::CliFlag.new('-v', '--verbose', 'Enable verbose output', ->(options, _v) { options.verbose = true }),
71
+ ::CSVPlusPlus::CliFlag.new(
72
+ '-x OFFSET',
73
+ '--offset-columns OFFSET',
74
+ 'Apply the template offset by OFFSET cells',
75
+ ->(options, v) { options.offset[0] = v }
76
+ ),
77
+ ::CSVPlusPlus::CliFlag.new(
78
+ '-y OFFSET',
79
+ '--offset-rows OFFSET',
80
+ 'Apply the template offset by OFFSET rows',
81
+ ->(options, v) { options.offset[1] = v }
82
+ )
83
+ ].freeze
@@ -3,20 +3,54 @@
3
3
  module CSVPlusPlus
4
4
  # A color value
5
5
  class Color
6
- attr_reader :red, :green, :blue
6
+ attr_reader :red_hex, :green_hex, :blue_hex
7
7
 
8
8
  # create an instance from a string like "#FFF" or "#FFFFFF"
9
9
  def initialize(hex_string)
10
- @red, @green, @blue =
11
- hex_string
12
- .gsub(/^#?/, '')
13
- .match(/(\w\w?)(\w\w?)(\w\w?)/)
14
- .captures
15
- .map do |s|
16
- 255 / (s.length == 2 ? s : s + s).to_i(16)
17
- rescue ::StandardError
18
- 0
19
- end
10
+ @red_hex, @green_hex, @blue_hex = hex_string
11
+ .gsub(/^#?/, '')
12
+ .match(/([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2})/i)
13
+ &.captures
14
+ &.map { |s| s.length == 1 ? s + s : s }
15
+ end
16
+
17
+ # The percent (decimal between 0-1) of red
18
+ def red_percent
19
+ hex_to_percent(@red_hex)
20
+ end
21
+
22
+ # The percent (decimal between 0-1) of green
23
+ def green_percent
24
+ hex_to_percent(@green_hex)
25
+ end
26
+
27
+ # The percent (decimal between 0-1) of blue
28
+ def blue_percent
29
+ hex_to_percent(@blue_hex)
30
+ end
31
+
32
+ # to_hex
33
+ def to_hex
34
+ [@red_hex, @green_hex, @blue_hex].join
35
+ end
36
+
37
+ # to_s
38
+ def to_s
39
+ "Color(r: #{@red_hex}, g: #{@green_hex}, b: #{@blue_hex})"
40
+ end
41
+
42
+ # ==
43
+ def ==(other)
44
+ other.is_a?(self.class) &&
45
+ other.red_hex == @red_hex &&
46
+ other.green_hex == @green_hex &&
47
+ other.blue_hex == @blue_hex
48
+ end
49
+
50
+ private
51
+
52
+ def hex_to_percent(hex)
53
+ hex.to_i(16) / 255
20
54
  end
21
55
  end
22
56
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ # An error thrown by our code (generally to be handled at the top level bin/ command)
5
+ class Error < StandardError
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CSVPlusPlus
4
+ # A convenience wrapper around Google's REST API client
5
+ module GoogleApiClient
6
+ # Get a +::Google::Apis::SheetsV4::SheetsService+ instance connected to the sheets API
7
+ def self.sheets_client
8
+ ::Google::Apis::SheetsV4::SheetsService.new.tap do |s|
9
+ s.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/spreadsheets'].freeze)
10
+ end
11
+ end
12
+
13
+ # Get a +::Google::Apis::DriveV3::DriveService+ instance connected to the drive API
14
+ def self.drive_client
15
+ ::Google::Apis::DriveV3::DriveService.new.tap do |d|
16
+ d.authorization = ::Google::Auth.get_application_default(['https://www.googleapis.com/auth/drive.file'].freeze)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -54,11 +54,6 @@ module CSVPlusPlus
54
54
  include ::TSort
55
55
  alias tsort_each_node each_key
56
56
 
57
- # create a +DependencyGraph+ from a +Hash+
58
- def self.from_hash(hash)
59
- self[hash.map { |k, v| [k, v] }]
60
- end
61
-
62
57
  # sort each child
63
58
  def tsort_each_child(node, &)
64
59
  fetch(node).each(&)
@@ -14,7 +14,6 @@ require_relative 'scope'
14
14
 
15
15
  module CSVPlusPlus
16
16
  module Language
17
- ##
18
17
  # Encapsulates the parsing and building of objects (+Template+ -> +Row+ -> +Cell+).
19
18
  # Variable resolution is delegated to the +Scope+
20
19
  # rubocop:disable Metrics/ClassLength
@@ -5,24 +5,24 @@ require_relative './entity'
5
5
  module CSVPlusPlus
6
6
  module Language
7
7
  module Entities
8
- ##
9
8
  # A boolean value
10
9
  class Boolean < Entity
11
10
  attr_reader :value
12
11
 
13
12
  # initialize
13
+ # @param value [String, Boolean]
14
14
  def initialize(value)
15
15
  super(:boolean)
16
16
  # TODO: probably can do a lot better in general on type validation
17
17
  @value = value.is_a?(::String) ? (value.downcase == 'true') : value
18
18
  end
19
19
 
20
- # to_s
20
+ # @return [String]
21
21
  def to_s
22
22
  @value.to_s.upcase
23
23
  end
24
24
 
25
- # ==
25
+ # @return [Boolean]
26
26
  def ==(other)
27
27
  super && value == other.value
28
28
  end
@@ -9,13 +9,14 @@ module CSVPlusPlus
9
9
  class Entity
10
10
  attr_reader :id, :type
11
11
 
12
- # initialize
12
+ # @param type [String, Symbol]
13
+ # @param id [String]
13
14
  def initialize(type, id: nil)
14
15
  @type = type.to_sym
15
16
  @id = id.downcase.to_sym if id
16
17
  end
17
18
 
18
- # ==
19
+ # @return [Boolean]
19
20
  def ==(other)
20
21
  self.class == other.class && @type == other.type && @id == other.id
21
22
  end
@@ -30,7 +31,8 @@ module CSVPlusPlus
30
31
  end
31
32
  end
32
33
 
33
- # support predicates by type
34
+ # Respond to predicates by type (entity.boolean?, entity.string?, etc)
35
+ # @return [Boolean]
34
36
  def respond_to_missing?(method_name, *_arguments)
35
37
  (method_name =~ /^(\w+)\?$/ && a_type?(::Regexp.last_match(1))) || super
36
38
  end
@@ -42,17 +44,19 @@ module CSVPlusPlus
42
44
  end
43
45
  end
44
46
 
45
- # An entity that can take arguments
47
+ # An entity that can take other entities as arguments
46
48
  class EntityWithArguments < Entity
47
49
  attr_reader :arguments
48
50
 
49
- # initialize
51
+ # @param type [String, Symbol]
52
+ # @param id [String]
53
+ # @param arguments [Array<Entity>]
50
54
  def initialize(type, id: nil, arguments: [])
51
55
  super(type, id:)
52
56
  @arguments = arguments
53
57
  end
54
58
 
55
- # ==
59
+ # @return [Boolean]
56
60
  def ==(other)
57
61
  super && @arguments == other.arguments
58
62
  end
@@ -127,8 +127,6 @@ module CSVPlusPlus
127
127
 
128
128
  # this will throw a syntax error if it doesn't exist (which is what we want)
129
129
  return ::BUILTIN_FUNCTIONS[id] if ::BUILTIN_FUNCTIONS.key?(id)
130
-
131
- @runtime.raise_syntax_error('Unknown function', fn_id)
132
130
  end
133
131
 
134
132
  def apply_arguments(function, function_call)
@@ -4,7 +4,7 @@ module CSVPlusPlus
4
4
  module Language
5
5
  ##
6
6
  # An error that can be thrown for various syntax errors
7
- class SyntaxError < StandardError
7
+ class SyntaxError < ::CSVPlusPlus::Error
8
8
  # initialize
9
9
  def initialize(message, bad_input, runtime, wrapped_error: nil)
10
10
  @bad_input = bad_input.to_s