csv_plus_plus 0.0.3 → 0.0.5

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 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