dcm 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cli.rb ADDED
@@ -0,0 +1,280 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+
4
+ require "ecu"
5
+ require "thor"
6
+ require "filesize"
7
+ require "json"
8
+
9
+ require_relative "file_reader"
10
+ require_relative "codinginfo"
11
+ require_relative "tempfile_handler"
12
+ require_relative "diff_viewer"
13
+ require_relative "label_selector"
14
+ require_relative "core_ext"
15
+ require_relative "version"
16
+
17
+ module Dcm
18
+
19
+ class CLI < Thor
20
+
21
+ desc "show FILE", "Show all labels in FILE"
22
+ option :oneline, type: :boolean, aliases: ["-l"]
23
+ def show(file=nil)
24
+ display_list(list: parse_file(file), detail: options[:oneline] ? :onelinefull : true)
25
+ end
26
+
27
+ desc "new [SPEC]", "Create a new dcm from scratch"
28
+ option :indented, aliases: ["-i"], type: :boolean, default: false
29
+ def new(*spec)
30
+ list = parse_spec(spec)
31
+ puts list.to_dcm(option[:indented])
32
+ rescue RuntimeError => e
33
+ puts e.message
34
+ exit 1
35
+ end
36
+
37
+ desc "liveview FILE", "Live search and view labels in FILE"
38
+ def liveview(file=nil)
39
+ list = parse_file(file)
40
+ loop { LabelSelector.choose_from(list) }
41
+ end
42
+
43
+ desc "creta2begu ID CODING FILE", "Convert a hierarchical DCM to a flat DCM"
44
+ def creta2begu(id, codingpath, file)
45
+ list = parse_file(file)
46
+
47
+ puts Codinginfo.new([id], codingpath)
48
+ .flatten_list(list)
49
+ .to_dcm
50
+ end
51
+
52
+ desc "begu2creta IDS CODING FILE", "Convert a flat DCM to a hierarchical DCM"
53
+ def begu2creta(ids, codingpath, file)
54
+ list = parse_file(file)
55
+
56
+ puts Codinginfo.new(ids.split(","), codingpath)
57
+ .unflatten_list(list)
58
+ .to_dcm
59
+ end
60
+
61
+ desc "list FILE", "List all labels in FILE"
62
+ option :lab, type: :boolean, aliases: ["-l"]
63
+ def list(file=nil)
64
+ display_list(list: parse_file(file),
65
+ detail: false,
66
+ format: (options[:lab] ? :lab : :tty))
67
+ end
68
+
69
+ desc "summary FILE", "Summarize labels in FILE"
70
+ option :verbose, type: :boolean, aliases: ["-v"], default: false
71
+ def summary(file=nil)
72
+ list = parse_file(file)
73
+ puts "DCM file with #{list.count.quantify("label")}"
74
+ if options[:verbose]
75
+ list.group_by(&:function).each do |function, labels|
76
+ puts " #{function}: #{labels.count}"
77
+ end
78
+ end
79
+ end
80
+
81
+ desc "cat FILE", "Output (filtered) dcm content"
82
+ option :include, aliases: ["-f"], default: ".*"
83
+ option :reject, aliases: ["-F"], default: "(?!x)x"
84
+ option :inlab, aliases: ["-l"], default: ""
85
+ option :notinlab, aliases: ["-L"], default: ""
86
+ option :indented, aliases: ["-i"], type: :boolean, default: false
87
+ option :headers, aliases: ["-h"], default: [], type: :array
88
+ option :subheaders, aliases: ["-H"], default: [], type: :array
89
+ option :mfile, type: :boolean, aliases: ["-m"]
90
+ def cat(file=nil)
91
+ list = parse_file(file)
92
+ if options[:include] != ".*" || options[:reject] != "(?!x)x"
93
+ selector = Regexp.new(options[:include], true)
94
+ rejector = Regexp.new(options[:reject], true)
95
+ list = list.
96
+ select { |l| l.match(selector) }.
97
+ reject { |l| l.match(rejector) }
98
+ end
99
+ if options[:inlab].present?
100
+ labfile = Ecu::LabelList.from_lab(File.read(options[:inlab]))
101
+ list = list.select { |l| labfile.any? { |s| s.name == l.name } }
102
+ end
103
+ if options[:notinlab].present?
104
+ labfile = Ecu::LabelList.from_lab(File.read(options[:notinlab]))
105
+ list = list.reject { |l| labfile.any? { |s| s.name == l.name } }
106
+ end
107
+ list.headers += options[:headers]
108
+ list.subheaders += options[:subheaders]
109
+ if options[:mfile]
110
+ puts list.to_mfile
111
+ else
112
+ puts list.to_dcm(options[:indented])
113
+ end
114
+ end
115
+
116
+ desc "rename MAPPING FILE", "Rename dcm content"
117
+ def rename(mapping=nil, file=nil)
118
+ list = parse_file(file)
119
+ mappings = JSON.parse(FileReader.read(mapping))
120
+ list = list.map_int do |label|
121
+ label.with \
122
+ name: mappings.filter_map { |m| m["new"] if m["old"] == label.name }&.first || label.name
123
+ end
124
+ puts list.to_dcm(options[:indented])
125
+ end
126
+
127
+ desc "validate FILE", "Validate DCM"
128
+ def validate(file=nil)
129
+ parse_file(file)
130
+ end
131
+
132
+ desc "compare FILE1 FILE2", "Compare contents of FILE1 and FILE2"
133
+ option :left, type: :boolean, aliases: ["-l"]
134
+ option :right, type: :boolean, aliases: ["-r"]
135
+ option :comparable, type: :boolean, aliases: ["-c"]
136
+ option :equal, type: :boolean, aliases: ["-e"]
137
+ option :unequal, type: :boolean, aliases: ["-u"]
138
+ option :precision, type: :numeric, aliases: ["-p"], default: 3
139
+ def compare(file1, file2)
140
+ comparison = render_comparison(file1, file2, precision: options[:precision])
141
+
142
+ if options[:left]
143
+ puts to_lab(comparison.names(:left_exclusive))
144
+ elsif options[:right]
145
+ puts to_lab(comparison.names(:right_exclusive))
146
+ elsif options[:equal]
147
+ puts to_lab(comparison.names(:equal))
148
+ elsif options[:unequal]
149
+ puts to_lab(comparison.names(:nonequal))
150
+ elsif options[:comparable]
151
+ puts (
152
+ comparison.names(:equal).map { |l| "#{l}: equal".colorize(:green) } +
153
+ comparison.names(:nonequal).map { |l| "#{l}: unequal".colorize(:red) }
154
+ )
155
+ else
156
+ puts "#{comparison.names(:left_exclusive).size.quantify("label")} exclusive to #{file1}"
157
+ puts "#{comparison.names(:right_exclusive).size.quantify("label")} exclusive to #{file2}"
158
+ puts "#{comparison.names(:common).size.quantify("label")} comparable"
159
+ puts " #{comparison.names(:equal).size.quantify("label")} equal".colorize(:green)
160
+ puts " #{comparison.names(:nonequal).size.quantify("label")} unequal".colorize(:red)
161
+ puts "DCMs are identical!".colorize(:green) if comparison.identical?
162
+ end
163
+ end
164
+
165
+ desc "gitdiff [ARGS]", "Provide a wrapper for git diff"
166
+ def gitdiff(path, file1, hex1, mode1, file2, hex2, mode2)
167
+ if [file1, file2].all? { |f| File.extname(f).downcase == ".dcm" }
168
+ invoke(:diff, [file1, file2])
169
+ else
170
+ puts DiffViewer.new(File.read(file1), File.read(file2))
171
+ end
172
+ end
173
+
174
+ desc "diff FILE1 FILE2", "Diff contents between FILE1 and FILE2"
175
+ option :left, type: :boolean, aliases: ["-l"]
176
+ option :right, type: :boolean, aliases: ["-r"]
177
+ option :comparable, type: :boolean, aliases: ["-c"]
178
+ option :precision, type: :numeric, aliases: ["-p"], default: 3
179
+ def diff(file1, file2)
180
+ comparison = render_comparison(file1, file2, precision: options[:precision])
181
+
182
+ showall = !(options[:left] || options[:comparable] || options[:right])
183
+
184
+ puts "-#{file1}".colorize(:red)
185
+ puts "+#{file2}".colorize(:green)
186
+ puts ""
187
+ if options[:left] || showall
188
+ comparison.left_exclusive.each do |label|
189
+ puts DiffViewer.new(label.to_s(detail: true), "")
190
+ end
191
+ end
192
+ if options[:comparable] || showall
193
+ comparison.differences.each do |left, right|
194
+ puts DiffViewer.new(left.to_s(detail: true), right.to_s(detail: true))
195
+ end
196
+ end
197
+ if options[:right] || showall
198
+ comparison.right_exclusive.each do |label|
199
+ puts DiffViewer.new("", label.to_s(detail: true))
200
+ end
201
+ end
202
+ end
203
+
204
+ desc "size FILE", "Guess the size of the labels contained in FILE"
205
+ def size(file)
206
+ list = parse_file(file)
207
+ puts Filesize.new(list.map(&:bytesize).reduce(:+)).pretty
208
+ end
209
+
210
+ desc "merge FILE1 FILE2 [FILE3 ...]", "Merge several files into a single DCM"
211
+ option :priority, aliases: ["-p"], default: "RIGHT"
212
+ def merge(*files)
213
+ baselist = parse_file(files.shift)
214
+ while not files.empty?
215
+ mergelist = parse_file(files.shift)
216
+ baselist = Ecu::LabelListComparison.new(baselist, mergelist).
217
+ merge(priority: options[:priority].downcase.to_sym)
218
+ end
219
+ puts baselist.to_dcm(options[:indented])
220
+ end
221
+
222
+ map %w[--version -v] => :__print_version
223
+
224
+ desc "--version, -v", "Print the version"
225
+ def __print_version
226
+ puts "v#{Dcm::VERSION}"
227
+ end
228
+
229
+ def self.exit_on_failure?
230
+ true
231
+ end
232
+
233
+ protected
234
+
235
+ def parse_file(file)
236
+ if file.nil? || file == "-"
237
+ file = TempfileHandler.create(STDIN.read, filename: ["stdin", ".dcm"])
238
+ end
239
+ Ecu::LabelList.from_dcm(FileReader.read(file))
240
+ rescue Ecu::MalformedDcmError => e
241
+ puts e.message
242
+ puts e.context
243
+ exit 1
244
+ end
245
+
246
+ def parse_spec(spec)
247
+ return Ecu::LabelList.new if spec.empty?
248
+ fail "Usage: dcm new TYPE NAME VALUE1 [VALUE2 ...]" if spec.length < 3
249
+ type, name, value = spec
250
+ value = Float(value) rescue value
251
+ label = Ecu::Festwert.new \
252
+ name: name,
253
+ value: value
254
+ Ecu::LabelList.new([label])
255
+ end
256
+
257
+ def display_list(list:, detail:, format: :tty)
258
+ case format
259
+ when :tty
260
+ puts list.map { ListColorizer.call(_1.to_s(detail: detail)) }.join("\n")
261
+ when :lab
262
+ puts list.to_lab
263
+ else
264
+ error "Unkown format #{format}"
265
+ end
266
+ end
267
+
268
+ def render_comparison(file1, file2, precision:)
269
+ lists = [file1, file2]
270
+ .map { FileReader.read(_1) }
271
+ .map { Ecu::LabelList.from_dcm(_1) }
272
+ .map { _1.map_int { |label| label.round_to(precision) } }
273
+ Ecu::LabelListComparison.new(*lists)
274
+ end
275
+
276
+ def to_lab(ary)
277
+ ary.unshift("[LABEL]").join("\n")
278
+ end
279
+ end
280
+ end
data/lib/codinginfo.rb ADDED
@@ -0,0 +1,151 @@
1
+ require "simple_xlsx_reader"
2
+
3
+ class Codinginfo
4
+ CVAR_PREFIX_REGEXP = /^CVAR_[A-Za-z0-9_]+_\d+\./
5
+ PARAM_ASSIGNMENT_HEADERS = { name: /Parameter/, cvar: /Schalter/, package: /Komponente/ }
6
+
7
+ class Cvar
8
+
9
+ # The CVAR must have a name and _may_ have a package and value
10
+ # the package and value can be merged
11
+
12
+ attr_reader :name, :value, :package
13
+ def initialize(name:, package: nil, value: nil)
14
+ @name = name
15
+ @package = package
16
+ @value = value
17
+ end
18
+
19
+ def label_prefix
20
+ return /^#{name}_(\w+_)?#{value}\./ if package.nil?
21
+
22
+ [name, package, value].join("_") + "."
23
+ end
24
+
25
+ def to_s = "#{name}=#{value}"
26
+ def ==(other) = name == other.name
27
+
28
+ def merge(other)
29
+ fail "Cannot merge #{name} with #{other.name}" unless self == other
30
+
31
+ self.class.new \
32
+ name: name,
33
+ package: package || other.package,
34
+ value: value || other.value
35
+ end
36
+ end
37
+
38
+ class Variant
39
+ def initialize(headers, row)
40
+ @properties = headers.zip(row).to_h
41
+ end
42
+
43
+ def cvars
44
+ @properties
45
+ .select { |key, _| key.match?(/^CVAR_/) }
46
+ .map { Cvar.new(name: _1, value: _2.to_i) }
47
+ end
48
+ end
49
+
50
+ def initialize(ids, filepath)
51
+ @doc = SimpleXlsxReader.open(filepath)
52
+ @headers = []
53
+ @variants = []
54
+ @assignments = {}
55
+
56
+ overview_sheet
57
+ .rows
58
+ .each do |row|
59
+ parse_headers(row) if @headers.empty?
60
+ next if @headers.empty?
61
+ next if row[1].nil? || row[3].nil?
62
+ next unless ids.any? { row[1].match?(/#{_1}/i) }
63
+ next unless row[3].match?(/CVAR/)
64
+
65
+ @variants << Variant.new(@headers, row)
66
+ end
67
+
68
+ param_assignment_sheet
69
+ .rows
70
+ .each(headers: PARAM_ASSIGNMENT_HEADERS) do |row|
71
+ next if row[:cvar].nil?
72
+
73
+ @assignments[row[:name]] = Cvar.new(name: row[:cvar], package: row[:package])
74
+ end
75
+ end
76
+
77
+ def overview_sheet
78
+ @doc
79
+ .sheets
80
+ .find { _1.name == "Gesamtübersicht" }
81
+ .tap { fail "Cannot find sheet Gesamtübersicht" if _1.nil? }
82
+ end
83
+
84
+ def param_assignment_sheet
85
+ @doc
86
+ .sheets
87
+ .find { _1.name == "Parameterdefinition" }
88
+ .tap { fail "Cannot find sheet Parameterdefinition" if _1.nil? }
89
+ end
90
+
91
+ def variant
92
+ fail "No variant matching #{id} found!" if @variants.empty?
93
+ fail "More than one variant matching #{id} found!" if @variants.size > 1
94
+
95
+ @variants.first
96
+ end
97
+
98
+ def unflatten_list(list)
99
+ Ecu::LabelList.new \
100
+ list.flat_map { |label|
101
+ if has_cvar_assignment?(label)
102
+ @variants.map { label.with(name: add_cvar_prefix(label, _1)) }
103
+ else
104
+ label
105
+ end
106
+ }
107
+ end
108
+
109
+ def flatten_list(list)
110
+ list
111
+ .select { !cvar_coded?(_1) || has_matching_cvar?(_1) }
112
+ .map_int { _1.with(name: remove_cvar_prefix(_1.name)) }
113
+ end
114
+
115
+ def cvar_coded?(label)
116
+ label.name.match?(CVAR_PREFIX_REGEXP)
117
+ end
118
+
119
+ def has_cvar_assignment?(label)
120
+ @assignments.key?(label.name)
121
+ end
122
+
123
+ def has_matching_cvar?(label)
124
+ variant.cvars.any? { label.name.match?(_1.label_prefix) }
125
+ end
126
+
127
+ def add_cvar_prefix(label, variant)
128
+ variant
129
+ .cvars
130
+ .find { _1 == @assignments[label.name] }
131
+ .then { _1.merge(@assignments[label.name]) }
132
+ .then { _1.label_prefix + label.name }
133
+ end
134
+
135
+ def remove_cvar_prefix(name)
136
+ name.sub(CVAR_PREFIX_REGEXP, "")
137
+ end
138
+
139
+ def parse_headers(row)
140
+ return unless row[1]&.match?(/SWFK-ID/)
141
+ return unless row[1]&.match?(/CVAR_BeguData/)
142
+ return unless row[2]&.match?(/Kommentar/)
143
+
144
+ @headers = row
145
+ .map(&:chomp)
146
+ .map(&:lstrip)
147
+ .map(&:strip)
148
+ .map { _1.sub(/\n.*/, "") }
149
+ .tap { _1[0] = "typestr" }
150
+ end
151
+ end
data/lib/core_ext.rb ADDED
@@ -0,0 +1,40 @@
1
+ require "pastel"
2
+
3
+ class Object
4
+ def as
5
+ yield self
6
+ end
7
+ end
8
+
9
+ class Integer
10
+ def quantify(singular, plural="#{singular}s")
11
+ "#{to_s} #{self == 1 ? singular : plural}"
12
+ end
13
+ end
14
+
15
+ class String
16
+ def indent(spaces = 2)
17
+ lines.map { |line| " " * spaces + line }.join
18
+ end
19
+
20
+ def inline_dcm
21
+ "KONSERVIERUNG_FORMAT 2.0\n\n" + self
22
+ end
23
+
24
+ def enquote
25
+ "\"#{to_s}\""
26
+ end
27
+
28
+ def present?
29
+ !empty?
30
+ end
31
+
32
+ def colorize(color)
33
+ return self if false
34
+ pastel.decorate(self, color)
35
+ end
36
+
37
+ def pastel
38
+ @@pastel ||= Pastel.new
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ require "diffy"
2
+
3
+ module Dcm
4
+ class DiffViewer
5
+ def initialize(left, right)
6
+ Diffy::Diff.default_format = :color
7
+ @diff = Diffy::Diff.new(left, right)
8
+ end
9
+
10
+ def to_s
11
+ @diff.to_s + "\n"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ class FileReader
2
+ def self.read(file)
3
+ str = File.read(file)
4
+
5
+ # Try it as UTF-8 directly
6
+ cleaned = str.dup.force_encoding('UTF-8')
7
+ unless cleaned.valid_encoding?
8
+ # Some of it might be old Windows code page
9
+ cleaned = str.encode( 'UTF-8', 'Windows-1252' )
10
+ end
11
+ str = cleaned
12
+ rescue EncodingError
13
+ # Force it to UTF-8, throwing out invalid bits
14
+ str.encode!('UTF-8', invalid: :replace, undef: :replace)
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ require_relative "core_ext"
2
+ require_relative "selecta"
3
+ require_relative "list_colorizer"
4
+
5
+ class LabelSelector
6
+ def self.choose_from(list)
7
+ view = selecta(list)
8
+ print view
9
+ STDIN.gets
10
+ rescue Interrupt => e
11
+ clear_screen
12
+ exit 0
13
+ end
14
+
15
+ def self.selecta(list)
16
+ clear_screen
17
+ Selecta.new.main_api(keys: list.map(&:name),
18
+ values: list.map { |e| ListColorizer.call(e.to_s(detail: true)) },
19
+ options: { height: "full" })
20
+ clear_screen
21
+ end
22
+
23
+ def self.clear_screen
24
+ print "\e[2J\e[H"
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ class ListColorizer
2
+ def self.call(str)
3
+ str.
4
+ gsub(/^([^ :]+)/) { $1.colorize(:green) }.
5
+ gsub(/^ (\w+)/) { " " + $1.colorize(:bright_blue) }
6
+ end
7
+ end