dcm 0.0.9

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