csv_plus_plus 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +103 -0
- data/bin/csv++ +42 -0
- data/lib/csv_plus_plus/cli_flag.rb +83 -0
- data/lib/csv_plus_plus/color.rb +45 -11
- data/lib/csv_plus_plus/error.rb +7 -0
- data/lib/csv_plus_plus/graph.rb +0 -5
- data/lib/csv_plus_plus/language/compiler.rb +0 -1
- data/lib/csv_plus_plus/language/scope.rb +0 -2
- data/lib/csv_plus_plus/language/syntax_error.rb +1 -1
- data/lib/csv_plus_plus/modifier.rb +4 -15
- data/lib/csv_plus_plus/modifier.tab.rb +367 -381
- data/lib/csv_plus_plus/options.rb +1 -0
- data/lib/csv_plus_plus/version.rb +1 -1
- data/lib/csv_plus_plus/writer/excel.rb +15 -2
- data/lib/csv_plus_plus/writer/google_sheet_builder.rb +12 -31
- data/lib/csv_plus_plus/writer/google_sheet_modifier.rb +56 -0
- data/lib/csv_plus_plus/writer/google_sheets.rb +4 -4
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +112 -0
- data/lib/csv_plus_plus/writer/rubyxl_modifier.rb +52 -0
- data/lib/csv_plus_plus/writer.rb +2 -3
- data/lib/csv_plus_plus.rb +1 -0
- metadata +106 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea5b029c524b401348cc42399514c9fb689b13bfcf7298664429f69c85ae6607
|
4
|
+
data.tar.gz: 5bb79395742dcd89bf3b4ca7ede92a9a41a2c3ddefc4dff1b4232c285117373d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a1bf4ccf98486b64ed69f34e701e9c4c69aea2b971c569aab7f6345ca721ccbde01570449033be23eeea37ec59d244dbb2692f27bfbfaae6069c84e23ea988a
|
7
|
+
data.tar.gz: 8cef1aa204255588787b3e3138ec282a9b45c174b83e647a91d0476aca199ee4ee8640f11c40418cded6026016a4383e1e1df7600cdfa5c5524d25a6c953fb76
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-community-brightgreen.svg)](https://rubystyle.guide)
|
2
|
+
|
3
|
+
# csv++
|
4
|
+
|
5
|
+
A tool that allows you to programatically author spreadsheets in your favorite text editor and write their results to CSV, Google Sheets, Excel and other spreadsheet formats. This allows you to write a spreadsheet template, check it into git and push changes out to spreadsheets using typical dev tools.
|
6
|
+
|
7
|
+
## Template Language
|
8
|
+
|
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
|
+
|
11
|
+
```
|
12
|
+
fees := 0.50 # my broker charges $0.50 a trade
|
13
|
+
|
14
|
+
price := cellref(C)
|
15
|
+
quantity := cellref(D)
|
16
|
+
|
17
|
+
def profit() (price * quantity) - fees
|
18
|
+
|
19
|
+
---
|
20
|
+
![[format=bold/align=center]]Date,Ticker,Price,Quantity,Total,Fees
|
21
|
+
![[expand]],[[format=bold]],,,"=PROFIT()",$$fees
|
22
|
+
```
|
23
|
+
|
24
|
+
## Predefined Variables
|
25
|
+
|
26
|
+
* `$$rownum` - The current row number. The first row of the spreadsheet starts at 1
|
27
|
+
|
28
|
+
## Predefined Functions
|
29
|
+
|
30
|
+
* `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
|
+
|
32
|
+
## Modifiers
|
33
|
+
|
34
|
+
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):
|
35
|
+
|
36
|
+
```
|
37
|
+
foo,[[...]]bar,baz
|
38
|
+
```
|
39
|
+
|
40
|
+
specifying formatting or various other modifiers to the cell. Additionally a row can start with:
|
41
|
+
|
42
|
+
```
|
43
|
+
![[...]]foo,bar,baz
|
44
|
+
```
|
45
|
+
|
46
|
+
which will apply that modifier to all cells in the row.
|
47
|
+
|
48
|
+
### Examples
|
49
|
+
|
50
|
+
* Align the second cell left, align the last cell to the center and make it bold and italicized:
|
51
|
+
|
52
|
+
```
|
53
|
+
Date,[[align=left]]Amount,Quantity,[[align=center/format=bold italic]]Price
|
54
|
+
```
|
55
|
+
|
56
|
+
* Underline and center-align an entire row:
|
57
|
+
|
58
|
+
```
|
59
|
+
![[align=center/format=underline]]Date,Amount,Quantity,Price
|
60
|
+
```
|
61
|
+
|
62
|
+
* A header for the first row, then some formulas that repeat for each row for the rest of the spreadsheet:
|
63
|
+
|
64
|
+
```
|
65
|
+
![[align=center/format=bold]]Date,Price,Quantity,Profit
|
66
|
+
![[expand=1:]],,,"=MULTIPLY(cellref(B), cellref(C))"
|
67
|
+
```
|
68
|
+
|
69
|
+
## Setup (Google Sheets)
|
70
|
+
|
71
|
+
Just install it via rubygems (homebrew and debian packages are in the works):
|
72
|
+
|
73
|
+
`$ gem install csv_plus_plus`
|
74
|
+
|
75
|
+
### Publishing to Google Sheets
|
76
|
+
|
77
|
+
* 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`
|
78
|
+
* "Share" the spreadsheet with the email associated with the service account
|
79
|
+
|
80
|
+
## CLI Arguments
|
81
|
+
|
82
|
+
```
|
83
|
+
Usage: csv++ [options]
|
84
|
+
-b, --backup Create a backup of the spreadsheet before applying changes.
|
85
|
+
-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
|
86
|
+
-c, --create Create the sheet if it doesn't exist. It will use --sheet-name if specified
|
87
|
+
-k, --key-values KEY_VALUES A comma-separated list of key=values which will be made available to the template
|
88
|
+
-n, --sheet-name SHEET_NAME The name of the sheet to apply the template to
|
89
|
+
-v, --verbose Enable verbose output
|
90
|
+
-x, --offset-columns OFFSET Apply the template offset by OFFSET cells
|
91
|
+
-y, --offset-rows OFFSET Apply the template offset by OFFSET rows
|
92
|
+
-h, --help Show help information
|
93
|
+
```
|
94
|
+
|
95
|
+
## Usage Examples
|
96
|
+
|
97
|
+
```
|
98
|
+
# apply my_taxes_template.csvpp to an existing Google Sheet with name "Taxes 2022"
|
99
|
+
$ csv++ --sheet-name "Taxes 2022" --sheet-id "[...]" my_taxes_template.csvpp
|
100
|
+
|
101
|
+
# take input from stdin, supply a variable ($$rate = 1) and apply to the "Stocks" spreadsheet
|
102
|
+
$ cat stocks.csvpp | csv++ -k "rate=1" -n "Stocks" -i "[...]"
|
103
|
+
```
|
data/bin/csv++
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require_relative '../lib/csv_plus_plus'
|
6
|
+
|
7
|
+
options = ::CSVPlusPlus::Options.new
|
8
|
+
|
9
|
+
option_parser =
|
10
|
+
::OptionParser.new do |parser|
|
11
|
+
parser.on('-h', '--help', 'Show help information') do
|
12
|
+
puts(parser)
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
::SUPPORTED_CSVPP_FLAGS.each do |flag|
|
17
|
+
parser.on(flag.short_flag, flag.long_flag, flag.description) do |v|
|
18
|
+
flag.handler.call(options, v)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
option_parser.parse!
|
24
|
+
|
25
|
+
error_message = options.validate
|
26
|
+
unless error_message.nil?
|
27
|
+
warn(error_message)
|
28
|
+
puts(option_parser)
|
29
|
+
exit(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
::CSVPlusPlus.apply_template_to_sheet!(::ARGF.read, ::ARGF.filename, options)
|
34
|
+
rescue ::CSVPlusPlus::Error => e
|
35
|
+
if e.is_a?(::CSVPlusPlus::Language::SyntaxError)
|
36
|
+
warn(options.verbose ? e.to_verbose_trace : e.to_trace)
|
37
|
+
else
|
38
|
+
warn(e.message)
|
39
|
+
end
|
40
|
+
|
41
|
+
exit(1)
|
42
|
+
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
|
data/lib/csv_plus_plus/color.rb
CHANGED
@@ -3,20 +3,54 @@
|
|
3
3
|
module CSVPlusPlus
|
4
4
|
# A color value
|
5
5
|
class Color
|
6
|
-
attr_reader :
|
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
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/csv_plus_plus/graph.rb
CHANGED
@@ -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(&)
|
@@ -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 <
|
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
|
@@ -6,32 +6,20 @@ require_relative './expand'
|
|
6
6
|
require_relative './language/syntax_error'
|
7
7
|
|
8
8
|
module CSVPlusPlus
|
9
|
-
##
|
10
9
|
# A container representing the operations that can be applied to a cell or row
|
11
10
|
class Modifier
|
12
11
|
attr_reader :bordercolor, :borders, :color, :fontcolor, :formats
|
13
12
|
attr_writer :borderstyle
|
14
|
-
attr_accessor :expand, :fontfamily, :fontsize, :note, :numberformat, :row_level, :validation
|
13
|
+
attr_accessor :expand, :fontfamily, :fontsize, :halign, :valign, :note, :numberformat, :row_level, :validation
|
15
14
|
|
16
15
|
# initialize
|
17
16
|
def initialize(row_level: false)
|
18
17
|
@row_level = row_level
|
19
18
|
@freeze = false
|
20
|
-
@align = ::Set.new
|
21
19
|
@borders = ::Set.new
|
22
20
|
@formats = ::Set.new
|
23
21
|
end
|
24
22
|
|
25
|
-
# Set an align format. +direction+ must be 'center', 'left', 'right', 'bottom'
|
26
|
-
def align=(direction)
|
27
|
-
@align << direction
|
28
|
-
end
|
29
|
-
|
30
|
-
# Is it aligned to a given direction?
|
31
|
-
def aligned?(direction)
|
32
|
-
@align.include?(direction)
|
33
|
-
end
|
34
|
-
|
35
23
|
# Set the color. hex_value is a String
|
36
24
|
def color=(hex_value)
|
37
25
|
@color = ::CSVPlusPlus::Color.new(hex_value)
|
@@ -110,12 +98,13 @@ module CSVPlusPlus
|
|
110
98
|
# to_s
|
111
99
|
def to_s
|
112
100
|
# TODO... I dunno, not sure how to manage this
|
113
|
-
"Modifier(row_level: #{@row_level}
|
101
|
+
"Modifier(row_level: #{@row_level} halign: #{@halign} valign: #{@valign} format: #{@formats} " \
|
102
|
+
"font_size: #{@font_size})"
|
114
103
|
end
|
115
104
|
|
116
105
|
# Create a new modifier instance, with all values defaulted from +other+
|
117
106
|
def take_defaults_from!(other)
|
118
|
-
instance_variables.each do |property|
|
107
|
+
other.instance_variables.each do |property|
|
119
108
|
value = other.instance_variable_get(property)
|
120
109
|
instance_variable_set(property, value.clone)
|
121
110
|
end
|