csv-diff-report 0.2
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 +15 -0
- data/LICENSE +22 -0
- data/README.md +264 -0
- data/bin/csvdiff +8 -0
- data/lib/csv-diff-report.rb +6 -0
- data/lib/csv-diff-report/cli.rb +122 -0
- data/lib/csv-diff-report/excel.rb +154 -0
- data/lib/csv-diff-report/html.rb +170 -0
- data/lib/csv-diff-report/report.rb +226 -0
- data/lib/csv_diff_report.rb +2 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZTAxMGE1NTA2MjRhMjkzOGUyNThmMGQ1MjJmMGUwY2FjOWUzNzg2OQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NDdhNTE0M2M5MWFmNDUyYTE4NTgyMWIzMWY0NWNkOTYwMGFhM2FmNA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YjU2ZGZjMTJlOTUzYzE1YTE5MmZiOTZkYTRhZjBhOWVmODRiMDlhMDI5ZDZh
|
10
|
+
ZTUzYjUxYzNlOGYzMDZiYTgzNjU1YTE2ZDkzZTk4Mjk1ZTQxMmI2MzAyYzM1
|
11
|
+
OGFmOWEzMTVjZDM0NThlYmExMWJlZjQwZmZkNWVkYWU1Nzk3ZTQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
N2I3NjgzYjQ0YTc5OTJkOTY2YWM3MTQyMDdiZjJiODdmNTQ5MDQ2MjBjZTYx
|
14
|
+
MjRhMDBiMTY1NDE0ZjI1YTE2NTUwZWFjZDcyMDFiMDg1MzNkMmU3MjVkNDBi
|
15
|
+
MDZkMWY2Yzk4YmVlYjZlMWNiZDQyYzIyMTg5MjA3MzJlODYxZmQ=
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013, Adam Gardiner
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
14
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
15
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
17
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
18
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
19
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
20
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
21
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
22
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
# CSV-Diff Report
|
2
|
+
|
3
|
+
CSV-Diff Report is a command-line tool for generating diff reports in Excel or
|
4
|
+
HTML format from CSV files. It uses the CSV-Diff gem to perform diffs, and adds
|
5
|
+
to that library the ability to generate formatted reports, and a command-line
|
6
|
+
tool `csvdiff` for running diffs between files or directories.
|
7
|
+
|
8
|
+
|
9
|
+
## CSV-Diff
|
10
|
+
|
11
|
+
Unlike a standard diff that compares line by line, and is sensitive to the
|
12
|
+
ordering of records, CSV-Diff identifies common lines by key field(s), and
|
13
|
+
then compares the contents of the fields in each line.
|
14
|
+
|
15
|
+
CSV-Diff is particularly well suited to data in parent-child format. Parent-
|
16
|
+
child data does not lend itself well to standard text diffs, as small changes
|
17
|
+
in the organisation of the tree at an upper level can lead to big movements
|
18
|
+
in the position of descendant records. By instead matching records by key,
|
19
|
+
CSV-Diff avoids this issue, while still being able to detect changes in
|
20
|
+
sibling order.
|
21
|
+
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
CSV-Diff Report is supplied as a gem, and has dependencies on a few small libraries.
|
26
|
+
To install it, simply:
|
27
|
+
```
|
28
|
+
gem install csv-diff-report
|
29
|
+
```
|
30
|
+
|
31
|
+
To compare two CSV files where the field names are in the first row of the file,
|
32
|
+
and the first field contains the unique key for each record, simply use:
|
33
|
+
```
|
34
|
+
csvdiff <file1> <file2>
|
35
|
+
```
|
36
|
+
|
37
|
+
The `csvdiff` command-line tool provides many options to control behaviour of
|
38
|
+
the diff and reporting process. To see all available options, run:
|
39
|
+
```
|
40
|
+
csv-diff --help
|
41
|
+
```
|
42
|
+
|
43
|
+
This will display a help screen like the following:
|
44
|
+
```
|
45
|
+
CSV-Diff
|
46
|
+
========
|
47
|
+
|
48
|
+
Generate a diff report between two files using the CSV-Diff algorithm.
|
49
|
+
|
50
|
+
|
51
|
+
USAGE
|
52
|
+
-----
|
53
|
+
ruby /usr/local/opt/ruby/bin/csvdiff FROM TO [OPTIONS]
|
54
|
+
|
55
|
+
FROM The file or dir to use as the left or from source in the diff
|
56
|
+
TO The file or dir to use as the right or to source in the diff
|
57
|
+
|
58
|
+
|
59
|
+
OPTIONS
|
60
|
+
-------
|
61
|
+
|
62
|
+
Source Options
|
63
|
+
--pattern PATTERN A file name pattern to use to filter matching files if a directory diff is
|
64
|
+
being performed
|
65
|
+
[Default: *]
|
66
|
+
--field-names FIELD-NAMES A comma-separated list of field names for each field in the source files
|
67
|
+
--parent-fields PARENT-FIELDS The parent field name(s) or index(es)
|
68
|
+
--child-fields CHILD-FIELDS The child field name(s) or index(es)
|
69
|
+
--key-fields KEY-FIELDS The key field name(s) or index(es)
|
70
|
+
--encoding ENCODING The encoding to use when opening the CSV files
|
71
|
+
--ignore-header If true, the first line in each source file is ignored; requires the use of
|
72
|
+
the --field-names option to name the fields
|
73
|
+
|
74
|
+
Diff Options
|
75
|
+
--ignore-fields IGNORE-FIELDS The names or indexes of any fields to be ignored during the diff
|
76
|
+
--ignore-adds If true, items in TO that are not in FROM are ignored
|
77
|
+
--ignore-deletes If true, items in FROM that are not in TO are ignored
|
78
|
+
--ignore-updates If true, changes to non-key properties are ignored
|
79
|
+
--ignore-moves If true, changes in an item's position are ignored
|
80
|
+
|
81
|
+
Output Options
|
82
|
+
--format FORMAT The format in which to produce the diff report
|
83
|
+
[Default: HTML]
|
84
|
+
--output OUTPUT The path to save the diff report to. If not specified, the diff report will
|
85
|
+
be placed in the same directory as the FROM file, and will be named
|
86
|
+
Diff_<FROM>_to_<TO>.<FORMAT>
|
87
|
+
|
88
|
+
```
|
89
|
+
|
90
|
+
## Unique Row Identifiers
|
91
|
+
|
92
|
+
CSVDiff is preferable over a standard line-by-line diff when row order is
|
93
|
+
significantly impacted by small changes. The classic example is a parent-child
|
94
|
+
file generated by a hierarchy traversal. A simple change in position of a parent
|
95
|
+
member near the root of the hierarchy will have a large impact on the positions
|
96
|
+
of all descendant rows. Consider the following example:
|
97
|
+
```
|
98
|
+
Root
|
99
|
+
|- A
|
100
|
+
| |- A1
|
101
|
+
| |- A2
|
102
|
+
|
|
103
|
+
|- B
|
104
|
+
|- B1
|
105
|
+
|- B2
|
106
|
+
```
|
107
|
+
|
108
|
+
A hierarchy traversal of this tree into a parent-child format would generate a CSV
|
109
|
+
as follows:
|
110
|
+
```
|
111
|
+
Root,A
|
112
|
+
A,A1
|
113
|
+
A,A2
|
114
|
+
Root,B
|
115
|
+
B,B1
|
116
|
+
B,B2
|
117
|
+
```
|
118
|
+
|
119
|
+
If the positions of A and B were swapped, a hierarchy traversal would now produce a CSV
|
120
|
+
as follows:
|
121
|
+
```
|
122
|
+
Root,B
|
123
|
+
B,B1
|
124
|
+
B,B2
|
125
|
+
Root,A
|
126
|
+
A,A1
|
127
|
+
A,A2
|
128
|
+
```
|
129
|
+
|
130
|
+
A simple diff using a diff utility would highlight this as 3 additions and 3 deletions.
|
131
|
+
CSVDiff, however, would classify this as 2 moves (a change in sibling position for A and B).
|
132
|
+
|
133
|
+
In order to do this, CSVDiff needs to know what field(s) confer uniqueness on each row.
|
134
|
+
In this example, we could use the child field alone (since each member name only appears
|
135
|
+
once); however, this would imply a flat structure, where all rows are children of a single
|
136
|
+
parent. This in turn would cause CSVDiff to classify the above change as a Move (i.e. a
|
137
|
+
change in order) of all 6 rows.
|
138
|
+
|
139
|
+
The more correct specification of this file is that column 0 contains a unique parent
|
140
|
+
identifier, and column 1 contains a unique child identifier. CSVDiff can then correctly
|
141
|
+
deduce that there is in fact only two changes in order - the swap in positions of A and
|
142
|
+
B below Root.
|
143
|
+
|
144
|
+
Note: If you aren't interested in changes in the order of siblings, then you could use
|
145
|
+
CSVDiff with a :key_field option of column 1, and specify the :ignore_moves option.
|
146
|
+
|
147
|
+
## Warnings
|
148
|
+
|
149
|
+
When processing and diffing files, CSVDiff may encounter problems with the data or
|
150
|
+
the specifications it has been given. It will continue even in the face of problems,
|
151
|
+
but will log details of the problems in a #warnings Array. The number of warnings
|
152
|
+
will also be included in the Hash returned by the #summary method.
|
153
|
+
|
154
|
+
Warnings may be raised for any of the following:
|
155
|
+
* Missing fields: If the right/to file contains fields that are not present in the
|
156
|
+
left/from file, a warning is raised and the field is ignored for diff purposes.
|
157
|
+
* Duplicate keys: If two rows are found that have the same values for the key field(s),
|
158
|
+
a warning is raised, and the duplicate values are ignored.
|
159
|
+
|
160
|
+
|
161
|
+
## Examples
|
162
|
+
|
163
|
+
The simplest use case is as shown above, where the data to be diffed is in CSV files
|
164
|
+
with the column names as the first record, and where the unique key is the first
|
165
|
+
column in the data. In this case, a diff can be created simply via:
|
166
|
+
```ruby
|
167
|
+
diff = CSVDiff.new(file1, file2)
|
168
|
+
```
|
169
|
+
|
170
|
+
### Specifynig Unique Row Identifiers
|
171
|
+
|
172
|
+
Often however, rows are not uniquely identifiable via the first column in the file.
|
173
|
+
In a parent-child hierarchy, for example, combinations of parent and child may be
|
174
|
+
necessary to uniquely identify a row. In these cases, it is necessary to indicate
|
175
|
+
which fields are used to uniquely identify common rows across the two files. This
|
176
|
+
can be done in several different ways.
|
177
|
+
|
178
|
+
1. Using the :key_fields option with field numbers (these are 0-based):
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
diff = CSVDiff.new(file1, file2, key_fields: [0, 1])
|
182
|
+
```
|
183
|
+
|
184
|
+
2. Using the :key_fields options with column names:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
diff = CSVDiff.new(file1, file2, key_fields: ['Parent', 'Child'])
|
188
|
+
```
|
189
|
+
|
190
|
+
3. Using the :parent_fields and :child_fields with field numbers:
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
diff = CSVDiff.new(file1, file2, parent_field: 1, child_fields: [2, 3])
|
194
|
+
```
|
195
|
+
|
196
|
+
4. Using the :parent_fields and :child_fields with column names:
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
diff = CSVDiff.new(file1, file2, parent_field: 'Date', child_fields: ['HomeTeam', 'AwayTeam'])
|
200
|
+
```
|
201
|
+
|
202
|
+
### Using Non-CSV File Sources
|
203
|
+
|
204
|
+
Data from non-CSV sources can be diffed, as long as it can be supplied as an Array
|
205
|
+
of Arrays:
|
206
|
+
```ruby
|
207
|
+
DATA1 = [
|
208
|
+
['Parent', 'Child', 'Description'],
|
209
|
+
['A', 'A1', 'Account 1'],
|
210
|
+
['A', 'A2', 'Account 2']
|
211
|
+
]
|
212
|
+
|
213
|
+
DATA2 = [
|
214
|
+
['Parent', 'Child', 'Description'],
|
215
|
+
['A', 'A1', 'Account1'],
|
216
|
+
['A', 'A2', 'Account2']
|
217
|
+
]
|
218
|
+
|
219
|
+
diff = CSVDiff.new(DATA1, DATA2, key_fields: [1, 0])
|
220
|
+
```
|
221
|
+
|
222
|
+
### Specifying Column Names
|
223
|
+
|
224
|
+
If your data file does not include column headers, you can specify the names of
|
225
|
+
each column when creating the diff. The names supplied are the keys used in the
|
226
|
+
diff results:
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
DATA1 = [
|
230
|
+
['A', 'A1', 'Account 1'],
|
231
|
+
['A', 'A2', 'Account 2']
|
232
|
+
]
|
233
|
+
|
234
|
+
DATA2 = [
|
235
|
+
['A', 'A1', 'Account1'],
|
236
|
+
['A', 'A2', 'Account2']
|
237
|
+
]
|
238
|
+
|
239
|
+
diff = CSVDiff.new(DATA1, DATA2, key_fields: [1, 0], field_names: ['Parent', 'Child', 'Description'])
|
240
|
+
```
|
241
|
+
|
242
|
+
If your data file does contain a header row, but you wish to use your own column
|
243
|
+
names, you can specify the :field_names option and the :ignore_header option to
|
244
|
+
ignore the first row.
|
245
|
+
|
246
|
+
|
247
|
+
### Ignoring Fields
|
248
|
+
|
249
|
+
If your data contains fields that you aren't interested in, these can be excluded
|
250
|
+
from the diff process using the :ignore_fields option:
|
251
|
+
```ruby
|
252
|
+
diff = CSVDiff.new(file1, file2, parent_field: 'Date', child_fields: ['HomeTeam', 'AwayTeam'],
|
253
|
+
ignore_fields: ['CreatedAt', 'UpdatedAt'])
|
254
|
+
```
|
255
|
+
|
256
|
+
### Ignoring Certain Changes
|
257
|
+
|
258
|
+
CSVDiff identifies Adds, Updates, Moves and Deletes; any of these changes can be selectively
|
259
|
+
ignored, e.g. if you are not interested in Deletes, you can pass the :ignore_deletes option:
|
260
|
+
```ruby
|
261
|
+
diff = CSVDiff.new(file1, file2, parent_field: 'Date', child_fields: ['HomeTeam', 'AwayTeam'],
|
262
|
+
ignore_fields: ['CreatedAt', 'UpdatedAt'],
|
263
|
+
ignore_deletes: true, ignore_moves: true)
|
264
|
+
```
|
data/bin/csvdiff
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'arg-parser'
|
2
|
+
require 'csv-diff-report'
|
3
|
+
|
4
|
+
|
5
|
+
class CSVDiff
|
6
|
+
|
7
|
+
class CLI
|
8
|
+
|
9
|
+
include ArgParser::DSL
|
10
|
+
|
11
|
+
# Define an on_parse handler for field names or indexes. Splits the
|
12
|
+
# supplied argument value on commas, and converts numbers to Fixnums.
|
13
|
+
register_parse_handler(:parse_fields) do |val, arg, hsh|
|
14
|
+
val.split(',').map{ |fld| fld =~ /^\d+$/ ? fld.to_i : fld }
|
15
|
+
end
|
16
|
+
|
17
|
+
title 'CSV-Diff'
|
18
|
+
|
19
|
+
purpose <<-EOT
|
20
|
+
Generate a diff report between two files using the CSV-Diff algorithm.
|
21
|
+
EOT
|
22
|
+
|
23
|
+
positional_arg :from, 'The file or dir to use as the left or from source in the diff'
|
24
|
+
positional_arg :to, 'The file or dir to use as the right or to source in the diff'
|
25
|
+
positional_arg :pattern, 'A file name pattern to use to filter matching files if a directory ' +
|
26
|
+
'diff is being performed', default: '*'
|
27
|
+
|
28
|
+
usage_break 'Source Options'
|
29
|
+
keyword_arg :file_types, 'A comma-separated list of file-type names (supports wildcards) to process. ' +
|
30
|
+
'Requires the presence of a .csvdiff file in the FROM or current directory to define ' +
|
31
|
+
'the file type patterns',
|
32
|
+
short_key: 't', on_parse: :split_to_array
|
33
|
+
keyword_arg :exclude, 'A file name pattern of files to exclude from the diff if a directory ' +
|
34
|
+
'diff is being performed',
|
35
|
+
short_key: 'x'
|
36
|
+
keyword_arg :field_names, 'A comma-separated list of field names for each ' +
|
37
|
+
'field in the source files',
|
38
|
+
short_key: 'f', on_parse: :split_to_array
|
39
|
+
keyword_arg :parent_fields, 'The parent field name(s) or index(es)',
|
40
|
+
short_key: 'p', on_parse: :parse_fields
|
41
|
+
keyword_arg :child_fields, 'The child field name(s) or index(es)',
|
42
|
+
short_key: 'c', on_parse: :parse_fields
|
43
|
+
keyword_arg :key_fields, 'The key field name(s) or index(es)',
|
44
|
+
short_key: 'k', on_parse: :parse_fields
|
45
|
+
keyword_arg :encoding, 'The encoding to use when opening the CSV files',
|
46
|
+
short_key: 'e'
|
47
|
+
flag_arg :tab_delimited, 'If true, the file is assumed to be tab-delimited rather than comma-delimited'
|
48
|
+
flag_arg :ignore_header, 'If true, the first line in each source file is ignored; ' +
|
49
|
+
'requires the use of the --field-names option to name the fields'
|
50
|
+
|
51
|
+
usage_break 'Diff Options'
|
52
|
+
keyword_arg :ignore_fields, 'The names or indexes of any fields to be ignored during the diff',
|
53
|
+
short_key: 'i', on_parse: :parse_fields
|
54
|
+
flag_arg :ignore_adds, "If true, items in TO that are not in FROM are ignored",
|
55
|
+
short_key: 'A'
|
56
|
+
flag_arg :ignore_deletes, "If true, items in FROM that are not in TO are ignored",
|
57
|
+
short_key: 'D'
|
58
|
+
flag_arg :ignore_updates, "If true, changes to non-key properties are ignored",
|
59
|
+
short_key: 'U'
|
60
|
+
flag_arg :ignore_moves, "If true, changes in an item's position are ignored",
|
61
|
+
short_key: 'M'
|
62
|
+
|
63
|
+
usage_break 'Output Options'
|
64
|
+
keyword_arg :format, 'The format in which to produce the diff report',
|
65
|
+
default: 'HTML', validation: /^(html|xls(x)?)$/i
|
66
|
+
keyword_arg :output, 'The path to save the diff report to. If not specified, the diff ' +
|
67
|
+
'report will be placed in the same directory as the FROM file, and will be named ' +
|
68
|
+
'Diff_<FROM>_to_<TO>.<FORMAT>'
|
69
|
+
|
70
|
+
|
71
|
+
# Parses command-line options, and then performs the diff.
|
72
|
+
def run
|
73
|
+
if arguments = parse_arguments
|
74
|
+
begin
|
75
|
+
process(arguments)
|
76
|
+
rescue RuntimeError => ex
|
77
|
+
Console.puts ex.message, :red
|
78
|
+
exit 1
|
79
|
+
end
|
80
|
+
else
|
81
|
+
if show_help?
|
82
|
+
show_help(nil, Console.width).each do |line|
|
83
|
+
Console.puts line, :cyan
|
84
|
+
end
|
85
|
+
else
|
86
|
+
show_usage(nil, Console.width).each do |line|
|
87
|
+
Console.puts line, :yellow
|
88
|
+
end
|
89
|
+
end
|
90
|
+
exit 2
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Process a CSVDiffReport using +arguments+ to determine all options.
|
96
|
+
def process(arguments)
|
97
|
+
options = {}
|
98
|
+
exclude_args = [:from, :to, :tab_delimited]
|
99
|
+
arguments.each_pair do |arg, val|
|
100
|
+
options[arg] = val if val && !exclude_args.include?(arg)
|
101
|
+
end
|
102
|
+
options[:csv_options] = {:col_sep => "\t"} if arguments.tab_delimited
|
103
|
+
rep = CSVDiff::Report.new
|
104
|
+
rep.diff(arguments.from, arguments.to, options)
|
105
|
+
|
106
|
+
output_dir = FileTest.directory?(arguments.from) ?
|
107
|
+
arguments.from : File.dirname(arguments.from)
|
108
|
+
left_name = File.basename(arguments.from, File.extname(arguments.from))
|
109
|
+
right_name = File.basename(arguments.to, File.extname(arguments.to))
|
110
|
+
output = arguments.output ||
|
111
|
+
"#{output_dir}/Diff_#{left_name}_to_#{right_name}.diff"
|
112
|
+
rep.output(output, arguments.format)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
if __FILE__ == $0
|
121
|
+
CSVDiff::CLI.new.run
|
122
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
|
2
|
+
class CSVDiff
|
3
|
+
|
4
|
+
# Defines functionality for exporting a Diff report to Excel in XLSX format
|
5
|
+
# using the Axlsx library.
|
6
|
+
module Excel
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Generare a diff report in XLSX format.
|
11
|
+
def xl_output(output)
|
12
|
+
require 'axlsx'
|
13
|
+
|
14
|
+
# Create workbook
|
15
|
+
xl = xl_new
|
16
|
+
|
17
|
+
# Add a summary sheet and diff sheets for each diff
|
18
|
+
xl_summary_sheet(xl)
|
19
|
+
|
20
|
+
# Save workbook
|
21
|
+
path = "#{File.dirname(output)}/#{File.basename(output, File.extname(output))}.xlsx"
|
22
|
+
xl_save(xl, path)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Create a new XL package object
|
27
|
+
def xl_new
|
28
|
+
@xl_styles = {}
|
29
|
+
xl = Axlsx::Package.new
|
30
|
+
xl.use_shared_strings = true
|
31
|
+
xl.workbook.styles do |s|
|
32
|
+
s.fonts[0].sz = 9
|
33
|
+
@xl_styles['Title'] = s.add_style(:b => true)
|
34
|
+
@xl_styles['Comma'] = s.add_style(:format_code => '#,##0')
|
35
|
+
@xl_styles['Right'] = s.add_style(:alignment => {:horizontal => :right})
|
36
|
+
@xl_styles['Add'] = s.add_style :fg_color => '00A000'
|
37
|
+
@xl_styles['Update'] = s.add_style :fg_color => '0000A0', :bg_color => 'F0F0FF'
|
38
|
+
@xl_styles['Move'] = s.add_style :fg_color => '4040FF'
|
39
|
+
@xl_styles['Delete'] = s.add_style :fg_color => 'FF0000', :strike => true
|
40
|
+
end
|
41
|
+
xl
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Add summary sheet
|
46
|
+
def xl_summary_sheet(xl)
|
47
|
+
compare_from = @left
|
48
|
+
compare_to = @right
|
49
|
+
|
50
|
+
xl.workbook.add_worksheet(name: 'Summary') do |sheet|
|
51
|
+
sheet.add_row do |row|
|
52
|
+
row.add_cell 'From:', :style => @xl_styles['Title']
|
53
|
+
row.add_cell compare_from
|
54
|
+
end
|
55
|
+
sheet.add_row do |row|
|
56
|
+
row.add_cell 'To:', :style => @xl_styles['Title']
|
57
|
+
row.add_cell compare_to
|
58
|
+
end
|
59
|
+
sheet.add_row
|
60
|
+
sheet.add_row ['Sheet', 'Adds', 'Deletes', 'Updates', 'Moves'], :style => @xl_styles['Title']
|
61
|
+
sheet.column_info.each do |ci|
|
62
|
+
ci.width = 10
|
63
|
+
end
|
64
|
+
sheet.column_info.first.width = 20
|
65
|
+
|
66
|
+
@diffs.each do |file_diff|
|
67
|
+
sheet.add_row([File.basename(file_diff.left.path, File.extname(file_diff.left.path)),
|
68
|
+
file_diff.summary['Add'], file_diff.summary['Delete'],
|
69
|
+
file_diff.summary['Update'], file_diff.summary['Move']])
|
70
|
+
xl_diff_sheet(xl, file_diff) if file_diff.diffs.size > 0
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Add diff sheet
|
78
|
+
def xl_diff_sheet(xl, file_diff)
|
79
|
+
sheet_name = File.basename(file_diff.left.path, File.extname(file_diff.left.path))
|
80
|
+
all_fields = [:row, :action, :sibling_position] + file_diff.diff_fields
|
81
|
+
xl.workbook.add_worksheet(name: sheet_name) do |sheet|
|
82
|
+
sheet.add_row(all_fields.map{ |f| f.to_s }, :style => @xl_styles['Title'])
|
83
|
+
file_diff.diffs.sort_by{|k, v| v[:row] }.each do |key, diff|
|
84
|
+
sheet.add_row do |row|
|
85
|
+
chg = diff[:action]
|
86
|
+
all_fields.each_with_index do |field, i|
|
87
|
+
cell = nil
|
88
|
+
comment = nil
|
89
|
+
old = nil
|
90
|
+
style = case chg
|
91
|
+
when 'Add', 'Delete' then @xl_styles[chg]
|
92
|
+
else 0
|
93
|
+
end
|
94
|
+
d = diff[field]
|
95
|
+
if d.is_a?(Array)
|
96
|
+
old = d.first
|
97
|
+
new = d.last
|
98
|
+
if old.nil?
|
99
|
+
style = @xl_styles['Add']
|
100
|
+
else
|
101
|
+
style = @xl_styles[chg]
|
102
|
+
comment = old
|
103
|
+
end
|
104
|
+
else
|
105
|
+
new = d
|
106
|
+
style = @xl_styles[chg] if i == 1
|
107
|
+
end
|
108
|
+
case new
|
109
|
+
when String
|
110
|
+
cell = row.add_cell(new.encode('utf-8'), :style => style) #, :type => :string)
|
111
|
+
# cell = row.add_cell(new, :style => style)
|
112
|
+
else
|
113
|
+
cell = row.add_cell(new, :style => style)
|
114
|
+
end
|
115
|
+
sheet.add_comment(:ref => cell.r, :author => 'Current', :visible => false,
|
116
|
+
:text => old.to_s.encode('utf-8')) if comment
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
sheet.column_info.each do |ci|
|
121
|
+
ci.width = 80 if ci.width > 80
|
122
|
+
end
|
123
|
+
xl_filter_and_freeze(sheet, 5)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# Freeze the top row and +freeze_cols+ of +sheet+.
|
129
|
+
def xl_filter_and_freeze(sheet, freeze_cols = 0)
|
130
|
+
sheet.auto_filter = "A1:#{Axlsx::cell_r(sheet.rows.first.cells.size - 1, sheet.rows.size - 1)}"
|
131
|
+
sheet.sheet_view do |sv|
|
132
|
+
sv.pane do |p|
|
133
|
+
p.state = :frozen
|
134
|
+
p.x_split = freeze_cols
|
135
|
+
p.y_split = 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
# Save +xl+ package to +path+
|
142
|
+
def xl_save(xl, path)
|
143
|
+
begin
|
144
|
+
xl.serialize(path)
|
145
|
+
path
|
146
|
+
rescue RuntimeError => ex
|
147
|
+
Console.puts ex.message, :red
|
148
|
+
raise "Unable to replace existing Excel file #{path} - is it already open in Excel?"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
|
2
|
+
class CSVDiff
|
3
|
+
|
4
|
+
# Defines functionality for exporting a Diff report in HTML format.
|
5
|
+
module Html
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Generare a diff report in XLSX format.
|
10
|
+
def html_output(output)
|
11
|
+
content = []
|
12
|
+
content << '<html>'
|
13
|
+
content << '<head>'
|
14
|
+
content << '<title>Diff Report</title>'
|
15
|
+
content << '<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">'
|
16
|
+
content << html_styles
|
17
|
+
content << '</head>'
|
18
|
+
content << '<body>'
|
19
|
+
|
20
|
+
html_summary(content)
|
21
|
+
@diffs.each do |file_diff|
|
22
|
+
html_diff(content, file_diff) if file_diff.diffs.size > 0
|
23
|
+
end
|
24
|
+
|
25
|
+
content << '</body>'
|
26
|
+
content << '</html>'
|
27
|
+
|
28
|
+
# Save workbook
|
29
|
+
path = "#{File.dirname(output)}/#{File.basename(output, File.extname(output))}.html"
|
30
|
+
File.open(path, 'w'){ |f| f.write(content.join("\n")) }
|
31
|
+
path
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Returns the HTML head content, which contains the styles used for diffing.
|
36
|
+
def html_styles
|
37
|
+
style = <<-EOT
|
38
|
+
<style>
|
39
|
+
@font-face {font-family: Calibri;}
|
40
|
+
|
41
|
+
h1 {font-family: Calibri; font-size: 16pt;}
|
42
|
+
h2 {font-family: Calibri; font-size: 14pt; margin: 1em 0em .2em;}
|
43
|
+
h3 {font-family: Calibri; font-size: 12pt; margin: 1em 0em .2em;}
|
44
|
+
body {font-family: Calibri; font-size: 11pt;}
|
45
|
+
p {margin: .2em 0em;}
|
46
|
+
table {font-family: Calibri; font-size: 10pt; line-height: 12pt; border-collapse: collapse;}
|
47
|
+
th {background-color: #00205B; color: white; font-size: 11pt; font-weight: bold; text-align: left;
|
48
|
+
border: 1px solid #DDDDFF; padding: 1px 5px;}
|
49
|
+
td {border: 1px solid #DDDDFF; padding: 1px 5px;}
|
50
|
+
|
51
|
+
.summary {font-size: 13pt;}
|
52
|
+
.add {background-color: white; color: #33A000;}
|
53
|
+
.delete {background-color: white; color: #FF0000; text-decoration: line-through;}
|
54
|
+
.update {background-color: white; color: #0000A0;}
|
55
|
+
.move {background-color: white; color: #0000A0;}
|
56
|
+
.bold {font-weight: bold;}
|
57
|
+
.center {text-align: center;}
|
58
|
+
.right {text-align: right;}
|
59
|
+
.separator {width: 200px; border-bottom: 1px gray solid;}
|
60
|
+
</style>
|
61
|
+
EOT
|
62
|
+
style
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def html_summary(body)
|
67
|
+
body << '<h2>Summary</h2>'
|
68
|
+
|
69
|
+
body << '<p>Source Locations:</p>'
|
70
|
+
body << '<table>'
|
71
|
+
body << '<tbody>'
|
72
|
+
body << "<tr><th>From:</th><td>#{@left}</td></tr>"
|
73
|
+
body << "<tr><th>To:</th><td>#{@right}</td></tr>"
|
74
|
+
body << '</tbody>'
|
75
|
+
body << '</table>'
|
76
|
+
body << '<br>'
|
77
|
+
body << '<p>Differences:</p>'
|
78
|
+
body << '<table>'
|
79
|
+
body << '<thead><tr>'
|
80
|
+
body << '<th>File</th><th>Adds</th><th>Deletes</th><th>Updates</th><th>Moves</th>'
|
81
|
+
body << '</tr></thead>'
|
82
|
+
body << '<tbody>'
|
83
|
+
@diffs.each do |file_diff|
|
84
|
+
label = File.basename(file_diff.left.path)
|
85
|
+
body << '<tr>'
|
86
|
+
if file_diff.diffs.size > 0
|
87
|
+
body << "<td><a href='##{label}'>#{label}</a></td>"
|
88
|
+
else
|
89
|
+
body << "<td>#{label}</td>"
|
90
|
+
end
|
91
|
+
body << "<td class='right'>#{file_diff.summary['Add']}</td>"
|
92
|
+
body << "<td class='right'>#{file_diff.summary['Delete']}</td>"
|
93
|
+
body << "<td class='right'>#{file_diff.summary['Update']}</td>"
|
94
|
+
body << "<td class='right'>#{file_diff.summary['Move']}</td>"
|
95
|
+
body << '</tr>'
|
96
|
+
end
|
97
|
+
body << '</tbody>'
|
98
|
+
body << '</table>'
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def html_diff(body, file_diff)
|
103
|
+
label = File.basename(file_diff.left.path)
|
104
|
+
body << "<h2 id=#{label}>#{label}</h2>"
|
105
|
+
body << '<p>'
|
106
|
+
count = 0
|
107
|
+
if file_diff.summary['Add'] > 0
|
108
|
+
body << "<span class='add'>#{file_diff.summary['Add']} Adds</span>"
|
109
|
+
count += 1
|
110
|
+
end
|
111
|
+
if file_diff.summary['Delete'] > 0
|
112
|
+
body << ', ' if count > 0
|
113
|
+
body << "<span class='delete'>#{file_diff.summary['Delete']} Deletes</span>"
|
114
|
+
count += 1
|
115
|
+
end
|
116
|
+
if file_diff.summary['Update'] > 0
|
117
|
+
body << ', ' if count > 0
|
118
|
+
body << "<span class='update'>#{file_diff.summary['Update']} Updates</span>"
|
119
|
+
count += 1
|
120
|
+
end
|
121
|
+
if file_diff.summary['Move'] > 0
|
122
|
+
body << ', ' if count > 0
|
123
|
+
body << "<span class='move'>#{file_diff.summary['Move']} Moves</span>"
|
124
|
+
end
|
125
|
+
body << '</p>'
|
126
|
+
|
127
|
+
all_fields = [:row, :action, :sibling_position] + file_diff.diff_fields
|
128
|
+
body << '<table>'
|
129
|
+
body << '<thead><tr>'
|
130
|
+
all_fields.each do |fld|
|
131
|
+
body << "<th>#{fld.to_s}</th>"
|
132
|
+
end
|
133
|
+
body << '</tr></thead>'
|
134
|
+
body << '<tbody>'
|
135
|
+
file_diff.diffs.sort_by{|k, v| v[:row] }.each do |key, diff|
|
136
|
+
body << '<tr>'
|
137
|
+
chg = diff[:action]
|
138
|
+
all_fields.each_with_index do |field, i|
|
139
|
+
old = nil
|
140
|
+
style = case chg
|
141
|
+
when 'Add', 'Delete' then chg.downcase
|
142
|
+
end
|
143
|
+
d = diff[field]
|
144
|
+
if d.is_a?(Array)
|
145
|
+
old = d.first
|
146
|
+
new = d.last
|
147
|
+
if old.nil?
|
148
|
+
style = 'add'
|
149
|
+
else
|
150
|
+
style = chg.downcase
|
151
|
+
end
|
152
|
+
else
|
153
|
+
new = d
|
154
|
+
style = chg.downcase if i == 1
|
155
|
+
end
|
156
|
+
body << '<td>'
|
157
|
+
body << "<span class='delete'>#{old}</span>" if old
|
158
|
+
body << '<br>' if old && old.to_s.length > 10
|
159
|
+
body << "<span#{style ? " class='#{style}'" : ''}>#{new}</span>"
|
160
|
+
body << '</td>'
|
161
|
+
end
|
162
|
+
body << '</tr>'
|
163
|
+
end
|
164
|
+
body << '</tbody>'
|
165
|
+
body << '</table>'
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'csv-diff-report/excel'
|
2
|
+
require 'csv-diff-report/html'
|
3
|
+
|
4
|
+
|
5
|
+
class CSVDiff
|
6
|
+
|
7
|
+
# Defines a class for generating diff reports using CSVDiff.
|
8
|
+
#
|
9
|
+
# A diff report may contain multiple file diffs, and can be output as either an
|
10
|
+
# XLSX spreadsheet document, or an HTML file.
|
11
|
+
class Report
|
12
|
+
|
13
|
+
include Excel
|
14
|
+
include Html
|
15
|
+
|
16
|
+
|
17
|
+
# Instantiate a new diff report object.
|
18
|
+
def initialize
|
19
|
+
@diffs = []
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Add a CSVDiff object to this report.
|
24
|
+
def <<(diff)
|
25
|
+
if diff.is_a?(CSVDiff)
|
26
|
+
@diffs << diff
|
27
|
+
else
|
28
|
+
raise ArgumentError, "Only CSVDiff objects can be added to a CSVDiff::Report"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Add a diff to the diff report.
|
34
|
+
#
|
35
|
+
# @param options [Hash] Options to be passed to the diff process.
|
36
|
+
def diff(left, right, options = {})
|
37
|
+
@left = Pathname.new(left)
|
38
|
+
@right = Pathname.new(right)
|
39
|
+
if @left.file? && @right.file?
|
40
|
+
Console.puts "Performing file diff:"
|
41
|
+
Console.puts " From File: #{@left}"
|
42
|
+
Console.puts " To File: #{@right}"
|
43
|
+
opt_file = load_opt_file(@left.dirname)
|
44
|
+
diff_file(@left.to_s, @right.to_s, options, opt_file)
|
45
|
+
elsif @left.directory? && @right.directory?
|
46
|
+
Console.puts "Performing directory diff:"
|
47
|
+
Console.puts " From directory: #{@left}"
|
48
|
+
Console.puts " To directory: #{@right}"
|
49
|
+
opt_file = load_opt_file(@left)
|
50
|
+
if fts = options[:file_types]
|
51
|
+
file_types = find_matching_file_types(fts, opt_file)
|
52
|
+
file_types.each do |file_type|
|
53
|
+
hsh = opt_file[:file_types][file_type]
|
54
|
+
ft_opts = options.merge(hsh)
|
55
|
+
diff_dir(@left, @right, ft_opts, opt_file)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
diff_dir(@left, @right, options, opt_file)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
raise ArgumentError, "Left and right must both exist and be files or directories"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Saves a diff report to +path+ in +format+.
|
67
|
+
#
|
68
|
+
# @param path [String] The path to the output report.
|
69
|
+
# @param format [Symbol] The output format for the report; one of :html or
|
70
|
+
# :xlsx.
|
71
|
+
def output(path, format = :html)
|
72
|
+
path = case format.to_s
|
73
|
+
when /^html$/i
|
74
|
+
html_output(path)
|
75
|
+
when /^xls(x)?$/i
|
76
|
+
xl_output(path)
|
77
|
+
else
|
78
|
+
raise ArgumentError, "Unrecognised output format: #{format}"
|
79
|
+
end
|
80
|
+
Console.puts "Diff report saved to '#{path}'"
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
|
87
|
+
# Loads an options file from +dir+
|
88
|
+
def load_opt_file(dir)
|
89
|
+
opt_path = Pathname(dir + '.csvdiff')
|
90
|
+
opt_path = Pathname('.csvdiff') unless opt_path.exist?
|
91
|
+
if opt_path.exist?
|
92
|
+
Console.puts "Loading options from .csvdiff at '#{dir}'"
|
93
|
+
opt_file = YAML.load(IO.read(opt_path))
|
94
|
+
symbolize_keys(opt_file)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# Convert keys in hashes to lower-case symbols for consistency
|
100
|
+
def symbolize_keys(hsh)
|
101
|
+
Hash[hsh.map{ |k, v| [k.to_s.downcase.intern, v.is_a?(Hash) ?
|
102
|
+
symbolize_keys(v) : v] }]
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Locates the file types in +opt_file+ that match the +file_types+ list of
|
107
|
+
# file type names or patterns
|
108
|
+
def find_matching_file_types(file_types, opt_file)
|
109
|
+
matched_fts = []
|
110
|
+
if known_fts = opt_file && opt_file[:file_types] && opt_file[:file_types].keys
|
111
|
+
file_types.each do |ft|
|
112
|
+
re = Regexp.new(ft.gsub('.', '\.').gsub('?', '.').gsub('*', '.*'), true)
|
113
|
+
matches = known_fts.select{ |file_type| file_type.to_s =~ re }
|
114
|
+
if matches.size > 0
|
115
|
+
matched_fts.concat(matches)
|
116
|
+
else
|
117
|
+
Console.puts "No file type matching '#{ft}' defined in .csvdiff", :yellow
|
118
|
+
Console.puts "Known file types are: #{opt_file[:file_types].keys.join(', ')}", :yellow
|
119
|
+
end
|
120
|
+
end
|
121
|
+
else
|
122
|
+
if opt_file
|
123
|
+
Console.puts "No file types are defined in .csvdiff", :yellow
|
124
|
+
else
|
125
|
+
Console.puts "The file_types option can only be used when a " +
|
126
|
+
".csvdiff is present in the LEFT or current directory", :yellow
|
127
|
+
end
|
128
|
+
end
|
129
|
+
matched_fts.uniq
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
# Diff files that exist in both +left+ and +right+ directories.
|
134
|
+
def diff_dir(left, right, options, opt_file)
|
135
|
+
pattern = Pathname(options[:pattern] || '*')
|
136
|
+
exclude = options[:exclude]
|
137
|
+
|
138
|
+
Console.puts " Include Pattern: #{pattern}"
|
139
|
+
Console.puts " Exclude Pattern: #{exclude}" if exclude
|
140
|
+
|
141
|
+
|
142
|
+
left_files = Dir[left + pattern].sort
|
143
|
+
excludes = exclude ? Dir[left + exclude] : []
|
144
|
+
(left_files - excludes).each_with_index do |file, i|
|
145
|
+
right_file = right + File.basename(file)
|
146
|
+
if right_file.file?
|
147
|
+
diff_file(file, right_file.to_s, options, opt_file)
|
148
|
+
else
|
149
|
+
Console.puts "Skipping file '#{File.basename(file)}', as there is " +
|
150
|
+
"no corresponding TO file", :yellow
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
# Diff two files, and add the results to the diff report.
|
157
|
+
#
|
158
|
+
# @param left [String] The path to the left file
|
159
|
+
# @param right [String] The path to the right file
|
160
|
+
# @param options [Hash] The options to be passed to CSVDiff.
|
161
|
+
def diff_file(left, right, options, opt_file)
|
162
|
+
settings = find_file_type_settings(left, opt_file)
|
163
|
+
return if settings[:ignore]
|
164
|
+
options = settings.merge(options)
|
165
|
+
from = open_source(left, :from, options)
|
166
|
+
to = open_source(right, :to, options)
|
167
|
+
diff = CSVDiff.new(left, right, options)
|
168
|
+
diff.diff_warnings.each{ |warn| Console.puts warn, :yellow }
|
169
|
+
Console.write "Found #{diff.diffs.size} differences"
|
170
|
+
diff.summary.each_with_index.map do |pair, i|
|
171
|
+
Console.write i == 0 ? ": " : ", "
|
172
|
+
k, v = pair
|
173
|
+
color = case k
|
174
|
+
when 'Add' then :light_green
|
175
|
+
when 'Delete' then :red
|
176
|
+
when 'Update' then :cyan
|
177
|
+
when 'Move' then :light_magenta
|
178
|
+
when 'Warning' then :yellow
|
179
|
+
end
|
180
|
+
Console.write "#{v} #{k}s", color
|
181
|
+
end
|
182
|
+
Console.puts
|
183
|
+
self << diff
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
# Locates any file type settings for +left+ in the +opt_file+ hash.
|
188
|
+
def find_file_type_settings(left, opt_file)
|
189
|
+
left = Pathname(left.gsub('\\', '/'))
|
190
|
+
settings = opt_file && opt_file[:defaults] || {}
|
191
|
+
opt_file && opt_file[:file_types] && opt_file[:file_types].each do |file_type, hsh|
|
192
|
+
unless hsh[:pattern]
|
193
|
+
Console.puts "Invalid setting for file_type #{file_type} in .csvdiff; " +
|
194
|
+
"missing a 'pattern' key to use to match files", :yellow
|
195
|
+
hsh[:pattern] = '-'
|
196
|
+
end
|
197
|
+
next if hsh[:pattern] == '-'
|
198
|
+
unless hsh[:matched_files]
|
199
|
+
hsh[:matched_files] = Dir[(left.dirname + hsh[:pattern]).to_s]
|
200
|
+
hsh[:matched_files] -= Dir[(left.dirname + hsh[:exclude]).to_s] if hsh[:exclude]
|
201
|
+
end
|
202
|
+
if hsh[:matched_files].include?(left.to_s)
|
203
|
+
settings.merge!(hsh)
|
204
|
+
[:pattern, :exclude, :matched_files].each{ |k| settings.delete(k) }
|
205
|
+
break
|
206
|
+
end
|
207
|
+
end
|
208
|
+
settings
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
# Opens a source file.
|
213
|
+
#
|
214
|
+
# @param src [String] A path to the file to be opened.
|
215
|
+
# @param options [Hash] An options hash to be passed to CSVSource.
|
216
|
+
def open_source(src, left_right, options)
|
217
|
+
Console.write "Opening #{left_right.to_s.upcase} file '#{File.basename(src)}'..."
|
218
|
+
csv_src = CSVDiff::CSVSource.new(src.to_s, options)
|
219
|
+
Console.puts " #{csv_src.lines.size} lines read", :white
|
220
|
+
csv_src.warnings.each{ |warn| Console.puts warn, :yellow }
|
221
|
+
csv_src
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: csv-diff-report
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.2'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Gardiner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: csv-diff
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: arg-parser
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: color-console
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: axlsx
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
description: ! " This library generates diff reports of CSV files, using the
|
70
|
+
diff capabilities\n of the CSV Diff gem.\n\n Unlike a standard diff
|
71
|
+
that compares line by line, and is sensitive to the\n ordering of records,
|
72
|
+
CSV Diff identifies common lines by key field(s), and\n then compares the
|
73
|
+
contents of the fields in each line.\n\n CSV Diff Report takes the diff information
|
74
|
+
calculated by CSV Diff, and uses it to produce\n Excel or HTML-based diff
|
75
|
+
reports. It also provides a command-line tool for generating\n these diff
|
76
|
+
reports from CSV files.\n"
|
77
|
+
email: adam.b.gardiner@gmail.com
|
78
|
+
executables:
|
79
|
+
- csvdiff
|
80
|
+
extensions: []
|
81
|
+
extra_rdoc_files: []
|
82
|
+
files:
|
83
|
+
- LICENSE
|
84
|
+
- README.md
|
85
|
+
- bin/csvdiff
|
86
|
+
- lib/csv-diff-report.rb
|
87
|
+
- lib/csv-diff-report/cli.rb
|
88
|
+
- lib/csv-diff-report/excel.rb
|
89
|
+
- lib/csv-diff-report/html.rb
|
90
|
+
- lib/csv-diff-report/report.rb
|
91
|
+
- lib/csv_diff_report.rb
|
92
|
+
homepage: https://github.com/agardiner/csv-diff-report
|
93
|
+
licenses: []
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ! '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.4.1
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: CSV Diff Report is a library for generating diff reports using the CSV Diff
|
115
|
+
gem
|
116
|
+
test_files: []
|