isq 0.1.0 → 0.1.1

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: 44823d1e9f21796b3c62de48a24105fa0b5c906e6990ddd75a1143052363f11b
4
- data.tar.gz: 2671bb4938158df5c272b790000da4c53e134c9a54d4bd6b553aa57f3e4d6ba4
3
+ metadata.gz: a0dc68df904baa524f066fa29fa52882f1ac1f1b9be557aa1ef3118affeda590
4
+ data.tar.gz: efdf842000e1a325713c1558a0b6e37dede13c5da796bcb26d28425282f4a57a
5
5
  SHA512:
6
- metadata.gz: 29c22ea5b84c9bce2c796164001e708a7fee5967acc0954c9042dbebda36e8f91d4a02d2badad8d6955bd5f66e9ace56600dec1b355d294ed17e34623a529e1c
7
- data.tar.gz: 5812745292b217fc5ea1acb9b8c802bddfc1f315ea2a748714d5064cbc3698df0267a7a1f18bad52e8999cc11fee38233778f7741f5e3fdc8c9d8b5ad138b81e
6
+ metadata.gz: 18ba08c24d8d9205810c2e113fae80340f138f3e4683a9a6e63354c68719fb451ccb2f0bd194bf4f221c3bc8865778fa5c17c2b6b8c5e6b530311101a15bb717
7
+ data.tar.gz: 38cc4b9c2cb152d1f1bad3911d8c3d9a757e340d74863233949d5270f0dc90db7860bf31c715d456fbf3da836aa1608b967233467e05b9db72edfea924bf6905
data/exe/isq ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "isq/cli"
5
+
6
+ Isq::Cli.start(ARGV)
data/lib/isq/cli.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "isq"
4
+ require "thor"
5
+
6
+ module Isq
7
+ class Cli < Thor
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ desc "export", "Generate TTL and JSON-LD exports from YAML source data"
13
+ method_option :dataset_dir,
14
+ aliases: "-d",
15
+ type: :string,
16
+ desc: "YAML dataset directory"
17
+ method_option :export_dir,
18
+ aliases: "-o",
19
+ type: :string,
20
+ desc: "Output directory"
21
+ method_option :format,
22
+ aliases: "-f",
23
+ type: :string,
24
+ enum: %w[ttl jsonld both],
25
+ default: "both",
26
+ desc: "Output format: ttl, jsonld, or both"
27
+ def export
28
+ dataset_dir = options[:dataset_dir] || ENV.fetch("ISQ_DATASET_DIR", default_dataset_dir)
29
+ export_dir = options[:export_dir] || ENV.fetch("ISQ_EXPORT_DIR", default_export_dir)
30
+
31
+ dataset = Isq::Dataset.load(dataset_dir)
32
+ Isq::Export.new(dataset: dataset, export_dir: export_dir,
33
+ format: options[:format].to_sym).run
34
+
35
+ say "Generated exports in #{export_dir}:"
36
+ say " #{dataset.total_count} entries across #{dataset.part_keys.length} parts"
37
+ end
38
+
39
+ private
40
+
41
+ def default_dataset_dir
42
+ File.expand_path("../../../iso-iec-80000/sources/dataset", __dir__)
43
+ end
44
+
45
+ def default_export_dir
46
+ File.expand_path("../../../browser/public/exports", __dir__)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isq
4
+ class Dataset
5
+ attr_reader :entries, :math_entries, :part_keys
6
+
7
+ def initialize(entries: [], math_entries: [])
8
+ @entries = Array(entries)
9
+ @math_entries = Array(math_entries)
10
+ index!
11
+ end
12
+
13
+ def self.load(dataset_dir)
14
+ entries = load_yaml_file(dataset_dir, "quantities.yaml") { |yaml| Isq::Quantity.from_yaml(yaml) }
15
+ math_entries = load_yaml_file(dataset_dir, "math.yaml") { |yaml| Isq::MathConcept.from_yaml(yaml) }
16
+
17
+ new(entries: entries, math_entries: math_entries)
18
+ end
19
+
20
+ def entries_for_part(part_key)
21
+ by_part[part_key] || []
22
+ end
23
+
24
+ def unique_units_for_part(part_key)
25
+ part_entries = entries_for_part(part_key)
26
+ seen = Set.new
27
+ part_entries.each_with_object([]) do |entry, acc|
28
+ next unless entry.is_a?(Isq::Quantity)
29
+ next unless entry.has_unit
30
+
31
+ Array(entry.has_unit).each do |unit_ref|
32
+ next if seen.include?(unit_ref)
33
+
34
+ seen << unit_ref
35
+ acc << unit_ref
36
+ end
37
+ end
38
+ end
39
+
40
+ def total_count
41
+ @entries.length + @math_entries.length
42
+ end
43
+
44
+ def counts_by_part
45
+ by_part.transform_values(&:length)
46
+ end
47
+
48
+ private
49
+
50
+ def by_part
51
+ @by_part
52
+ end
53
+
54
+ def index!
55
+ all_entries = @entries + @math_entries
56
+ @by_part = all_entries.group_by(&:part)
57
+ @part_keys = @by_part.keys.sort
58
+ end
59
+
60
+ def self.load_yaml_file(dir, filename)
61
+ path = File.join(dir, filename)
62
+ return [] unless File.exist?(path)
63
+
64
+ yaml = File.read(path)
65
+ yield yaml
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isq
4
+ class Designation < TermInstance
5
+ attribute :index_as, :string, collection: true
6
+ end
7
+ end
data/lib/isq/export.rb ADDED
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+
6
+ module Isq
7
+ class Export
8
+ def initialize(dataset:, export_dir:, format: :both)
9
+ @dataset = dataset
10
+ @export_dir = export_dir
11
+ @format = format
12
+ end
13
+
14
+ def run
15
+ prepare_output_dir
16
+ write_per_part
17
+ write_bulk
18
+ write_manifest
19
+ end
20
+
21
+ private
22
+
23
+ def prepare_output_dir
24
+ FileUtils.rm_rf(@export_dir)
25
+ FileUtils.mkdir_p(@export_dir)
26
+ end
27
+
28
+ def write_per_part
29
+ @dataset.part_keys.each do |part_key|
30
+ part_dir = File.join(@export_dir, "part-#{part_key}")
31
+ FileUtils.mkdir_p(part_dir)
32
+
33
+ part_doc = Isq::PartDocument.for_part(part_key)
34
+ entries = @dataset.entries_for_part(part_key)
35
+ units = build_unit_instances(@dataset.unique_units_for_part(part_key), entries)
36
+
37
+ if write_turtle?
38
+ part_ttl = compose_part_turtle(part_doc, units, entries)
39
+ File.write(File.join(part_dir, "index.ttl"), part_ttl)
40
+ end
41
+
42
+ if write_jsonld?
43
+ part_jsonld = compose_part_jsonld(part_doc, units, entries)
44
+ File.write(File.join(part_dir, "index.jsonld"), part_jsonld)
45
+ end
46
+
47
+ write_per_entry_files(part_dir, entries)
48
+ end
49
+ end
50
+
51
+ def write_per_entry_files(part_dir, entries)
52
+ entries.each do |entry|
53
+ if write_turtle?
54
+ File.write(File.join(part_dir, "#{entry.id}.ttl"), entry.to_turtle)
55
+ end
56
+
57
+ if write_jsonld?
58
+ File.write(File.join(part_dir, "#{entry.id}.jsonld"), entry.to_jsonld)
59
+ end
60
+ end
61
+ end
62
+
63
+ def write_bulk
64
+ if write_turtle?
65
+ all_turtle = @dataset.part_keys.map do |part_key|
66
+ part_doc = Isq::PartDocument.for_part(part_key)
67
+ entries = @dataset.entries_for_part(part_key)
68
+ units = build_unit_instances(@dataset.unique_units_for_part(part_key), entries)
69
+ compose_part_turtle(part_doc, units, entries)
70
+ end.join("\n")
71
+ File.write(File.join(@export_dir, "iso80000-all.ttl"), all_turtle)
72
+ end
73
+
74
+ if write_jsonld?
75
+ all_jsonld = @dataset.part_keys.map do |part_key|
76
+ part_doc = Isq::PartDocument.for_part(part_key)
77
+ entries = @dataset.entries_for_part(part_key)
78
+ units = build_unit_instances(@dataset.unique_units_for_part(part_key), entries)
79
+ compose_part_jsonld(part_doc, units, entries)
80
+ end.join("\n")
81
+ File.write(File.join(@export_dir, "iso80000-all.jsonld"), all_jsonld)
82
+ end
83
+ end
84
+
85
+ def write_manifest
86
+ manifest = {
87
+ generated: Time.now.utc.iso8601,
88
+ total_entries: @dataset.total_count,
89
+ parts: @dataset.counts_by_part,
90
+ namespaces: namespace_registry,
91
+ }
92
+ File.write(File.join(@export_dir, "manifest.json"), JSON.pretty_generate(manifest))
93
+ end
94
+
95
+ def build_unit_instances(unit_refs, entries)
96
+ unit_map = collect_unit_data(entries)
97
+ unit_refs.filter_map do |ref|
98
+ data = unit_map[ref]
99
+ next unless data
100
+
101
+ Isq::Unit.new(
102
+ id: ref.sub("isoiec80000:", ""),
103
+ pref_label: data[:name],
104
+ notation: data[:symbols],
105
+ bindingness_type: "smart:normative",
106
+ )
107
+ end
108
+ end
109
+
110
+ def collect_unit_data(entries)
111
+ entries.each_with_object({}) do |entry, map|
112
+ next unless entry.is_a?(Isq::Quantity)
113
+ next unless entry.unit_data
114
+
115
+ map.merge!(entry.unit_data) { |_key, old, _new| old }
116
+ end
117
+ end
118
+
119
+ def compose_part_turtle(part_doc, units, entries)
120
+ lines = []
121
+ lines << part_doc.to_turtle
122
+ units.each { |u| lines << u.to_turtle }
123
+ entries.each { |e| lines << e.to_turtle }
124
+ lines.join("\n")
125
+ end
126
+
127
+ def compose_part_jsonld(part_doc, units, entries)
128
+ lines = []
129
+ lines << part_doc.to_jsonld
130
+ units.each { |u| lines << u.to_jsonld }
131
+ entries.each { |e| lines << e.to_jsonld }
132
+ lines.join("\n")
133
+ end
134
+
135
+ def namespace_registry
136
+ {
137
+ smart: "https://w3id.org/standards/smart/ontologies/core/",
138
+ isoiec80000: "https://w3id.org/standards/isoiec80000/ontologies/core/",
139
+ dcterms: "http://purl.org/dc/terms/",
140
+ skos: "http://www.w3.org/2004/02/skos/core#",
141
+ skosxl: "http://www.w3.org/2008/05/skos-xl#",
142
+ }
143
+ end
144
+
145
+ def write_turtle?
146
+ @format == :both || @format == :ttl
147
+ end
148
+
149
+ def write_jsonld?
150
+ @format == :both || @format == :jsonld
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/rdf/language_tagged"
4
+
5
+ module Isq
6
+ class LangString < String
7
+ include Lutaml::Rdf::LanguageTagged
8
+
9
+ attr_reader :language
10
+
11
+ def initialize(text, language: "en")
12
+ super(text.to_s)
13
+ @language = language
14
+ end
15
+ end
16
+ end
@@ -2,21 +2,49 @@
2
2
 
3
3
  module Isq
4
4
  class MathConcept < SduSmart::TermEntry
5
+ include Isq::YamlAdapters
6
+
5
7
  attribute :identifier, :string
6
8
  attribute :pref_label, :string
7
- attribute :notation, :string, collection: true
9
+ attribute :notation, :string, collection: true, initialize_empty: true
8
10
  attribute :definition, :string
9
11
  attribute :note, :string
12
+ attribute :part, :string
13
+ attribute :edition, :string
14
+ attribute :designations, Isq::Designation, collection: true, initialize_empty: true
15
+ attribute :symbols, Isq::SymbolTerm, collection: true, initialize_empty: true
16
+
17
+ def definition=(value)
18
+ value_set_for(:definition)
19
+ @definition = value
20
+ end
21
+
22
+ def note=(value)
23
+ value_set_for(:note)
24
+ @note = value
25
+ end
26
+
27
+ yaml do
28
+ map "id", to: :id
29
+ map "num", to: :identifier
30
+ map "part", to: :part
31
+ map "edition", to: :edition
32
+ map "def", with: { from: :definition_from_yaml, to: :definition_to_yaml }
33
+ map "remarks", with: { from: :note_from_yaml, to: :note_to_yaml }
34
+ map "designations", with: { from: :designations_from_yaml, to: :designations_to_yaml }
35
+ map "symbols", with: { from: :symbols_from_yaml, to: :symbols_to_yaml }
36
+ end
10
37
 
11
38
  rdf do
12
39
  namespace SduSmart::Rdf::Namespaces::IsoIec80000Namespace,
13
40
  SduSmart::Rdf::Namespaces::SmartNamespace,
14
41
  Lutaml::Rdf::Namespaces::DctermsNamespace,
15
- Lutaml::Rdf::Namespaces::SkosNamespace
42
+ Lutaml::Rdf::Namespaces::SkosNamespace,
43
+ SduSmart::Rdf::Namespaces::SkosXlNamespace
16
44
 
17
45
  subject { |m| "https://w3id.org/standards/isoiec80000/ontologies/core/#{m.id}" }
18
46
 
19
- type "isoiec80000:MathConcept"
47
+ type ["isoiec80000:MathConcept", "smart:TermEntry"]
20
48
 
21
49
  predicate :identifier,
22
50
  namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
@@ -32,11 +60,13 @@ module Isq
32
60
 
33
61
  predicate :definition,
34
62
  namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
35
- to: :definition
63
+ to: :definition,
64
+ lang_tagged: true
36
65
 
37
66
  predicate :note,
38
67
  namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
39
- to: :note
68
+ to: :note,
69
+ lang_tagged: true
40
70
 
41
71
  predicate :hasBindingnessType,
42
72
  namespace: SduSmart::Rdf::Namespaces::SmartNamespace,
@@ -45,6 +75,13 @@ module Isq
45
75
  predicate :isPartOf,
46
76
  namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
47
77
  to: :is_part_of
78
+
79
+ members :designations,
80
+ predicate_name: :prefLabel,
81
+ namespace: SduSmart::Rdf::Namespaces::SkosXlNamespace
82
+ members :symbols,
83
+ predicate_name: :altLabel,
84
+ namespace: SduSmart::Rdf::Namespaces::SkosXlNamespace
48
85
  end
49
86
  end
50
87
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isq
4
+ class PartDocument < SduSmart::PublicationDocument
5
+ PART_TITLES = {
6
+ "2" => "Mathematics",
7
+ "3" => "Space and Time",
8
+ "4" => "Mechanics",
9
+ "5" => "Thermodynamics",
10
+ "6" => "Electromagnetism",
11
+ "7" => "Light and Radiation",
12
+ "8" => "Acoustics",
13
+ "9" => "Physical Chemistry and Molecular Physics",
14
+ "10" => "Atomic and Nuclear Physics",
15
+ "11" => "Characteristic Numbers",
16
+ "12" => "Condensed Matter Physics",
17
+ "13" => "Information Science",
18
+ }.freeze
19
+
20
+ attribute :part_number, :string
21
+ attribute :title, :string
22
+ attribute :bindingness_type, :string
23
+
24
+ rdf do
25
+ namespace SduSmart::Rdf::Namespaces::SmartNamespace,
26
+ Lutaml::Rdf::Namespaces::DctermsNamespace
27
+
28
+ subject { |m| "https://w3id.org/standards/isoiec80000/ontologies/core/part-#{m.part_number}" }
29
+
30
+ type "smart:PublicationDocument"
31
+
32
+ predicate :title,
33
+ namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
34
+ to: :title
35
+
36
+ predicate :identifier,
37
+ namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
38
+ to: :part_number
39
+
40
+ predicate :hasPublicationType,
41
+ namespace: SduSmart::Rdf::Namespaces::SmartNamespace,
42
+ to: :publication_type
43
+
44
+ predicate :hasBindingnessType,
45
+ namespace: SduSmart::Rdf::Namespaces::SmartNamespace,
46
+ to: :bindingness_type
47
+ end
48
+
49
+ def self.for_part(part_number)
50
+ new(
51
+ id: "part-#{part_number}",
52
+ part_number: "ISO 80000-#{part_number}",
53
+ title: PART_TITLES.fetch(part_number, "Part #{part_number}"),
54
+ publication_type: "smart:internationalStandard",
55
+ bindingness_type: "smart:normative",
56
+ )
57
+ end
58
+
59
+ def self.all_parts
60
+ PART_TITLES.keys.map { |p| for_part(p) }
61
+ end
62
+ end
63
+ end
data/lib/isq/quantity.rb CHANGED
@@ -2,22 +2,53 @@
2
2
 
3
3
  module Isq
4
4
  class Quantity < SduSmart::TermEntry
5
+ include Isq::YamlAdapters
6
+
5
7
  attribute :identifier, :string
6
8
  attribute :pref_label, :string
7
- attribute :notation, :string, collection: true
9
+ attribute :notation, :string, collection: true, initialize_empty: true
8
10
  attribute :definition, :string
9
11
  attribute :note, :string
10
- attribute :has_unit, :string, collection: true
12
+ attribute :has_unit, :string, collection: true, initialize_empty: true
13
+ attribute :part, :string
14
+ attribute :edition, :string
15
+ attribute :designations, Isq::Designation, collection: true, initialize_empty: true
16
+ attribute :symbols, Isq::SymbolTerm, collection: true, initialize_empty: true
17
+
18
+ attr_accessor :unit_data
19
+
20
+ def definition=(value)
21
+ value_set_for(:definition)
22
+ @definition = value
23
+ end
24
+
25
+ def note=(value)
26
+ value_set_for(:note)
27
+ @note = value
28
+ end
29
+
30
+ yaml do
31
+ map "id", to: :id
32
+ map "num", to: :identifier
33
+ map "part", to: :part
34
+ map "edition", to: :edition
35
+ map "def", with: { from: :definition_from_yaml, to: :definition_to_yaml }
36
+ map "remarks", with: { from: :note_from_yaml, to: :note_to_yaml }
37
+ map "designations", with: { from: :designations_from_yaml, to: :designations_to_yaml }
38
+ map "symbols", with: { from: :symbols_from_yaml, to: :symbols_to_yaml }
39
+ map "units", with: { from: :units_from_yaml, to: :units_to_yaml }
40
+ end
11
41
 
12
42
  rdf do
13
43
  namespace SduSmart::Rdf::Namespaces::IsoIec80000Namespace,
14
44
  SduSmart::Rdf::Namespaces::SmartNamespace,
15
45
  Lutaml::Rdf::Namespaces::DctermsNamespace,
16
- Lutaml::Rdf::Namespaces::SkosNamespace
46
+ Lutaml::Rdf::Namespaces::SkosNamespace,
47
+ SduSmart::Rdf::Namespaces::SkosXlNamespace
17
48
 
18
49
  subject { |m| "https://w3id.org/standards/isoiec80000/ontologies/core/#{m.id}" }
19
50
 
20
- type "isoiec80000:Quantity"
51
+ type ["isoiec80000:Quantity", "smart:TermEntry"]
21
52
 
22
53
  predicate :identifier,
23
54
  namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
@@ -33,15 +64,18 @@ module Isq
33
64
 
34
65
  predicate :definition,
35
66
  namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
36
- to: :definition
67
+ to: :definition,
68
+ lang_tagged: true
37
69
 
38
70
  predicate :note,
39
71
  namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
40
- to: :note
72
+ to: :note,
73
+ lang_tagged: true
41
74
 
42
75
  predicate :hasUnit,
43
76
  namespace: SduSmart::Rdf::Namespaces::IsoIec80000Namespace,
44
- to: :has_unit
77
+ to: :has_unit,
78
+ uri_reference: true
45
79
 
46
80
  predicate :hasBindingnessType,
47
81
  namespace: SduSmart::Rdf::Namespaces::SmartNamespace,
@@ -50,6 +84,42 @@ module Isq
50
84
  predicate :isPartOf,
51
85
  namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
52
86
  to: :is_part_of
87
+
88
+ members :designations,
89
+ predicate_name: :prefLabel,
90
+ namespace: SduSmart::Rdf::Namespaces::SkosXlNamespace
91
+ members :symbols,
92
+ predicate_name: :altLabel,
93
+ namespace: SduSmart::Rdf::Namespaces::SkosXlNamespace
94
+ end
95
+
96
+ def units_from_yaml(model, value)
97
+ unless value.is_a?(Array)
98
+ model.has_unit = []
99
+ model.unit_data = {}
100
+ return []
101
+ end
102
+
103
+ data = {}
104
+ refs = value.map do |u|
105
+ sym = Array(u["symbol"]).first
106
+ ref = if sym
107
+ "isoiec80000:unit-#{sym}"
108
+ else
109
+ name = u["en"]&.downcase&.gsub(/\s+/, "-")
110
+ "isoiec80000:unit-#{name}" if name
111
+ end
112
+ data[ref] = { name: u["en"], symbols: Array(u["symbol"]) } if ref
113
+ ref
114
+ end.compact
115
+
116
+ model.has_unit = refs
117
+ model.unit_data = data
118
+ refs
119
+ end
120
+
121
+ def units_to_yaml(model, doc)
122
+ # Unit YAML round-trip requires Dataset lookup
53
123
  end
54
124
  end
55
125
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isq
4
+ class SymbolTerm < TermInstance
5
+ end
6
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isq
4
+ class TermInstance < SduSmart::Term
5
+ attribute :text, :string
6
+ attribute :lang, :string, default: "en"
7
+
8
+ def text=(value)
9
+ value_set_for(:text)
10
+ @text = value
11
+ end
12
+
13
+ rdf do
14
+ namespace SduSmart::Rdf::Namespaces::SkosXlNamespace,
15
+ SduSmart::Rdf::Namespaces::SmartNamespace
16
+
17
+ subject { |m| "https://w3id.org/standards/isoiec80000/ontologies/core/#{m.id}" }
18
+
19
+ type ["skosxl:Label", "smart:Term"]
20
+
21
+ predicate :literalForm,
22
+ namespace: SduSmart::Rdf::Namespaces::SkosXlNamespace,
23
+ to: :text,
24
+ lang_tagged: true
25
+
26
+ predicate :hasTermFormType,
27
+ namespace: SduSmart::Rdf::Namespaces::SmartNamespace,
28
+ to: :term_form_type,
29
+ uri_reference: true
30
+ end
31
+ end
32
+ end
data/lib/isq/unit.rb CHANGED
@@ -3,7 +3,12 @@
3
3
  module Isq
4
4
  class Unit < SduSmart::TermEntry
5
5
  attribute :pref_label, :string
6
- attribute :notation, :string, collection: true
6
+ attribute :notation, :string, collection: true, initialize_empty: true
7
+
8
+ yaml do
9
+ map "en", to: :pref_label
10
+ map "symbol", to: :notation
11
+ end
7
12
 
8
13
  rdf do
9
14
  namespace SduSmart::Rdf::Namespaces::IsoIec80000Namespace,
@@ -13,7 +18,7 @@ module Isq
13
18
 
14
19
  subject { |m| "https://w3id.org/standards/isoiec80000/ontologies/core/#{m.id}" }
15
20
 
16
- type "isoiec80000:Unit"
21
+ type ["isoiec80000:Unit", "smart:TermEntry"]
17
22
 
18
23
  predicate :prefLabel,
19
24
  namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
data/lib/isq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isq
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Isq
4
+ module YamlAdapters
5
+ def definition_from_yaml(model, value)
6
+ return unless value.is_a?(Hash)
7
+
8
+ lang = value.keys.first.to_s
9
+ text = value.values.first
10
+ model.definition = Isq::LangString.new(text.to_s, language: lang)
11
+ end
12
+
13
+ def definition_to_yaml(model, doc)
14
+ return unless model.definition
15
+
16
+ lang = model.definition.language || "en"
17
+ doc["def"] = { lang => model.definition.to_s }
18
+ end
19
+
20
+ def note_from_yaml(model, value)
21
+ return unless value.is_a?(Hash)
22
+
23
+ lang = value.keys.first.to_s
24
+ text = value.values.first
25
+ model.note = Isq::LangString.new(text.to_s, language: lang)
26
+ end
27
+
28
+ def note_to_yaml(model, doc)
29
+ return unless model.note
30
+
31
+ lang = model.note.language || "en"
32
+ doc["remarks"] = { lang => model.note.to_s }
33
+ end
34
+
35
+ def designations_from_yaml(model, value)
36
+ unless value.is_a?(Array)
37
+ model.designations = []
38
+ return []
39
+ end
40
+
41
+ designations = value.each_with_index.map do |entry, i|
42
+ lang_data = entry["designation"]
43
+ next unless lang_data.is_a?(Hash)
44
+
45
+ lang = lang_data.keys.first.to_s
46
+ text_data = lang_data[lang] || lang_data[lang.to_sym]
47
+ next unless text_data.is_a?(Hash)
48
+
49
+ d = Isq::Designation.new(
50
+ id: "term-#{model.id}-#{i}",
51
+ lang: lang,
52
+ term_form_type: "smart:fullForm",
53
+ index_as: Array(text_data["index_as"]),
54
+ )
55
+ d.text = Isq::LangString.new(text_data["text"].to_s, language: lang)
56
+ d
57
+ end.compact
58
+
59
+ model.designations = designations
60
+ model.pref_label = designations.first&.text
61
+ designations
62
+ end
63
+
64
+ def designations_to_yaml(model, doc)
65
+ doc["designations"] = model.designations.map do |d|
66
+ { "designation" => { d.lang => { "text" => d.text.to_s, "index_as" => Array(d.index_as) } } }
67
+ end
68
+ end
69
+
70
+ def symbols_from_yaml(model, value)
71
+ unless value.is_a?(Array)
72
+ model.symbols = []
73
+ model.notation = []
74
+ return []
75
+ end
76
+
77
+ symbols = value.each_with_index.map do |sym, i|
78
+ s = Isq::SymbolTerm.new(
79
+ id: "sym-#{model.id}-#{i}",
80
+ lang: "en",
81
+ term_form_type: "smart:symbol",
82
+ )
83
+ s.text = Isq::LangString.new(sym.to_s, language: "en")
84
+ s
85
+ end
86
+
87
+ model.symbols = symbols
88
+ model.notation = symbols.map { |s| s.text.to_s }
89
+ symbols
90
+ end
91
+
92
+ def symbols_to_yaml(model, doc)
93
+ doc["symbols"] = model.symbols.map { |s| s.text.to_s }
94
+ end
95
+ end
96
+ end
data/lib/isq.rb CHANGED
@@ -3,8 +3,16 @@
3
3
  require "sdu_smart"
4
4
 
5
5
  module Isq
6
+ autoload :LangString, "#{__dir__}/isq/lang_string"
7
+ autoload :YamlAdapters, "#{__dir__}/isq/yaml_adapters"
8
+ autoload :TermInstance, "#{__dir__}/isq/term_instance"
9
+ autoload :Designation, "#{__dir__}/isq/designation"
10
+ autoload :SymbolTerm, "#{__dir__}/isq/symbol_term"
6
11
  autoload :Quantity, "#{__dir__}/isq/quantity"
7
12
  autoload :Unit, "#{__dir__}/isq/unit"
8
13
  autoload :MathConcept, "#{__dir__}/isq/math_concept"
14
+ autoload :PartDocument, "#{__dir__}/isq/part_document"
15
+ autoload :Dataset, "#{__dir__}/isq/dataset"
16
+ autoload :Export, "#{__dir__}/isq/export"
9
17
  autoload :VERSION, "#{__dir__}/isq/version"
10
18
  end
@@ -1,309 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
- require "json"
5
- require "fileutils"
6
-
7
3
  namespace :export do
8
- ROOT = File.join(__dir__, "..", "..", "..")
9
- EXPORT_DIR = ENV.fetch("ISQ_EXPORT_DIR", File.join(ROOT, "browser", "public", "exports"))
10
- DATASET_DIR = ENV.fetch("ISQ_DATASET_DIR", File.join(ROOT, "iso-iec-80000", "sources", "dataset"))
11
-
12
- PART_TITLES = {
13
- "3" => "Space and Time", "4" => "Mechanics", "5" => "Thermodynamics",
14
- "6" => "Electromagnetism", "7" => "Light and Radiation", "8" => "Acoustics",
15
- "9" => "Physical Chemistry", "10" => "Atomic and Nuclear", "11" => "Characteristic Numbers",
16
- "12" => "Condensed Matter", "13" => "Information Science", "2" => "Mathematics",
17
- }.freeze
18
-
19
- SMART = "https://w3id.org/standards/smart/ontologies/core/"
20
- ISO = "https://w3id.org/standards/isoiec80000/ontologies/core/"
21
- DCTERMS = "http://purl.org/dc/terms/"
22
- SKOS = "http://www.w3.org/2004/02/skos/core#"
23
- SKOSXL = "http://www.w3.org/2008/05/skos-xl#"
24
-
25
- JSONLD_CONTEXT = {
26
- "@context" => {
27
- "smart" => SMART,
28
- "isoiec80000" => ISO,
29
- "dcterms" => DCTERMS,
30
- "skos" => SKOS,
31
- "skosxl" => SKOSXL,
32
- "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
33
- "rdfs" => "http://www.w3.org/2000/01/rdf-schema#",
34
- "owl" => "http://www.w3.org/2002/07/owl#",
35
- "xsd" => "http://www.w3.org/2001/XMLSchema#",
36
- },
37
- }.freeze
38
-
39
- def load_entries
40
- quantities = YAML.load_file(File.join(DATASET_DIR, "quantities.yaml"))
41
- math = YAML.load_file(File.join(DATASET_DIR, "math.yaml"))
42
- quantities + math
43
- end
44
-
45
- def math?(entry)
46
- entry["part"].to_s.start_with?("2")
47
- end
48
-
49
- def entry_rdf_type(entry)
50
- math?(entry) ? "isoiec80000:MathConcept" : "isoiec80000:Quantity"
51
- end
52
-
53
- def unit_uri(unit)
54
- sym = unit["symbol"]&.first
55
- sym ? "isoiec80000:unit-#{sym}" : "isoiec80000:unit-#{unit['en']&.downcase&.gsub(/\s+/, '-')}"
56
- end
57
-
58
- def escape_literal(s)
59
- s.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
60
- end
61
-
62
- def generate_turtle(entry)
63
- id = entry["id"]
64
- rdf_type = entry_rdf_type(entry)
65
- lines = []
66
-
67
- # Term instances for designations
68
- entry["designations"]&.each_with_index do |des, i|
69
- tid = "term-#{id}-#{i}"
70
- text = des.dig("designation", "en", "text")
71
- next unless text
72
-
73
- lines << "isoiec80000:#{tid} a smart:Term, skosxl:Label ;"
74
- lines << " skosxl:literalForm \"#{escape_literal(text)}\"@en ;"
75
- lines << " smart:hasTermFormType smart:fullForm ."
76
- lines << ""
77
- end
78
-
79
- # Term instances for symbols
80
- entry["symbols"]&.each_with_index do |sym, i|
81
- tid = "sym-#{id}-#{i}"
82
- lines << "isoiec80000:#{tid} a smart:Term, skosxl:Label ;"
83
- lines << " skosxl:literalForm \"#{escape_literal(sym)}\"@en ;"
84
- lines << " smart:hasTermFormType smart:symbol ."
85
- lines << ""
86
- end
87
-
88
- # Main entry
89
- lines << "isoiec80000:#{id} a #{rdf_type}, smart:TermEntry ;"
90
- lines << " dcterms:identifier \"#{entry['num']}\" ;"
91
-
92
- # skosxl:prefLabel for first designation
93
- if entry["designations"]&.any?
94
- lines << " skosxl:prefLabel isoiec80000:term-#{id}-0 ;"
95
- end
96
-
97
- # skosxl:altLabel for remaining designations
98
- entry["designations"]&.each_with_index do |_des, i|
99
- next if i == 0
100
- lines << " skosxl:altLabel isoiec80000:term-#{id}-#{i} ;"
101
- end
102
-
103
- # skosxl:altLabel for symbol terms
104
- entry["symbols"]&.each_with_index do |_sym, i|
105
- lines << " skosxl:altLabel isoiec80000:sym-#{id}-#{i} ;"
106
- end
107
-
108
- if entry["def"]&.dig("en")
109
- lines << " skos:definition \"\"\"#{escape_literal(entry['def']['en'])}\"\"\"@en ;"
110
- end
111
-
112
- if entry["remarks"]&.dig("en")
113
- lines << " skos:note \"\"\"#{escape_literal(entry['remarks']['en'])}\"\"\"@en ;"
114
- end
115
-
116
- if entry["units"]&.any?
117
- for unit in entry["units"]
118
- lines << " isoiec80000:hasUnit #{unit_uri(unit)} ;"
119
- end
120
- end
121
-
122
- lines << " smart:hasBindingnessType smart:normative ;"
123
- lines << " dcterms:isPartOf isoiec80000:part-#{entry['part']} ."
124
-
125
- lines.join("\n")
126
- end
127
-
128
- def generate_unit_turtle(unit)
129
- uri = unit_uri(unit)
130
- name = unit["en"]
131
- lines = ["#{uri} a isoiec80000:Unit, smart:TermEntry ;"]
132
- lines << " skos:prefLabel \"#{name}\"@en ;"
133
- if unit["symbol"]&.any?
134
- for sym in unit["symbol"]
135
- lines << " skos:notation \"#{sym}\" ;"
136
- end
137
- end
138
- lines << " smart:hasBindingnessType smart:normative ."
139
- lines.join("\n")
140
- end
141
-
142
- def entry_jsonld(entry)
143
- id = entry["id"]
144
- part = entry["part"]
145
-
146
- # Term instances for designations
147
- terms = entry["designations"]&.each_with_index&.map do |des, i|
148
- text = des.dig("designation", "en", "text")
149
- next unless text
150
-
151
- {
152
- "@id" => "#{ISO}term-#{id}-#{i}",
153
- "@type" => ["smart:Term", "skosxl:Label"],
154
- "skosxl:literalForm" => { "@value" => text, "@language" => "en" },
155
- "smart:hasTermFormType" => { "@id" => "smart:fullForm" },
156
- }
157
- end&.compact || []
158
-
159
- # Term instances for symbols
160
- symbol_terms = entry["symbols"]&.each_with_index&.map do |sym, i|
161
- {
162
- "@id" => "#{ISO}sym-#{id}-#{i}",
163
- "@type" => ["smart:Term", "skosxl:Label"],
164
- "skosxl:literalForm" => { "@value" => sym, "@language" => "en" },
165
- "smart:hasTermFormType" => { "@id" => "smart:symbol" },
166
- }
167
- end || []
168
-
169
- all_terms = terms + symbol_terms
170
-
171
- entry_obj = {
172
- "@id" => "#{ISO}#{id}",
173
- "@type" => [entry_rdf_type(entry), "smart:TermEntry"],
174
- "dcterms:identifier" => entry["num"],
175
- }
176
-
177
- if terms.any?
178
- entry_obj["skosxl:prefLabel"] = { "@id" => "#{ISO}term-#{id}-0" }
179
- end
180
-
181
- alt_labels = []
182
- entry["designations"]&.each_with_index do |_des, i|
183
- next if i == 0
184
- alt_labels << { "@id" => "#{ISO}term-#{id}-#{i}" }
185
- end
186
- entry["symbols"]&.each_with_index do |_sym, i|
187
- alt_labels << { "@id" => "#{ISO}sym-#{id}-#{i}" }
188
- end
189
- entry_obj["skosxl:altLabel"] = alt_labels if alt_labels.any?
190
-
191
- if entry["def"]&.dig("en")
192
- entry_obj["skos:definition"] = { "@value" => entry["def"]["en"], "@language" => "en" }
193
- end
194
-
195
- if entry["remarks"]&.dig("en")
196
- entry_obj["skos:note"] = { "@value" => entry["remarks"]["en"], "@language" => "en" }
197
- end
198
-
199
- if entry["units"]&.any?
200
- entry_obj["isoiec80000:hasUnit"] = entry["units"].map { |u| { "@id" => unit_uri(u) } }
201
- end
202
-
203
- entry_obj["smart:hasBindingnessType"] = { "@id" => "smart:normative" }
204
- entry_obj["dcterms:isPartOf"] = { "@id" => "#{ISO}part-#{part}" }
205
-
206
- { "@graph" => all_terms + [entry_obj] }
207
- end
208
-
209
- def generate_part_turtle(part_key, part_entries)
210
- lines = [
211
- "@prefix isoiec80000: <#{ISO}> .",
212
- "@prefix smart: <#{SMART}> .",
213
- "@prefix dcterms: <#{DCTERMS}> .",
214
- "@prefix skos: <#{SKOS}> .",
215
- "@prefix skosxl: <#{SKOSXL}> .",
216
- "",
217
- ]
218
-
219
- # PublicationDocument instance
220
- lines << "isoiec80000:part-#{part_key} a smart:PublicationDocument ;"
221
- lines << " dcterms:title \"#{PART_TITLES[part_key] || "Part #{part_key}"}\"@en ;"
222
- lines << " dcterms:identifier \"ISO 80000-#{part_key}\" ;"
223
- lines << " smart:hasPublicationType smart:internationalStandard ;"
224
- lines << " smart:hasBindingnessType smart:normative ."
225
- lines << ""
226
-
227
- # Collect unique units
228
- units = part_entries.each_with_object([]) do |entry, acc|
229
- entry["units"]&.each do |u|
230
- key = unit_uri(u)
231
- acc << u unless acc.any? { |existing| unit_uri(existing) == key }
232
- end
233
- end
234
-
235
- units.each do |u|
236
- lines << generate_unit_turtle(u)
237
- lines << ""
238
- end
239
-
240
- part_entries.each do |entry|
241
- lines << generate_turtle(entry)
242
- lines << ""
243
- end
244
-
245
- lines.join("\n")
246
- end
247
-
248
4
  desc "Generate TTL and JSON-LD exports for all entries"
249
5
  task :all do
250
- require "sdu_smart"
251
-
252
- FileUtils.rm_rf(EXPORT_DIR)
253
- FileUtils.mkdir_p(EXPORT_DIR)
254
-
255
- entries = load_entries
256
- by_part = entries.group_by { |e| e["part"].to_s }
257
-
258
- all_turtle = []
259
- all_jsonld = []
260
-
261
- by_part.each do |part_key, part_entries|
262
- part_dir = File.join(EXPORT_DIR, "part-#{part_key}")
263
- FileUtils.mkdir_p(part_dir)
264
-
265
- # Per-part Turtle
266
- part_ttl = generate_part_turtle(part_key, part_entries)
267
- File.write(File.join(part_dir, "index.ttl"), part_ttl)
268
-
269
- # Per-part JSON-LD
270
- part_graphs = part_entries.map { |e| entry_jsonld(e) }
271
- part_jsonld = { **JSONLD_CONTEXT, "@graph" => part_graphs.flat_map { |g| g["@graph"] } }
272
- File.write(File.join(part_dir, "index.jsonld"), JSON.pretty_generate(part_jsonld))
273
-
274
- all_turtle << part_ttl
275
- all_jsonld.concat(part_graphs.flat_map { |g| g["@graph"] })
276
-
277
- # Per-entry files
278
- part_entries.each do |entry|
279
- base = entry["id"]
280
- entry_json = entry_jsonld(entry)
281
- File.write(File.join(part_dir, "#{base}.ttl"), generate_turtle(entry))
282
- File.write(File.join(part_dir, "#{base}.jsonld"), JSON.pretty_generate({ **JSONLD_CONTEXT, **entry_json }))
283
- end
284
- end
6
+ require "isq"
285
7
 
286
- # Full bulk exports
287
- File.write(File.join(EXPORT_DIR, "iso80000-all.ttl"), all_turtle.join("\n"))
288
- bulk_jsonld = { **JSONLD_CONTEXT, "@graph" => all_jsonld }
289
- File.write(File.join(EXPORT_DIR, "iso80000-all.jsonld"), JSON.pretty_generate(bulk_jsonld))
8
+ root = File.join(__dir__, "..", "..", "..")
9
+ dataset_dir = ENV.fetch("ISQ_DATASET_DIR", File.join(root, "iso-iec-80000", "sources", "dataset"))
10
+ export_dir = ENV.fetch("ISQ_EXPORT_DIR", File.join(root, "browser", "public", "exports"))
290
11
 
291
- # Manifest
292
- manifest = {
293
- generated: Time.now.utc.iso8601,
294
- total_entries: entries.length,
295
- parts: by_part.transform_values(&:length),
296
- namespaces: {
297
- smart: SMART,
298
- isoiec80000: ISO,
299
- dcterms: DCTERMS,
300
- skos: SKOS,
301
- skosxl: SKOSXL,
302
- },
303
- }
304
- File.write(File.join(EXPORT_DIR, "manifest.json"), JSON.pretty_generate(manifest))
12
+ dataset = Isq::Dataset.load(dataset_dir)
13
+ Isq::Export.new(dataset: dataset, export_dir: export_dir).run
305
14
 
306
- puts "Generated exports in #{EXPORT_DIR}:"
307
- puts " #{entries.length} entries across #{by_part.length} parts"
15
+ puts "Generated exports in #{export_dir}:"
16
+ puts " #{dataset.total_count} entries across #{dataset.part_keys.length} parts"
308
17
  end
309
18
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
- bindir: bin
8
+ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -37,20 +37,45 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 0.8.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: thor
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
40
54
  description: 'Ruby gem extending the SmartSDU Core Ontology with ISO/IEC 80000-specific
41
55
  domain classes: Quantity, Unit, and MathConcept. Includes a Rake task for generating
42
56
  per-part Turtle and JSON-LD exports from YAML source data.'
43
57
  email:
44
58
  - open.source@ribose.com
45
- executables: []
59
+ executables:
60
+ - isq
46
61
  extensions: []
47
62
  extra_rdoc_files: []
48
63
  files:
64
+ - exe/isq
49
65
  - lib/isq.rb
66
+ - lib/isq/cli.rb
67
+ - lib/isq/dataset.rb
68
+ - lib/isq/designation.rb
69
+ - lib/isq/export.rb
70
+ - lib/isq/lang_string.rb
50
71
  - lib/isq/math_concept.rb
72
+ - lib/isq/part_document.rb
51
73
  - lib/isq/quantity.rb
74
+ - lib/isq/symbol_term.rb
75
+ - lib/isq/term_instance.rb
52
76
  - lib/isq/unit.rb
53
77
  - lib/isq/version.rb
78
+ - lib/isq/yaml_adapters.rb
54
79
  - lib/tasks/export.rake
55
80
  homepage: https://github.com/metanorma/isq
56
81
  licenses: