opencdd 0.1.1 → 0.2.0

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: c1ea74c1b474208d733e1aedb3174ae3c088b6899f78e65a88b87cff180932b3
4
- data.tar.gz: ea3d59ec075cd72cb7182955936dc577fb6be2b53bacd45045a5409cd002df8a
3
+ metadata.gz: 74a134259cf9d336b0e401139d10fcda58d1df883ec52312e04eadd2c6016bb9
4
+ data.tar.gz: b2c6cd320d420f7258c4116c218e0de50850d9ebb1745f81e4b06c33113196ef
5
5
  SHA512:
6
- metadata.gz: f9efe7bf576ed24dc6f124a234ea12c3a2a8a70872e38e6e274550b3b77c78847434992f47250b18718cd3f5719103b2182afcacb517a2e5568ccce0588bcc12
7
- data.tar.gz: 32499575fc889ae2b278ed5d767b0ad75e91cdd339ccecea4cc02b008edab66b87c123963132e307ed932553cbfdded8c0e047773cbcbb113963b03585ce89eb
6
+ metadata.gz: 5147138e2581efbaf28bf224de9c56ed541040cdedec3569436c786419df35fc190a144982d78d53ca7d1ce4d4b7f73b7cd21847d5c3597e1987b0ade886db85
7
+ data.tar.gz: 9bd7ee2d030cfb9792177763875b02114aca94534754986fe17ba6c7b24dcf5cd47ad9361fd9f9533c8d3fcbdc4bcc296d4610b335809358d5a6d3ae9844f0f0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Database
5
+ # Finalization: link class hierarchy, property↔class links,
6
+ # value-list links, normalize reference collections, rebuild
7
+ # the symbol table.
8
+ module Finalization
9
+ def finalize!
10
+ normalize_reference_collections!
11
+ link_class_hierarchy!
12
+ link_property_classes!
13
+ link_value_lists!
14
+ rebuild_symbol_table!
15
+ self
16
+ end
17
+
18
+ def normalize_reference_collections!
19
+ each_reference_collection do |entity, pid, _raw, elements|
20
+ entity.write_property!(pid, Opencdd::StructuredValues.rejoin(elements))
21
+ end
22
+ self
23
+ end
24
+
25
+ def each_reference_collection
26
+ return enum_for(:each_reference_collection) unless block_given?
27
+ set_property_ids = Opencdd::PropertyIds::REGISTRY
28
+ .select { |_, e| e.value_kind == :set_of_refs }
29
+ .keys
30
+ entities.each do |entity|
31
+ set_property_ids.each do |pid|
32
+ raw = entity.properties[pid]
33
+ next if raw.nil? || raw.to_s.strip.empty?
34
+ elements = Opencdd::StructuredValues.unwrap_and_split(raw)
35
+ next if elements.empty?
36
+ yield(entity, pid, raw, elements)
37
+ end
38
+ end
39
+ end
40
+
41
+ def rebuild_symbol_table!
42
+ entities.each { |e| register_entity_symbols(e) }
43
+ end
44
+
45
+ private
46
+
47
+ def link_class_hierarchy!
48
+ classes.each do |klass|
49
+ next if klass.parent_irdi
50
+ parent_id = klass.parent_property_id || Opencdd::PropertyIds::MDC_P010
51
+ parent_raw = klass.properties[parent_id]
52
+ next if parent_raw.nil? || parent_raw.to_s.strip.empty?
53
+ target = resolve_reference(parent_raw)
54
+ next unless target
55
+ next unless target.is_a?(Opencdd::Klass)
56
+ klass.attach_parent_irdi(target.irdi)
57
+ target.add_child(klass)
58
+ end
59
+ end
60
+
61
+ def link_property_classes!
62
+ @class_by_property_irdi = Hash.new { |h, k| h[k] = [] }
63
+ link_property_classes_via_relations!
64
+ link_property_classes_via_definition_class!
65
+ end
66
+
67
+ def link_property_classes_via_relations!
68
+ relations.each do |r|
69
+ next unless r.predication? || r.function?
70
+ next if r.domain_irdis.empty?
71
+ next unless r.codomain_irdi
72
+ r.domain_irdis.each do |d|
73
+ next unless d
74
+ src = find(d)
75
+ dst = find(r.codomain_irdi)
76
+ next unless src && dst
77
+
78
+ if src.is_a?(Opencdd::Klass) && dst.is_a?(Opencdd::Property)
79
+ src.declare_property(dst.irdi)
80
+ @class_by_property_irdi[dst.irdi] << src unless @class_by_property_irdi[dst.irdi].include?(src)
81
+ elsif src.is_a?(Opencdd::Property) && dst.is_a?(Opencdd::Klass)
82
+ dst.declare_property(src.irdi)
83
+ @class_by_property_irdi[src.irdi] << dst unless @class_by_property_irdi[src.irdi].include?(dst)
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def link_property_classes_via_definition_class!
90
+ properties.each do |prop|
91
+ dc_raw = prop.properties[Opencdd::PropertyIds::MDC_P021]
92
+ next unless dc_raw
93
+ target = resolve_reference(dc_raw)
94
+ next unless target.is_a?(Opencdd::Klass)
95
+ target.declare_property(prop.irdi)
96
+ @class_by_property_irdi[prop.irdi] << target unless @class_by_property_irdi[prop.irdi].include?(target)
97
+ end
98
+ end
99
+
100
+ def link_value_lists!
101
+ properties.each do |prop|
102
+ next unless prop.enum?
103
+ identifier = value_list_identifier_of(prop)
104
+ vl_irdi = identifier && resolve_reference(identifier)&.irdi
105
+ next unless vl_irdi
106
+ vl = find(vl_irdi)
107
+ next unless vl.is_a?(Opencdd::ValueList)
108
+ @class_by_property_irdi[prop.irdi] << vl unless @class_by_property_irdi[prop.irdi].include?(vl)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Database
5
+ # Mutations: rename, merge, apply_change_request, remove,
6
+ # apply_view_control, and back-reference rewriting.
7
+ module Mutations
8
+ REFERENCE_VALUE_KINDS = %i[identifier_ref set_of_refs class_ref].freeze
9
+
10
+ def rename_entity(old_code, new_code)
11
+ old_code_s = old_code.to_s
12
+ new_code_s = new_code.to_s
13
+ return self if old_code_s == new_code_s
14
+
15
+ target = find_by_code(old_code_s)
16
+ return self unless target
17
+
18
+ clash = find_by_code(new_code_s)
19
+ if clash && clash.irdi != target.irdi
20
+ warn "Cannot rename #{old_code_s} → #{new_code_s}: target code already in use by #{clash.irdi}"
21
+ return self
22
+ end
23
+
24
+ old_irdi = target.irdi
25
+ new_irdi = old_irdi.with_code(new_code_s)
26
+
27
+ @entities_by_irdi.delete(old_irdi)
28
+ @entities_by_code[old_code_s].delete(target)
29
+
30
+ ref_property_ids = Opencdd::PropertyIds::REGISTRY.select do |_, e|
31
+ REFERENCE_VALUE_KINDS.include?(e.value_kind)
32
+ end.keys
33
+
34
+ entities.each do |entity|
35
+ rewrite_back_references!(entity, old_irdi, new_irdi, ref_property_ids)
36
+ end
37
+
38
+ code_pid = target.class.default_code_property_id(target.meta_class_irdi)
39
+ target.replace_code_value!(code_pid, new_code_s)
40
+ target.replace_irdi!(new_irdi)
41
+
42
+ @entities_by_irdi[new_irdi] = target
43
+ @entities_by_code[new_code_s] << target
44
+
45
+ rebuild_symbol_table!
46
+ self
47
+ end
48
+
49
+ def merge(other)
50
+ raise TypeError, "merge expects a Opencdd::Database" unless other.is_a?(Opencdd::Database)
51
+ other.entities.each { |e| add_entity(e) }
52
+ finalize!
53
+ self
54
+ end
55
+
56
+ def apply_change_request(cr, removals: [])
57
+ raise TypeError, "apply_change_request expects a Opencdd::Database" unless cr.is_a?(Opencdd::Database)
58
+ cr.entities.each { |e| add_entity(e) }
59
+ Array(removals).each { |r| remove_by_irdi(r) }
60
+ finalize!
61
+ self
62
+ end
63
+
64
+ def remove_by_irdi(ref)
65
+ irdi = ref.is_a?(Opencdd::IRDI) ? ref : Opencdd::IRDI.parse(ref.to_s)
66
+ return unless irdi
67
+ remove_entity_by_irdi!(irdi)
68
+ @entity_sources.delete(irdi)
69
+ self
70
+ end
71
+
72
+ def apply_view_control(klass, view_control)
73
+ properties = effective_properties.for(klass).to_a
74
+ return properties unless view_control.is_a?(Opencdd::ViewControl)
75
+
76
+ controlled = view_control.controlled_class_irdis
77
+ k = coerce_entity(klass)
78
+ return properties unless k.is_a?(Opencdd::Klass) && controlled.include?(k.irdi)
79
+
80
+ shown = view_control.shown_property_irdis
81
+ lookup = properties.each_with_object({}) { |p, h| h[p.irdi] = p }
82
+ shown.filter_map { |irdi| lookup[irdi] }
83
+ end
84
+
85
+ private
86
+
87
+ def rewrite_back_references!(entity, old_irdi, new_irdi, ref_property_ids)
88
+ ref_property_ids.each do |pid|
89
+ raw = entity.properties[pid]
90
+ next if raw.nil?
91
+ entry = Opencdd::PropertyIds::REGISTRY[pid]
92
+ case entry.value_kind
93
+ when :identifier_ref
94
+ parsed = Opencdd::IRDI.parse(raw.to_s)
95
+ next unless parsed == old_irdi
96
+ entity.write_property!(pid, new_irdi.to_s)
97
+ when :set_of_refs
98
+ elements = Opencdd::StructuredValues.unwrap_and_split(raw)
99
+ next if elements.empty?
100
+ mapped = elements.map { |e| e == old_irdi.to_s ? new_irdi.to_s : e }
101
+ entity.write_property!(pid, Opencdd::StructuredValues.rejoin(mapped))
102
+ when :class_ref
103
+ entity.write_property!(pid, substitute_class_ref_value(raw.to_s, old_irdi, new_irdi))
104
+ end
105
+ end
106
+ end
107
+
108
+ def substitute_class_ref_value(raw, old_irdi, new_irdi)
109
+ out = raw.sub(old_irdi.to_s, new_irdi.to_s)
110
+ return out if old_irdi.code.nil? || old_irdi.code == new_irdi.code
111
+ out.sub(old_irdi.code, new_irdi.code)
112
+ end
113
+
114
+ def remove_entity_by_irdi!(irdi)
115
+ entity = @entities_by_irdi.delete(irdi)
116
+ return unless entity
117
+ if entity.code
118
+ list = @entities_by_code[entity.code]
119
+ list.delete(entity)
120
+ @entities_by_code.delete(entity.code) if list.empty?
121
+ end
122
+ if entity.type
123
+ @entities_by_type[entity.type].delete(entity)
124
+ end
125
+ @symbol_table.delete_if { |_, e| e.equal?(entity) }
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Database
5
+ # Parcel workbook ingestion and dictionary lifecycle (add/drop/
6
+ # register_external). Sheetmap construction and translation-
7
+ # language parsing.
8
+ module ParcelIntegration
9
+ def add_workbook(workbook)
10
+ @workbooks << workbook
11
+ parcel_id = workbook.parcel_id
12
+ workbook.each_sheet do |sheet|
13
+ next unless sheet.type && sheet.rows.any?
14
+ type = sheet.type
15
+ entity_class = Opencdd::MetaClasses.entity_class_for_type(type)
16
+ raise "Unknown entity type #{type.inspect} for sheet #{sheet.name.inspect}" unless entity_class
17
+
18
+ sheet.rows.each do |row|
19
+ entity = entity_class.from_row(
20
+ row,
21
+ schema: sheet.schema,
22
+ meta_class_irdi: sheet.meta_class_irdi,
23
+ )
24
+ add_entity(entity)
25
+ if parcel_id && entity.irdi
26
+ prev = @entity_sources[entity.irdi]
27
+ @entity_sources[entity.irdi] = prev.nil? ? parcel_id : prev
28
+ end
29
+ end
30
+ end
31
+ self
32
+ end
33
+
34
+ def add_dictionary(dict)
35
+ validate_parcel_id!(dict.parcel_id)
36
+ source_language = dict.source_language || "en"
37
+ translation_languages = Array(dict.translation_languages)
38
+ meta_irdis = normalize_meta_class_irdis(dict.meta_class_irdis)
39
+
40
+ sheets = meta_irdis.map do |meta_irdi|
41
+ Opencdd::Parcel::Sheet.scaffold(
42
+ meta_class_irdi: meta_irdi,
43
+ parcel_id: dict.parcel_id,
44
+ source_language: source_language,
45
+ translation_languages: translation_languages,
46
+ )
47
+ end
48
+
49
+ project = Opencdd::Parcel::Workbook::ProjectInfo.new(
50
+ project_id: dict.parcel_id,
51
+ parcel_id: dict.parcel_id,
52
+ multi_language: translation_languages.join(","),
53
+ base_language: source_language,
54
+ )
55
+ sheetmap = build_sheetmap_for(dict.parcel_id, sheets)
56
+ workbook = Opencdd::Parcel::Workbook.new(
57
+ sheets: sheets, sheetmap: sheetmap, project: project,
58
+ )
59
+ @workbooks << workbook
60
+ workbook
61
+ end
62
+
63
+ def drop_dictionary(parcel_id)
64
+ matching = @workbooks.select { |wb| wb.parcel_id == parcel_id }
65
+ raise ArgumentError, "no dictionary with parcel_id #{parcel_id.inspect}" if matching.empty?
66
+
67
+ irdis_to_drop = @entity_sources.select { |_, pid| pid == parcel_id }.keys
68
+ irdis_to_drop.each do |irdi|
69
+ remove_entity_by_irdi!(irdi)
70
+ @entity_sources.delete(irdi)
71
+ end
72
+ @workbooks.reject! { |wb| wb.parcel_id == parcel_id }
73
+ self
74
+ end
75
+
76
+ def register_external_sheet(sheet, parcel_id:)
77
+ validate_parcel_id!(parcel_id)
78
+ wb = @workbooks.find { |w| w.parcel_id == parcel_id }
79
+ if wb.nil?
80
+ project = Opencdd::Parcel::Workbook::ProjectInfo.new(
81
+ project_id: parcel_id, parcel_id: parcel_id,
82
+ multi_language: "", base_language: "en",
83
+ )
84
+ wb = Opencdd::Parcel::Workbook.new(sheets: [], sheetmap: [], project: project)
85
+ @workbooks << wb
86
+ end
87
+ wb.register_sheet(sheet)
88
+ self
89
+ end
90
+
91
+ def dictionaries
92
+ @workbooks.map do |wb|
93
+ Dictionary.new(
94
+ parcel_id: wb.parcel_id,
95
+ source_language: wb.base_language,
96
+ translation_languages: parse_translation_languages(wb),
97
+ meta_class_irdis: wb.sheets.map(&:meta_class_irdi).compact.uniq,
98
+ )
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def validate_parcel_id!(parcel_id)
105
+ unless parcel_id.is_a?(String) && parcel_id.match?(PARCEL_ID_PATTERN)
106
+ raise ArgumentError, "invalid parcel_id: #{parcel_id.inspect}"
107
+ end
108
+ end
109
+
110
+ def normalize_meta_class_irdis(raw)
111
+ codes = Array(raw).map { |v| v.to_s.split("#").last }
112
+ FORCED_META_CLASSES.each { |c| codes << c unless codes.include?(c) }
113
+ codes.uniq
114
+ end
115
+
116
+ def build_sheetmap_for(parcel_id, sheets)
117
+ entries = [
118
+ Opencdd::Parcel::Workbook::SheetMapEntry.new(
119
+ project_id: parcel_id, parcel_id: parcel_id, class_irdi: nil,
120
+ content_no: 0, sheet_no: 2, sheet_name: "pcls_LOCAL",
121
+ type: "PARCEL_LIST", target: "",
122
+ ),
123
+ ]
124
+ sheets.each_with_index do |sheet, idx|
125
+ entries << Opencdd::Parcel::Workbook::SheetMapEntry.new(
126
+ project_id: parcel_id, parcel_id: parcel_id,
127
+ class_irdi: sheet.meta_class_irdi,
128
+ content_no: 0, sheet_no: idx + 3,
129
+ sheet_name: sheet.name,
130
+ type: Opencdd::MetaClasses.sheet_type_for_type(Opencdd::MetaClasses.type_for(sheet.meta_class_code)),
131
+ target: "",
132
+ )
133
+ end
134
+ entries
135
+ end
136
+
137
+ def parse_translation_languages(workbook)
138
+ multi = workbook.project&.multi_language.to_s
139
+ multi.split(",").map(&:strip).reject(&:empty?)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Database
5
+ # YAML persistence: whole-database YAML (to_yaml via
6
+ # Model::YamlDatabase) and per-entity directory persistence
7
+ # (save_to_directory via Model::EntityStore). Class-method
8
+ # counterparts (from_yaml, load_from_directory) are defined
9
+ # on Database directly in database.rb.
10
+ module Persistence
11
+ def to_yaml(*args)
12
+ Opencdd::Model::YamlDatabase.from_database(self).to_yaml(*args)
13
+ end
14
+
15
+ def save_to_directory(path)
16
+ Opencdd::Model::EntityStore.new(path).save_database(self)
17
+ self
18
+ end
19
+
20
+ def semantically_equal?(other)
21
+ return false unless other.is_a?(Opencdd::Database)
22
+ return false unless entities.size == other.entities.size
23
+ entities.all? do |e|
24
+ oe = other.find(e.irdi)
25
+ oe && e.type == oe.type && e.properties == oe.properties
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opencdd
4
+ class Database
5
+ # Graph queries: find, coerce, powertype API, tree walkers,
6
+ # property/class/value-list lookups, relation queries.
7
+ module Queries
8
+ def resolve_reference(ref)
9
+ return nil if ref.nil?
10
+ return ref if ref.is_a?(Opencdd::Entity)
11
+
12
+ key = ref.to_s.strip
13
+ return nil if key.empty?
14
+
15
+ if key.include?("/") || key.include?("#")
16
+ irdi = Opencdd::IRDI.parse(key)
17
+ return find(irdi)
18
+ end
19
+
20
+ entity = @symbol_table[key]
21
+ return entity if entity
22
+
23
+ by_code = find_by_code(key)
24
+ return by_code if by_code
25
+
26
+ irdi = Opencdd::IRDI.parse(key)
27
+ irdi ? find(irdi) : nil
28
+ end
29
+
30
+ def find(irdi)
31
+ return nil if irdi.nil?
32
+ key = Opencdd::IRDI === irdi ? irdi : Opencdd::IRDI.parse(irdi)
33
+ return nil unless key
34
+ @entities_by_irdi[key]
35
+ end
36
+
37
+ def find_by_code(code)
38
+ matches = @entities_by_code[code.to_s]
39
+ return nil if matches.empty?
40
+ matches.first
41
+ end
42
+
43
+ def find_all_by_code(code)
44
+ @entities_by_code[code.to_s].dup
45
+ end
46
+
47
+ def find_by_name(name, type: nil, lang: :en)
48
+ pools = type ? [@entities_by_type[type]].compact : @entities_by_type.values
49
+ pools.each do |pool|
50
+ hit = pool.find { |e| e.preferred_name(lang).to_s.casecmp(name.to_s).zero? }
51
+ return hit if hit
52
+ end
53
+ nil
54
+ end
55
+
56
+ def coerce_entity(value)
57
+ return nil if value.nil?
58
+ return value if value.is_a?(Opencdd::Entity)
59
+ find(value)
60
+ end
61
+
62
+ def coerce_irdi(value)
63
+ return nil if value.nil?
64
+ return value.irdi if value.is_a?(Opencdd::Entity)
65
+ return value if value.is_a?(Opencdd::IRDI)
66
+ Opencdd::IRDI.parse(value.to_s)
67
+ end
68
+
69
+ def root_classes
70
+ classes.select { |c| c.parent_irdi.nil? }
71
+ end
72
+
73
+ # ── Powertype accessors (4-layer ontology) ──────────────
74
+
75
+ def categorical_classes
76
+ classes.select(&:powertype?)
77
+ end
78
+
79
+ def instances_of(categorical_klass)
80
+ k = coerce_entity(categorical_klass)
81
+ return [] unless k.is_a?(Opencdd::Klass)
82
+ k.categorical_instances(self)
83
+ end
84
+
85
+ def valid_class_reference?(categorical_klass, value)
86
+ target = coerce_entity(value)
87
+ return false unless target
88
+ instances_of(categorical_klass).any? { |c| c.irdi == target.irdi }
89
+ end
90
+
91
+ # ── Tree walkers ─────────────────────────────────────────
92
+
93
+ def class_tree(fields: Opencdd::ClassTree::DEFAULT_FIELDS)
94
+ Opencdd::ClassTree.new(self, fields: fields)
95
+ end
96
+
97
+ def effective_properties
98
+ @effective_properties ||= Opencdd::EffectiveProperties.new(self)
99
+ end
100
+
101
+ def composition_tree(klass, max_depth: 10)
102
+ Opencdd::CompositionTree.new(self).for(klass, max_depth: max_depth)
103
+ end
104
+
105
+ def relation_tree(root = nil, max_depth: 10)
106
+ Opencdd::RelationTree.new(self).for(root, max_depth: max_depth)
107
+ end
108
+
109
+ # ── Property / class / value-list lookups ────────────────
110
+
111
+ def properties_of(klass)
112
+ k = coerce_entity(klass)
113
+ return [] unless k.is_a?(Opencdd::Klass)
114
+ k.properties_on_class(self)
115
+ end
116
+
117
+ def classes_with_property(property)
118
+ irdi = coerce_irdi(property)
119
+ return [] unless irdi
120
+ @class_by_property_irdi&.fetch(irdi, []) || []
121
+ end
122
+
123
+ def value_list_of(property)
124
+ prop = coerce_entity(property)
125
+ return nil unless prop.is_a?(Opencdd::Property)
126
+
127
+ relations.each do |r|
128
+ next unless r.predication?
129
+ next unless r.domain_irdis.include?(prop.irdi)
130
+ vl = r.codomain_irdi && find(r.codomain_irdi)
131
+ return vl if vl.is_a?(Opencdd::ValueList)
132
+ end
133
+
134
+ return nil unless prop.enum?
135
+ identifier = value_list_identifier_of(prop)
136
+ return nil unless identifier
137
+ vl = resolve_reference(identifier)
138
+ vl if vl.is_a?(Opencdd::ValueList)
139
+ end
140
+
141
+ def value_list_identifier_of(property)
142
+ case property.parsed_data_type
143
+ when Opencdd::DataType::EnumStringType, Opencdd::DataType::EnumReferenceType
144
+ property.parsed_data_type.value_list_identifier
145
+ end
146
+ end
147
+
148
+ def terms_of(value_list)
149
+ return [] if value_list.nil?
150
+ value_list.term_irdis.map { |i| find(i) }.compact
151
+ end
152
+
153
+ def relations_for(domain: nil, codomain: nil)
154
+ dom = coerce_irdi(domain)
155
+ cod = coerce_irdi(codomain)
156
+ relations.select do |r|
157
+ (dom.nil? || r.domain_irdis.include?(dom)) &&
158
+ (cod.nil? || r.codomain_irdi == cod)
159
+ end
160
+ end
161
+
162
+ def functions_involving(property)
163
+ irdi = coerce_irdi(property)
164
+ return [] unless irdi
165
+ relations.select do |r|
166
+ r.function? && (r.domain_irdis.include?(irdi) || r.codomain_irdi == irdi)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end