opencdd 0.3.1 → 0.3.3

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: c582aa6a0bf79bedf583a586f97c1c308852b53d4efc9972b3bb883a2be425fc
4
- data.tar.gz: 1195b6615ee045c3c1ac63f237ccdb930d80838db9dbe33677216e271748accc
3
+ metadata.gz: c3b245ffe7055b867a634912373305b044261c4fab4af420029ad62be539321f
4
+ data.tar.gz: c26921adb3a18950743b39274420522d3b41ff68e1b478ee1c27fb1e0c19d374
5
5
  SHA512:
6
- metadata.gz: 1cbdf2e8832ccd86b95fcfc6789d8d9dfe4be656a27f7b4cac4cb854ea6f224153f5aa8cca521362f364954e123e0c50d3424da3daf031cd73f1974823794cb9
7
- data.tar.gz: 072c326fba5a5807249b7ea79eda95c31fc42384c80de750dba20c741de0601c8c67afe61e9b58a27e594797c68b12cbb38b4166c0f56ce547130e610625c80c
6
+ metadata.gz: 96952a531d921778810b8c6f929589d933d536dbbac9ceed1c3f5b41006f710b4b649262420241a05554cba496d8ff45ab6b9b54e8caa58883fb865180fac96f
7
+ data.tar.gz: 625fd243c5ca85e9cca57b2c2c4eae0c1f9024b9444255ba0df60773175c2029ec76ca4e3acb070e527bd9190066c38682130c1be046fca22c00f60b17b0f8be
data/bin/lint-no-raw-mdc CHANGED
@@ -1,4 +1,6 @@
1
- #!/usr/bin/env bash
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
2
4
  # lint-no-raw-mdc — fail if any raw MDC_P### / MDC_C### / EXT_P### /
3
5
  # EXT_C### / CIM_P### string literal appears in lib/opencdd/ outside
4
6
  # the designated SSOT files.
@@ -14,29 +16,50 @@
14
16
  #
15
17
  # Comment lines (lines whose first non-whitespace character is '#')
16
18
  # are also exempt.
17
- #
18
- set -euo pipefail
19
-
20
- excludes=(
21
- ':!lib/opencdd/property_ids.rb'
22
- ':!lib/opencdd/meta_class.rb'
23
- ':!lib/opencdd/alias_table.rb'
24
- ':!lib/opencdd/parcel/sheet_schema.rb'
25
- ':!lib/opencdd/cddal/generated_parser.rb'
26
- )
27
-
28
- # Collect hits, filtering out Ruby comment-only lines.
29
- hits=$(git grep -nE '"(MDC_[CP][0-9]+(_[0-9]+)?|EXT_[CP][0-9]+|CIM_P[0-9]+)"' \
30
- -- 'lib/opencdd/**/*.rb' "${excludes[@]}" 2>/dev/null \
31
- | grep -vE '^[^:]+:[0-9]+:\s*#' \
32
- || true)
33
-
34
- if [ -n "$hits" ]; then
35
- echo "ERROR: raw MDC/EXT/CIM literal found outside designated SSOT files." >&2
36
- echo "Use Opencdd::PropertyIds::REGISTRY / Opencdd::MetaClasses::REGISTRY instead." >&2
37
- echo "" >&2
38
- echo "$hits" >&2
19
+
20
+ require "open3"
21
+
22
+ EXCLUDED_PATHS = %w[
23
+ lib/opencdd/property_ids.rb
24
+ lib/opencdd/meta_class.rb
25
+ lib/opencdd/alias_table.rb
26
+ lib/opencdd/parcel/sheet_schema.rb
27
+ lib/opencdd/cddal/generated_parser.rb
28
+ ].freeze
29
+
30
+ # Match "MDC_P###", "MDC_C###", "EXT_P###", "EXT_C###", "CIM_P###"
31
+ # with optional _### sub-ids, as a double-quoted Ruby string literal.
32
+ # Source string is POSIX ERE (not Ruby regex) — git grep -E requires it.
33
+ LITERAL_PATTERN_SOURCE = '"(MDC_[CP][0-9]+(_[0-9]+)?|EXT_[CP][0-9]+|CIM_P[0-9]+)"'.freeze
34
+
35
+ # Each git grep hit looks like: <path>:<lineno>:<content>
36
+ # Skip comment-only lines (content's first non-space char is '#').
37
+ HIT_LINE_PATTERN = /\A(?<path>[^:]+):(?<lineno>\d+):(?<content>.*)\z/m.freeze
38
+
39
+ # lib/opencdd/**/*.rb minus the excluded SSOT files.
40
+ pathspecs = ["lib/opencdd/**/*.rb"].concat(EXCLUDED_PATHS.map { |p| ":!#{p}" })
41
+
42
+ cmd = ["git", "grep", "-n", "-E", LITERAL_PATTERN_SOURCE] + ["--"] + pathspecs
43
+ stdout, _stderr, status = Open3.capture3(*cmd)
44
+
45
+ # git grep returns 1 when there are no matches, which is the success
46
+ # case. Treat anything other than 0/1 as a real failure.
47
+ unless [0, 1].include?(status.exitstatus)
48
+ warn "ERROR: git grep failed (exit #{status.exitstatus})"
49
+ exit 2
50
+ end
51
+
52
+ hits = stdout.each_line.map(&:chomp).reject do |line|
53
+ match = HIT_LINE_PATTERN.match(line)
54
+ match && match[:content].lstrip.start_with?("#")
55
+ end
56
+
57
+ if hits.any?
58
+ warn "ERROR: raw MDC/EXT/CIM literal found outside designated SSOT files."
59
+ warn "Use Opencdd::PropertyIds::REGISTRY / Opencdd::MetaClasses::REGISTRY instead."
60
+ warn ""
61
+ hits.each { |hit| warn hit }
39
62
  exit 1
40
- fi
63
+ end
41
64
 
42
65
  exit 0
data/lib/cdd.rb CHANGED
@@ -6,6 +6,6 @@
6
6
  require "opencdd"
7
7
 
8
8
  # Alias for backward compatibility. The module was renamed from
9
- # Cdd to Opencdd, but many callers (cdd-data Rakefile, specs, etc.)
9
+ # Cdd to Opencdd, but many callers (data-private Rakefile, specs, etc.)
10
10
  # still reference Cdd::. This alias lets them keep working.
11
11
  Cdd = Opencdd unless defined?(Cdd)
@@ -174,7 +174,6 @@ module Opencdd
174
174
  def format_value(value)
175
175
  s = value.to_s
176
176
  return quote_string(s) if string_literal?(s)
177
- return format_set(s) if set_like?(s)
178
177
  s
179
178
  end
180
179
 
@@ -194,15 +193,6 @@ module Opencdd
194
193
  "\"#{escaped}\""
195
194
  end
196
195
 
197
- def set_like?(s)
198
- s.start_with?("(") && s.end_with?(")")
199
- end
200
-
201
- def format_set(s)
202
- elements = Opencdd::StructuredValues.unwrap_and_split(s)
203
- "{ #{elements.join(', ')} }"
204
- end
205
-
206
196
  def symbol_name_for(entity)
207
197
  code = entity.code&.to_s
208
198
  return code if code && !code.empty?
@@ -110,6 +110,7 @@ module Opencdd
110
110
  def relations ; @entities_by_type[:relation] || [] ; end
111
111
  def view_controls ; @entities_by_type[:view_control] || [] ; end
112
112
  def list_of_units ; @entities_by_type[:list_of_unit] || [] ; end
113
+ def det_classifications ; @entities_by_type[:det_classification] || [] ; end
113
114
 
114
115
  def entities_of_type(type)
115
116
  @entities_by_type[type.to_sym] || []
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class DetClassification < Opencdd::Entity
5
+ # The cdd.iec.ch search-export DET classification .xls stores the
6
+ # entity code as a short string (e.g. "A11") in column 1. We
7
+ # need to synthesize the full IRDI by prepending the meta-class's
8
+ # supplier prefix (IECCDD_001) — the standard from_row would
9
+ # IRDI.parse("A11") which fails (no namespace).
10
+ DET_CLASSIFICATION_SUPPLIER = "0112/2///IECCDD_001".freeze
11
+
12
+ def self.from_row(row, schema:, meta_class_irdi:, code_property_id: nil)
13
+ row = row.dup
14
+ if meta_class_irdi&.code == "MDC_C0101"
15
+ # Promote the short code to a full IRDI before parent handles it.
16
+ # code_property_id for MDC_C0101 is the MDC code ID (MDC_P001_5).
17
+ cid = code_property_id || Opencdd::MetaClasses.code_property_id_for("MDC_C0101")
18
+ short = cid && row[cid]
19
+ if short && !short.to_s.include?("#")
20
+ row[cid] = "#{DET_CLASSIFICATION_SUPPLIER}##{short}"
21
+ end
22
+ end
23
+ super
24
+ end
25
+ end
26
+ end
@@ -70,14 +70,15 @@ module Opencdd
70
70
  # omitted (payload still has all the entity's own fields).
71
71
  # ─────────────────────────────────────────────────────────────
72
72
  PAYLOAD_BUILDERS = {
73
- Opencdd::Klass => :class_node,
74
- Opencdd::Property => :property_node,
75
- Opencdd::Unit => :unit_node,
76
- Opencdd::ValueList => :value_list_node,
77
- Opencdd::ValueTerm => :value_term_node,
78
- Opencdd::Relation => :relation_node,
79
- Opencdd::ViewControl => :view_control_node,
80
- Opencdd::ListUnit => :list_of_unit_node,
73
+ Opencdd::Klass => :class_node,
74
+ Opencdd::Property => :property_node,
75
+ Opencdd::Unit => :unit_node,
76
+ Opencdd::ValueList => :value_list_node,
77
+ Opencdd::ValueTerm => :value_term_node,
78
+ Opencdd::Relation => :relation_node,
79
+ Opencdd::ViewControl => :view_control_node,
80
+ Opencdd::ListUnit => :list_of_unit_node,
81
+ Opencdd::DetClassification => :det_classification_node,
81
82
  }.freeze
82
83
 
83
84
  def payload_for(entity, database: nil)
@@ -134,6 +135,10 @@ module Opencdd
134
135
  entity_payload(lou).merge(type: "list_of_unit").compact
135
136
  end
136
137
 
138
+ def det_classification_node(det)
139
+ entity_payload(det).merge(type: "det_classification").compact
140
+ end
141
+
137
142
  private
138
143
 
139
144
  # Iterates every declared field on the entity's class (walking
@@ -12,6 +12,12 @@ module Opencdd
12
12
  # "what languages does this dictionary have?" — used by the browser
13
13
  # to render a language switcher and by the data pipeline to report
14
14
  # language coverage.
15
+ #
16
+ # This is a model object. It does not normalize language codes:
17
+ # source-format quirks (e.g. IEC CDD .xls files using "jp" for
18
+ # Japanese) are normalized at the SheetSchema ingestion boundary
19
+ # via +Opencdd::Parcel::LanguageAliases+, so every consumer of
20
+ # this class can assume ISO 639-1 codes unconditionally.
15
21
  class Languages
16
22
  attr_reader :source, :translations
17
23
 
@@ -58,7 +64,7 @@ module Opencdd
58
64
  def self.from_properties(properties, default_source: "en")
59
65
  langs = properties.keys.each_with_object(Set.new) do |key, acc|
60
66
  next unless key.include?(".")
61
- prefix, lang = key.split(".", 2)
67
+ _, lang = key.split(".", 2)
62
68
  next unless lang =~ /\A[a-z]{2}(-[a-z0-9]+)?\z/i
63
69
  acc << lang
64
70
  end
@@ -67,4 +73,4 @@ module Opencdd
67
73
  new(source: source, translations: translations)
68
74
  end
69
75
  end
70
- end
76
+ end
@@ -80,6 +80,7 @@ module Opencdd
80
80
  "MDC_C009" => "MDC_P001_10",
81
81
  "MDC_C010" => "MDC_P001_11",
82
82
  "MDC_C0100" => "C0101",
83
+ "MDC_C0101" => "MDC_P001_5",
83
84
  "EXT_C001" => "EXT_P001",
84
85
  }.freeze
85
86
 
@@ -91,6 +92,8 @@ module Opencdd
91
92
  "MDC_C009" => :unit,
92
93
  "MDC_C010" => :value_term,
93
94
  "MDC_C0100" => :list_of_unit,
95
+ "MDC_C0101" => :det_classification,
96
+ "IECCDD_001" => :det_classification,
94
97
  "EXT_C001" => :view_control,
95
98
  }.freeze
96
99
 
@@ -261,6 +264,17 @@ module Opencdd
261
264
  type: :list_of_unit,
262
265
  allowed_property_ids: common,
263
266
  )
267
+ # cdd.iec.ch's search-export uses CLASS_ID:=IECCDD_001 (an
268
+ # IEC-internal supplier scheme) — not a Parcel meta-class IRDI.
269
+ # We register MDC_C0101 as the canonical meta-class IRDI; the
270
+ # file's CLASS_ID is documentation, not the parsing gate.
271
+ det_classification = MetaClass.new(
272
+ irdi: "MDC_C0101",
273
+ name: "DetClassification",
274
+ entity_class: Opencdd::DetClassification,
275
+ type: :det_classification,
276
+ allowed_property_ids: common,
277
+ )
264
278
  {
265
279
  klass_class.irdi => klass_class,
266
280
  property_class.irdi => property_class,
@@ -270,6 +284,7 @@ module Opencdd
270
284
  value_term.irdi => value_term,
271
285
  view_control.irdi => view_control,
272
286
  list_of_unit.irdi => list_of_unit,
287
+ det_classification.irdi => det_classification,
273
288
  }
274
289
  end
275
290
  end
@@ -24,6 +24,7 @@ module Opencdd
24
24
  "VALUELIST" => :value_list,
25
25
  "VALUETERMS" => :value_term,
26
26
  "LISTOFUNITS" => :list_of_unit,
27
+ "DETCLASSIFICATION" => :det_classification,
27
28
  }.freeze
28
29
 
29
30
  attr_reader :path
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ module Parcel
5
+ # Maps non-conformant language codes found in IEC CDD source .xls
6
+ # exports to their ISO 639-1 equivalents.
7
+ #
8
+ # This is a Parcel-layer concern, not a model concern: the aliases
9
+ # exist because one specific source format (IEC CDD XLS) uses
10
+ # non-standard codes in its property ID suffixes and directive
11
+ # rows. The +Languages+ value object — and every downstream
12
+ # consumer (JSON wire format, browser, TS model) — speaks ISO
13
+ # 639-1 unconditionally.
14
+ #
15
+ # The normalization happens at the SheetSchema ingestion
16
+ # boundary so downstream code never sees an alias. SheetSchema
17
+ # also exposes the set of original-to-canonical mappings it
18
+ # applied via +#normalized_language_codes+ for data-quality
19
+ # reporting.
20
+ module LanguageAliases
21
+ ALIASES = {
22
+ "jp" => "ja",
23
+ }.freeze
24
+
25
+ # Returns the ISO 639-1 form of +code+, or the input unchanged
26
+ # if it is already standard. Pure function: no I/O, no side
27
+ # effects.
28
+ def self.normalize(code)
29
+ return code if code.nil?
30
+ s = code.to_s.strip
31
+ return s if s.empty?
32
+ ALIASES.fetch(s, s)
33
+ end
34
+
35
+ # True when +code+ would be rewritten by +normalize+.
36
+ def self.alias?(code)
37
+ return false if code.nil?
38
+ ALIASES.key?(code.to_s.strip)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -32,7 +32,7 @@ module Opencdd
32
32
 
33
33
  # Per-type export filename produced by cdd.iec.ch's download
34
34
  # endpoint.
35
- FILE_PATTERN = /\Aexport_(CLASS|PROPERTY|RELATION|UNIT|VALUELIST|VALUETERMS|LISTOFUNITS)_[^\.]+\.(xls|xlsx)\z/i.freeze
35
+ FILE_PATTERN = /\Aexport_(CLASS|PROPERTY|RELATION|UNIT|VALUELIST|VALUETERMS|LISTOFUNITS|DETCLASSIFICATION)_[^\.]+\.(xls|xlsx)\z/i.freeze
36
36
 
37
37
  module_function
38
38
 
@@ -4,7 +4,7 @@ module Opencdd
4
4
  module Parcel
5
5
  # Verifies that scrape output produced populated .xls files, not
6
6
  # just schema stubs. Catches the silent failure mode discovered
7
- # on 2026-07-08: iec61360/iec63213/iec61360-7 scrapes appeared
7
+ # on 2026-07-08: iec61360/iec-63213/iec-61360-7 scrapes appeared
8
8
  # complete (10,338 PROPERTY .xls files for iec61360) but every
9
9
  # file had 0 data rows. The build pipeline exported 574 classes
10
10
  # + 0 properties, and the browser showed "No declared properties"
@@ -26,7 +26,7 @@ module Opencdd
26
26
  # +.xls+ files into a Workbook.
27
27
  #
28
28
  # Example:
29
- # reader = Opencdd::Parcel::ShardedDirReader.new("downloads/iec63213")
29
+ # reader = Opencdd::Parcel::ShardedDirReader.new("downloads/iec-63213")
30
30
  # db = Opencdd::Database.new
31
31
  # reader.load_into(db)
32
32
  #
@@ -3,11 +3,15 @@
3
3
  module Opencdd
4
4
  module Parcel
5
5
  class SheetSchema
6
- # Parcel-specific column-ID canonicalization. Maps the
7
- # ParcelMaker variant headers (MDC_P004_1, MDC_P005, ...)
8
- # to the canonical IEC 61360 IDs (MDC_P004, MDC_P006, ...).
6
+ # Parcel-specific column-ID canonicalization. Maps:
9
7
  #
10
- # Owned by SheetSchema because this mapping only exists for
8
+ # - ParcelMaker language-variant headers (MDC_P004_1, MDC_P005, ...)
9
+ # to the canonical IEC 61360 IDs (MDC_P004, MDC_P006, ...).
10
+ # - cdd.iec.ch search-export's project-specific IDs for DET
11
+ # classification (IECCDD_001_C0001, IECCDD_001_C0002.<lang>, ...)
12
+ # to the canonical IEC 61360 IDs (MDC_P001_5, MDC_P004_1.<lang>, ...).
13
+ #
14
+ # Owned by SheetSchema because these mappings only exist for
11
15
  # the Parcel sheet layout — the PropertyIds registry is
12
16
  # ontology-only and shouldn't carry format-specific details.
13
17
  VARIANT_TO_CANONICAL = {
@@ -17,11 +21,29 @@ module Opencdd
17
21
  "MDC_P005" => "MDC_P006",
18
22
  "MDC_P007_1" => "MDC_P008",
19
23
  "MDC_P007_2" => "MDC_P009",
24
+
25
+ # cdd.iec.ch search-export DET classification aliases.
26
+ # The file uses IEC-internal supplier scheme (IECCDD_001) and
27
+ # project-specific column IDs (C0001 = code, C0002.<lang> = name)
28
+ # rather than the standard MDC codes. Without these mappings,
29
+ # every DET classification entity would have nil code + name.
30
+ #
31
+ # C0002 maps to MDC_P004 (preferred_name), not MDC_P004_1
32
+ # (preferred_name_localized), because canonical_id splits the
33
+ # language suffix off and reapplies it after the base lookup —
34
+ # so the per-language entries (C0002.en, C0002.fr, etc.) below
35
+ # would be dead code. The base entry alone produces
36
+ # MDC_P004.<lang>, which is what the preferred_name field DSL
37
+ # reads via FieldRegistry.
38
+ "IECCDD_001_C0001" => "MDC_P001_5",
39
+ "IECCDD_001_C0002" => "MDC_P004",
20
40
  }.freeze
21
41
 
22
42
  # Canonicalize a Parcel column ID. Splits language tags
23
43
  # (<id>.<lang>) so they round-trip cleanly. Returns the
24
44
  # canonical ID (or the input unchanged if no mapping exists).
45
+ # Language codes are normalized to ISO 639-1 via
46
+ # +Opencdd::Parcel::LanguageAliases.normalize+.
25
47
  def self.canonical_id(raw_id)
26
48
  return nil if raw_id.nil?
27
49
  s = raw_id.to_s.strip
@@ -30,7 +52,7 @@ module Opencdd
30
52
  base = match ? match.pre_match : s
31
53
  lang = match && match[:lang]
32
54
  canonical = VARIANT_TO_CANONICAL[base] || base
33
- lang ? "#{canonical}.#{lang}" : canonical
55
+ lang ? "#{canonical}.#{Opencdd::Parcel::LanguageAliases.normalize(lang)}" : canonical
34
56
  end
35
57
 
36
58
  DIRECTIVE_ROWS = %w[
@@ -110,7 +132,7 @@ module Opencdd
110
132
 
111
133
  DIRECTIVE_ROW_PREFIX = "#".freeze
112
134
 
113
- attr_reader :columns, :columns_by_id, :column_directives
135
+ attr_reader :columns, :columns_by_id, :column_directives, :normalized_language_codes
114
136
 
115
137
  def initialize
116
138
  @columns = []
@@ -194,6 +216,7 @@ module Opencdd
194
216
  @columns_by_id[col.property_id] = col
195
217
  end
196
218
 
219
+ @normalized_language_codes = compute_normalized_language_codes.freeze
197
220
  freeze
198
221
  end
199
222
 
@@ -260,11 +283,34 @@ module Opencdd
260
283
  next if v.nil?
261
284
  s = v.to_s.strip
262
285
  next if s.empty?
286
+ lang = Opencdd::Parcel::LanguageAliases.normalize(lang)
263
287
  lang = "en" if lang.nil? || lang.empty?
264
288
  h[lang] = s
265
289
  end
266
290
  h
267
291
  end
292
+
293
+ # Audit trail of language-code normalizations applied at this
294
+ # schema's ingestion boundary. Returns a frozen Hash mapping each
295
+ # original (non-conformant) language code seen in the source to
296
+ # its ISO 639-1 equivalent. Empty when the source spoke ISO.
297
+ def compute_normalized_language_codes
298
+ seen = {}
299
+ @column_directives.each_key do |key|
300
+ _, lang = key.to_s.split(".", 2)
301
+ next if lang.nil? || lang.empty?
302
+ normalized = Opencdd::Parcel::LanguageAliases.normalize(lang)
303
+ seen[lang] = normalized if normalized != lang
304
+ end
305
+ @columns.each do |col|
306
+ next if col.raw_property_id.nil?
307
+ _, lang = col.raw_property_id.split(".", 2)
308
+ next if lang.nil? || lang.empty?
309
+ normalized = Opencdd::Parcel::LanguageAliases.normalize(lang)
310
+ seen[lang] = normalized if normalized != lang
311
+ end
312
+ seen
313
+ end
268
314
  end
269
315
  end
270
316
  end
@@ -17,7 +17,7 @@ module Opencdd
17
17
  # version's subfolder.
18
18
  #
19
19
  # Example:
20
- # reader = Opencdd::Parcel::VersionedReader.new("downloads/iec63213")
20
+ # reader = Opencdd::Parcel::VersionedReader.new("downloads/iec-63213")
21
21
  # reader.versions_for("KEA012") # => 3 VersionHistory::Entry
22
22
  # reader.load_version("KEA012", "ABC123...") # => Database with v002 content
23
23
  class VersionedReader
@@ -18,6 +18,8 @@ module Opencdd
18
18
  "UNIT" => :unit,
19
19
  "VALUELIST" => :value_list,
20
20
  "VALUETERMS" => :value_term,
21
+ "LISTOFUNITS" => :list_of_unit,
22
+ "DETCLASSIFICATION" => :det_classification,
21
23
  }.freeze
22
24
 
23
25
  LEGACY_TYPE_TO_PARCEL_NAME = {
@@ -27,6 +29,8 @@ module Opencdd
27
29
  value_term: "TERMINOLOGY",
28
30
  unit: "UoM",
29
31
  relation: "RELATION",
32
+ list_of_unit: "LISTOFUNITS",
33
+ det_classification: "DETCLASSIFICATION",
30
34
  }.freeze
31
35
 
32
36
  META_CLASS_BY_LEGACY_TYPE = Opencdd::MetaClasses::TYPE_BY_META_CLASS.invert.freeze
@@ -147,7 +147,11 @@ module Opencdd
147
147
  rows = emitter.emit(built.sheet, built.entities)
148
148
  workbook.add_worksheet(name: built.sheet.name) do |ws|
149
149
  rows.each do |row|
150
- emitted = ws.add_row(row)
150
+ # All property values are strings per IEC 61360 wire format.
151
+ # Force string cell type so values like "001" survive round-trip
152
+ # (otherwise caxlsx auto-types numeric-looking strings and roo
153
+ # reads them back as integers, breaking semantically_equal?).
154
+ emitted = ws.add_row(row, types: :string)
151
155
  emitted.hidden = true if row_hidden?(row, hidden_directives)
152
156
  end
153
157
  end
@@ -7,6 +7,7 @@ module Opencdd
7
7
 
8
8
  autoload :Metadata, "opencdd/parcel/metadata"
9
9
  autoload :SheetSchema, "opencdd/parcel/sheet_schema"
10
+ autoload :LanguageAliases, "opencdd/parcel/language_aliases"
10
11
  autoload :Sheet, "opencdd/parcel/sheet"
11
12
  autoload :Workbook, "opencdd/parcel/workbook"
12
13
  autoload :WorkbookReader, "opencdd/parcel/workbook_reader"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Opencdd
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.3"
5
5
  end
data/lib/opencdd.rb CHANGED
@@ -29,6 +29,7 @@ module Opencdd
29
29
  autoload :Relation, "opencdd/relation"
30
30
  autoload :ViewControl, "opencdd/view_control"
31
31
  autoload :ListUnit, "opencdd/list_unit"
32
+ autoload :DetClassification, "opencdd/det_classification"
32
33
 
33
34
  autoload :Database, "opencdd/database"
34
35
  autoload :EffectiveProperties, "opencdd/effective_properties"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opencdd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenCDD contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-24 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: roo
@@ -236,6 +236,7 @@ files:
236
236
  - lib/opencdd/database/parcel_integration.rb
237
237
  - lib/opencdd/database/persistence.rb
238
238
  - lib/opencdd/database/queries.rb
239
+ - lib/opencdd/det_classification.rb
239
240
  - lib/opencdd/effective_properties.rb
240
241
  - lib/opencdd/entity.rb
241
242
  - lib/opencdd/entity/field_reader.rb
@@ -263,6 +264,7 @@ files:
263
264
  - lib/opencdd/parcel/csv_writer.rb
264
265
  - lib/opencdd/parcel/entity_manifest.rb
265
266
  - lib/opencdd/parcel/flat_dir_reader.rb
267
+ - lib/opencdd/parcel/language_aliases.rb
266
268
  - lib/opencdd/parcel/layout_detector.rb
267
269
  - lib/opencdd/parcel/metadata.rb
268
270
  - lib/opencdd/parcel/referenced_irdis.rb