ruby-hdf5 0.0.2 → 0.0.4

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/hdf5/file.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  module HDF5
2
2
  class File
3
+ include Hierarchy
4
+
3
5
  H5F_ACC_RDONLY = 0x0000
4
6
  H5F_ACC_RDWR = 0x0001
5
7
  H5F_ACC_TRUNC = 0x0002
8
+ H5F_ACC_EXCL = 0x0004
6
9
 
7
10
  class << self
8
11
  def create(filename, flags = H5F_ACC_TRUNC)
@@ -17,8 +20,9 @@ module HDF5
17
20
  end
18
21
  end
19
22
 
20
- def open(filename, mode = H5F_ACC_RDONLY)
21
- file = from_id(HDF5::FFI.H5Fopen(filename, mode, HDF5::DEFAULT_PROPERTY_LIST), filename, mode)
23
+ def open(filename, mode = 'r')
24
+ file_id, flags = open_file(filename, mode)
25
+ file = from_id(file_id, filename, flags)
22
26
  return file unless block_given?
23
27
 
24
28
  begin
@@ -30,6 +34,29 @@ module HDF5
30
34
 
31
35
  private
32
36
 
37
+ def open_file(filename, mode)
38
+ return [HDF5::FFI.H5Fopen(filename, mode, HDF5::DEFAULT_PROPERTY_LIST), mode] if mode.is_a?(Integer)
39
+
40
+ case mode
41
+ when 'r'
42
+ [HDF5::FFI.H5Fopen(filename, H5F_ACC_RDONLY, HDF5::DEFAULT_PROPERTY_LIST), H5F_ACC_RDONLY]
43
+ when 'r+'
44
+ [HDF5::FFI.H5Fopen(filename, H5F_ACC_RDWR, HDF5::DEFAULT_PROPERTY_LIST), H5F_ACC_RDWR]
45
+ when 'w'
46
+ [HDF5::FFI.H5Fcreate(filename, H5F_ACC_TRUNC, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST),
47
+ H5F_ACC_TRUNC]
48
+ when 'x'
49
+ [HDF5::FFI.H5Fcreate(filename, H5F_ACC_EXCL, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST),
50
+ H5F_ACC_EXCL]
51
+ when 'a'
52
+ file_id = HDF5::FFI.H5Fcreate(filename, H5F_ACC_EXCL, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST)
53
+ file_id = HDF5::FFI.H5Fopen(filename, H5F_ACC_RDWR, HDF5::DEFAULT_PROPERTY_LIST) if file_id < 0
54
+ [file_id, H5F_ACC_RDWR]
55
+ else
56
+ raise ArgumentError, "Unsupported file mode: #{mode.inspect}"
57
+ end
58
+ end
59
+
33
60
  def from_id(file_id, filename, mode)
34
61
  file = allocate
35
62
  file.send(:initialize_from_id, file_id, filename, mode)
@@ -42,21 +69,54 @@ module HDF5
42
69
  end
43
70
 
44
71
  def close
45
- return if @file_id.nil?
72
+ return if @file_id.nil? || @context.closed?
46
73
 
47
- HDF5::FFI.H5Fclose(@file_id)
74
+ @context.close_file
48
75
  @file_id = nil
49
76
  end
50
77
 
78
+ def closed?
79
+ @file_id.nil? || @context.closed?
80
+ end
81
+
82
+ def flush
83
+ ensure_open!
84
+ status = HDF5::FFI.H5Fflush(@file_id, :H5F_SCOPE_GLOBAL)
85
+ raise HDF5::Error, 'Failed to flush file' if status < 0
86
+
87
+ self
88
+ end
89
+
51
90
  def create_group(name, &block)
52
- Group.create(@file_id, name, &block)
91
+ group = @context.synchronize do
92
+ ensure_open!
93
+ Group.create(@file_id, name, @context)
94
+ end
95
+ return group unless block
96
+
97
+ begin
98
+ block.call(group)
99
+ ensure
100
+ group.close
101
+ end
53
102
  end
54
103
 
55
- def create_dataset(name, data, &block)
56
- Dataset.create(@file_id, name, data, &block)
104
+ def create_dataset(name, data = nil, **options, &block)
105
+ dataset = @context.synchronize do
106
+ ensure_open!
107
+ Dataset.create(@file_id, name, data, context: @context, **options)
108
+ end
109
+ return dataset unless block
110
+
111
+ begin
112
+ block.call(dataset)
113
+ ensure
114
+ dataset.close
115
+ end
57
116
  end
58
117
 
59
118
  def list_entries
119
+ ensure_open!
60
120
  list = []
61
121
  callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
62
122
  list << name
@@ -74,27 +134,41 @@ module HDF5
74
134
  end
75
135
 
76
136
  def [](name)
137
+ ensure_open!
77
138
  if group?(name)
78
- Group.open(@file_id, name)
139
+ Group.open(@file_id, name, @context)
79
140
  elsif dataset?(name)
80
- Dataset.open(@file_id, name)
141
+ Dataset.open(@file_id, name, context: @context)
81
142
  else
82
143
  raise HDF5::Error, "Group or dataset not found: #{name}"
83
144
  end
84
145
  end
85
146
 
86
147
  def attrs
87
- @attrs ||= AttributeManager.new(@file_id)
148
+ ensure_open!
149
+ @attrs ||= AttributeManager.new(@file_id, @context)
88
150
  end
89
151
 
90
152
  private
91
153
 
154
+ def hdf5_id
155
+ ensure_open!
156
+ @file_id
157
+ end
158
+
92
159
  def initialize_from_id(file_id, filename, mode)
93
160
  raise HDF5::Error, "Failed to open file: #{filename}" if file_id < 0
94
161
 
95
162
  @filename = filename
96
163
  @mode = mode
97
164
  @file_id = file_id
165
+ @context = FileContext.new(file_id)
166
+ end
167
+
168
+ def ensure_open!
169
+ raise ClosedError, 'HDF5 file is closed' if @file_id.nil?
170
+
171
+ @context.ensure_open!(@file_id)
98
172
  end
99
173
 
100
174
  def group?(name)
@@ -122,5 +196,7 @@ module HDF5
122
196
  end
123
197
  info[:type] == :H5O_TYPE_DATASET
124
198
  end
199
+
200
+ prepend FileContext.guard(:flush, :list_entries, :[], :attrs, :close, :closed?)
125
201
  end
126
202
  end
@@ -0,0 +1,70 @@
1
+ module HDF5
2
+ class FileContext
3
+ CLOSE_FUNCTIONS = {
4
+ file: :H5Fclose,
5
+ group: :H5Gclose,
6
+ dataset: :H5Dclose
7
+ }.freeze
8
+
9
+ def initialize(file_id)
10
+ @file_id = file_id
11
+ @handles = { file_id => :file }
12
+ @closed = false
13
+ end
14
+
15
+ def register(id, type)
16
+ raise ClosedError, 'HDF5 file is closed' if closed?
17
+
18
+ @handles[id] = type
19
+ id
20
+ end
21
+
22
+ def close(id)
23
+ synchronize do
24
+ type = @handles[id]
25
+ return if type.nil?
26
+
27
+ status = HDF5::FFI.public_send(CLOSE_FUNCTIONS.fetch(type), id)
28
+ raise HDF5::Error, "Failed to close HDF5 #{type}" if status < 0
29
+
30
+ @handles.delete(id)
31
+ @closed = true if type == :file
32
+ nil
33
+ end
34
+ end
35
+
36
+ def close_file
37
+ synchronize do
38
+ return if closed?
39
+
40
+ children = @handles.keys.reject { |id| id == @file_id }
41
+ children.reverse_each { |id| close(id) }
42
+ close(@file_id)
43
+ end
44
+ end
45
+
46
+ def ensure_open!(id = nil)
47
+ raise ClosedError, 'HDF5 file is closed' if closed?
48
+ raise ClosedError, 'HDF5 object is closed' if id && !@handles.key?(id)
49
+ end
50
+
51
+ def closed?
52
+ @closed
53
+ end
54
+
55
+ def synchronize(&block)
56
+ HDF5::FFI::CALL_LOCK.synchronize(&block)
57
+ end
58
+
59
+ def self.guard(*methods)
60
+ Module.new do
61
+ methods.each do |method|
62
+ define_method(method) do |*args, **kwargs, &block|
63
+ lock = @context || HDF5::FFI::CALL_LOCK
64
+ lock.synchronize { super(*args, **kwargs, &block) }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/hdf5/group.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  module HDF5
2
2
  class Group
3
+ include Hierarchy
4
+
3
5
  class << self
4
- def create(parent_id, name)
6
+ def create(parent_id, name, context = nil)
5
7
  group = from_id(
6
8
  HDF5::FFI.H5Gcreate2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST, HDF5::DEFAULT_PROPERTY_LIST,
7
- HDF5::DEFAULT_PROPERTY_LIST), name
9
+ HDF5::DEFAULT_PROPERTY_LIST), name, context
8
10
  )
9
11
  return group unless block_given?
10
12
 
@@ -15,8 +17,8 @@ module HDF5
15
17
  end
16
18
  end
17
19
 
18
- def open(parent_id, name)
19
- group = from_id(HDF5::FFI.H5Gopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
20
+ def open(parent_id, name, context = nil)
21
+ group = from_id(HDF5::FFI.H5Gopen2(parent_id, name, HDF5::DEFAULT_PROPERTY_LIST), name, context)
20
22
  return group unless block_given?
21
23
 
22
24
  begin
@@ -28,33 +30,58 @@ module HDF5
28
30
 
29
31
  private
30
32
 
31
- def from_id(group_id, name)
33
+ def from_id(group_id, name, context)
32
34
  group = allocate
33
- group.send(:initialize_from_id, group_id, name)
35
+ group.send(:initialize_from_id, group_id, name, context)
34
36
  group
35
37
  end
36
38
  end
37
39
 
38
40
  def initialize(file_id, name)
39
- initialize_from_id(HDF5::FFI.H5Gopen2(file_id, name, HDF5::DEFAULT_PROPERTY_LIST), name)
41
+ initialize_from_id(HDF5::FFI.H5Gopen2(file_id, name, HDF5::DEFAULT_PROPERTY_LIST), name, nil)
40
42
  end
41
43
 
42
44
  def close
43
45
  return if @group_id.nil?
44
46
 
45
- HDF5::FFI.H5Gclose(@group_id)
47
+ @context ? @context.close(@group_id) : HDF5::FFI.H5Gclose(@group_id)
46
48
  @group_id = nil
47
49
  end
48
50
 
51
+ def closed?
52
+ @group_id.nil? || (@context && @context.closed?)
53
+ end
54
+
49
55
  def create_group(name, &block)
50
- self.class.create(@group_id, name, &block)
56
+ group = HDF5::FFI::CALL_LOCK.synchronize do
57
+ ensure_open!
58
+ self.class.create(@group_id, name, @context)
59
+ end
60
+ return group unless block
61
+
62
+ begin
63
+ block.call(group)
64
+ ensure
65
+ group.close
66
+ end
51
67
  end
52
68
 
53
- def create_dataset(name, data, &block)
54
- Dataset.create(@group_id, name, data, &block)
69
+ def create_dataset(name, data = nil, **options, &block)
70
+ dataset = HDF5::FFI::CALL_LOCK.synchronize do
71
+ ensure_open!
72
+ Dataset.create(@group_id, name, data, context: @context, **options)
73
+ end
74
+ return dataset unless block
75
+
76
+ begin
77
+ block.call(dataset)
78
+ ensure
79
+ dataset.close
80
+ end
55
81
  end
56
82
 
57
83
  def list_entries
84
+ ensure_open!
58
85
  entries = []
59
86
  callback = ::FFI::Function.new(:int, %i[int64_t string pointer pointer]) do |_, name, _, _|
60
87
  entries << name
@@ -76,26 +103,41 @@ module HDF5
76
103
  end
77
104
 
78
105
  def [](name)
106
+ ensure_open!
79
107
  if group?(name)
80
- self.class.open(@group_id, name)
108
+ self.class.open(@group_id, name, @context)
81
109
  elsif dataset?(name)
82
- Dataset.open(@group_id, name)
110
+ Dataset.open(@group_id, name, context: @context)
83
111
  else
84
112
  raise HDF5::Error, "Group or dataset not found: #{name}"
85
113
  end
86
114
  end
87
115
 
88
116
  def attrs
89
- @attrs ||= AttributeManager.new(@group_id)
117
+ ensure_open!
118
+ @attrs ||= AttributeManager.new(@group_id, @context)
90
119
  end
91
120
 
92
121
  private
93
122
 
94
- def initialize_from_id(group_id, name)
123
+ def hdf5_id
124
+ ensure_open!
125
+ @group_id
126
+ end
127
+
128
+ def initialize_from_id(group_id, name, context)
95
129
  raise HDF5::Error, "Failed to open group: #{name}" if group_id < 0
96
130
 
97
131
  @group_id = group_id
98
132
  @name = name
133
+ @context = context
134
+ @context.register(group_id, :group) if @context
135
+ end
136
+
137
+ def ensure_open!
138
+ raise ClosedError, 'HDF5 group is closed' if @group_id.nil?
139
+
140
+ @context&.ensure_open!(@group_id)
99
141
  end
100
142
 
101
143
  def group?(name)
@@ -123,5 +165,7 @@ module HDF5
123
165
  end
124
166
  info[:type] == :H5O_TYPE_DATASET
125
167
  end
168
+
169
+ prepend FileContext.guard(:list_entries, :list_datasets, :[], :attrs, :close, :closed?)
126
170
  end
127
171
  end
@@ -0,0 +1,107 @@
1
+ module HDF5
2
+ module Hierarchy
3
+ def keys
4
+ list_entries
5
+ end
6
+
7
+ def each_key(&block)
8
+ return enum_for(__method__) unless block
9
+
10
+ keys.each(&block)
11
+ self
12
+ end
13
+
14
+ def require_group(path)
15
+ path.split('/').reject(&:empty?).inject(self) do |parent, name|
16
+ if parent.key?(name)
17
+ child = parent[name]
18
+ raise HDF5::Error, "Existing object is not a group: #{name}" unless child.is_a?(Group)
19
+
20
+ child
21
+ else
22
+ parent.create_group(name)
23
+ end
24
+ end
25
+ end
26
+
27
+ def open_dataset(name)
28
+ object = self[name]
29
+ raise HDF5::Error, "Object is not a dataset: #{name}" unless object.is_a?(Dataset)
30
+ return object unless block_given?
31
+
32
+ begin
33
+ yield object
34
+ ensure
35
+ object.close
36
+ end
37
+ end
38
+
39
+ def key?(name)
40
+ exists = HDF5::FFI.H5Lexists(hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST)
41
+ raise HDF5::Error, "Failed to check link existence: #{name}" if exists.negative?
42
+
43
+ exists.positive?
44
+ end
45
+
46
+ def delete(name)
47
+ status = HDF5::FFI.H5Ldelete(hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST)
48
+ raise HDF5::Error, "Failed to delete link: #{name}" if status < 0
49
+
50
+ self
51
+ end
52
+
53
+ def create_hard_link(target, name)
54
+ status = HDF5::FFI.H5Lcreate_hard(hdf5_id, target, hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST,
55
+ HDF5::DEFAULT_PROPERTY_LIST)
56
+ raise HDF5::Error, "Failed to create hard link: #{name}" if status < 0
57
+
58
+ self
59
+ end
60
+
61
+ def create_soft_link(target, name)
62
+ status = HDF5::FFI.H5Lcreate_soft(target, hdf5_id, name, HDF5::DEFAULT_PROPERTY_LIST,
63
+ HDF5::DEFAULT_PROPERTY_LIST)
64
+ raise HDF5::Error, "Failed to create soft link: #{name}" if status < 0
65
+
66
+ self
67
+ end
68
+
69
+ def link_info(name)
70
+ info = HDF5::FFI::MiV == 10 ? HDF5::FFI::H5LInfoT.new : HDF5::FFI::H5LInfo2T.new
71
+ status = if HDF5::FFI::MiV == 10
72
+ HDF5::FFI.H5Lget_info(hdf5_id, name, info, HDF5::DEFAULT_PROPERTY_LIST)
73
+ else
74
+ HDF5::FFI.H5Lget_info2(hdf5_id, name, info, HDF5::DEFAULT_PROPERTY_LIST)
75
+ end
76
+ raise HDF5::Error, "Failed to get link information: #{name}" if status < 0
77
+
78
+ type = info[:type]
79
+ return { type: :hard } if type == :H5L_TYPE_HARD
80
+
81
+ size = info[:u][:val_size]
82
+ buffer = ::FFI::MemoryPointer.new(:char, size)
83
+ status = HDF5::FFI.H5Lget_val(hdf5_id, name, buffer, size, HDF5::DEFAULT_PROPERTY_LIST)
84
+ raise HDF5::Error, "Failed to get link value: #{name}" if status < 0
85
+
86
+ raw = buffer.read_bytes(size)
87
+ return { type: :soft, target: raw.delete_suffix("\0") } if type == :H5L_TYPE_SOFT
88
+
89
+ if type == :H5L_TYPE_EXTERNAL
90
+ filename, path = raw.byteslice(1..).split("\0", 2)
91
+ return { type: :external, filename:, path: path&.delete_suffix("\0") }
92
+ end
93
+
94
+ raise UnsupportedTypeError, "Unsupported link type: #{type}"
95
+ end
96
+
97
+ def move(source, destination)
98
+ status = HDF5::FFI.H5Lmove(hdf5_id, source, hdf5_id, destination, HDF5::DEFAULT_PROPERTY_LIST,
99
+ HDF5::DEFAULT_PROPERTY_LIST)
100
+ raise HDF5::Error, "Failed to move link: #{source}" if status < 0
101
+
102
+ self
103
+ end
104
+
105
+ prepend FileContext.guard(*(instance_methods(false) - %i[each_key open_dataset]))
106
+ end
107
+ end
@@ -0,0 +1,97 @@
1
+ module HDF5
2
+ class Selection
3
+ attr_reader :start, :stride, :count, :result_shape
4
+
5
+ def self.normalize(selection, shape)
6
+ return new(Array.new(shape.length, true), shape) if selection.nil?
7
+
8
+ selectors = selection.is_a?(Array) ? selection.dup : [selection]
9
+ raise IndexError, 'Too many indices for dataset' if selectors.length > shape.length
10
+
11
+ selectors.concat(Array.new(shape.length - selectors.length, true))
12
+ new(selectors, shape)
13
+ end
14
+
15
+ def initialize(selectors, shape)
16
+ @start = []
17
+ @stride = []
18
+ @count = []
19
+ @result_shape = []
20
+ @result_axes = []
21
+
22
+ selectors.zip(shape).each_with_index do |(selector, dimension), axis|
23
+ @result_axes << axis unless selector.is_a?(Integer)
24
+ normalize_axis(selector, dimension)
25
+ end
26
+ end
27
+
28
+ def scalar?
29
+ result_shape.empty?
30
+ end
31
+
32
+ def size
33
+ count.inject(1, :*)
34
+ end
35
+
36
+ def block(ranges)
37
+ result = dup
38
+ starts = start.dup
39
+ counts = count.dup
40
+ @result_axes.zip(ranges).each do |axis, range|
41
+ starts[axis] += range.begin * stride[axis]
42
+ counts[axis] = range.end - range.begin
43
+ end
44
+ result.instance_variable_set(:@start, starts)
45
+ result.instance_variable_set(:@count, counts)
46
+ result.instance_variable_set(:@result_shape, ranges.map { |range| range.end - range.begin })
47
+ result
48
+ end
49
+
50
+ private
51
+
52
+ def normalize_axis(selector, dimension)
53
+ case selector
54
+ when true
55
+ @start << 0
56
+ @stride << 1
57
+ @count << dimension
58
+ @result_shape << dimension
59
+ when Integer
60
+ index = selector.negative? ? dimension + selector : selector
61
+ raise IndexError, "Index #{selector} is outside dimension of size #{dimension}" unless index.between?(0,
62
+ dimension - 1)
63
+
64
+ @start << index
65
+ @stride << 1
66
+ @count << 1
67
+ when Range
68
+ first, last = normalize_range(selector, dimension)
69
+ @start << first
70
+ @stride << 1
71
+ @count << last - first
72
+ @result_shape << last - first
73
+ when HDF5::Slice
74
+ first, last = normalize_range(selector.range, dimension)
75
+ count = (last - first).fdiv(selector.step).ceil
76
+ @start << first
77
+ @stride << selector.step
78
+ @count << count
79
+ @result_shape << count
80
+ else
81
+ raise IndexError, "Unsupported index: #{selector.inspect}"
82
+ end
83
+ end
84
+
85
+ def normalize_range(range, dimension)
86
+ first = range.begin.nil? ? 0 : range.begin
87
+ last = range.end.nil? ? dimension : range.end
88
+ first += dimension if first.negative?
89
+ last += dimension if last.negative?
90
+ last += 1 unless range.exclude_end? || range.end.nil?
91
+
92
+ first = [[first, 0].max, dimension].min
93
+ last = [[last, 0].max, dimension].min
94
+ [first, [last, first].max]
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,106 @@
1
+ module HDF5
2
+ module StringCodec
3
+ module_function
4
+
5
+ UTF8 = 1
6
+ VARIABLE_SIZE = (1 << (::FFI.type_size(:size_t) * 8)) - 1
7
+
8
+ def datatype_id
9
+ type_id = HDF5::FFI.H5Tcopy(HDF5::FFI.H5T_C_S1)
10
+ raise HDF5::Error, 'Failed to create string datatype' if type_id < 0
11
+
12
+ check(HDF5::FFI.H5Tset_size(type_id, VARIABLE_SIZE), 'set variable string size')
13
+ check(HDF5::FFI.H5Tset_cset(type_id, UTF8), 'set UTF-8 string encoding')
14
+ type_id
15
+ rescue StandardError
16
+ HDF5::FFI.H5Tclose(type_id) if type_id && type_id >= 0
17
+ raise
18
+ end
19
+
20
+ def buffer_for(value)
21
+ buffer_for_values([normalize(value)])
22
+ end
23
+
24
+ def buffer_for_values(values, encoding: Encoding::UTF_8)
25
+ pointers = values.map { |value| ::FFI::MemoryPointer.from_string(normalize(value, encoding:)) }
26
+ buffer = ::FFI::MemoryPointer.new(:pointer, pointers.length)
27
+ buffer.write_array_of_pointer(pointers)
28
+ [buffer, pointers]
29
+ end
30
+
31
+ def read(buffer, encoding: Encoding::UTF_8)
32
+ read_values(buffer, 1, [], encoding:)
33
+ end
34
+
35
+ def read_values(buffer, count, shape, encoding: Encoding::UTF_8)
36
+ values = buffer.read_array_of_pointer(count).map do |pointer|
37
+ next nil if pointer.null?
38
+
39
+ value = pointer.read_string.force_encoding(encoding)
40
+ raise HDF5::Error, "String data is not valid #{encoding.name}" unless value.valid_encoding?
41
+
42
+ value
43
+ end
44
+ return values.first if shape.empty?
45
+
46
+ Numo::RObject.cast(values).reshape(*shape)
47
+ end
48
+
49
+ def string_data?(value)
50
+ return true if value.is_a?(String)
51
+
52
+ values = if value.is_a?(Numo::RObject)
53
+ value.to_a.flatten
54
+ elsif value.is_a?(Array)
55
+ value.flatten
56
+ else
57
+ []
58
+ end
59
+ !values.empty? && values.all? { |item| item.is_a?(String) }
60
+ end
61
+
62
+ def normalize_data(value, encoding: Encoding::UTF_8)
63
+ return [[normalize(value, encoding:)], []] if value.is_a?(String)
64
+ return [value.to_a.flatten.map { |item| normalize(item, encoding:) }, value.shape] if value.is_a?(Numo::RObject)
65
+
66
+ shape = array_shape(value)
67
+ [value.flatten.map { |item| normalize(item, encoding:) }, shape]
68
+ end
69
+
70
+ def array_shape(value)
71
+ return [] unless value.is_a?(Array)
72
+ raise HDF5::Error, 'String data must not be empty' if value.empty?
73
+
74
+ DataHelpers.array_shape(value)
75
+ end
76
+
77
+ def variable?(type_id)
78
+ HDF5::FFI.H5Tis_variable_str(type_id).positive?
79
+ end
80
+
81
+ def encoding_for(type_id)
82
+ case HDF5::FFI.H5Tget_cset(type_id)
83
+ when :H5T_CSET_ASCII then Encoding::US_ASCII
84
+ when :H5T_CSET_UTF8 then Encoding::UTF_8
85
+ else raise UnsupportedTypeError, 'Unsupported HDF5 string encoding'
86
+ end
87
+ end
88
+
89
+ def normalize(value, encoding: Encoding::UTF_8)
90
+ raise HDF5::Error, 'String dataset data must be a String' unless value.is_a?(String)
91
+ raise HDF5::Error, 'String data has an invalid encoding' unless value.valid_encoding?
92
+ raise HDF5::Error, 'Variable-length strings cannot contain NUL bytes' if value.include?("\0")
93
+
94
+ result = value.encode(encoding)
95
+ raise HDF5::Error, "String data is not valid #{encoding.name}" unless result.valid_encoding?
96
+
97
+ result
98
+ rescue EncodingError
99
+ raise HDF5::Error, "String data must be representable as #{encoding.name}"
100
+ end
101
+
102
+ def check(status, operation)
103
+ raise HDF5::Error, "Failed to #{operation}" if status < 0
104
+ end
105
+ end
106
+ end
data/lib/hdf5/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HDF5
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end