carray-netcdf 2.0.0 → 2.1.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.
@@ -18,6 +18,9 @@ module CA
18
18
  class Var
19
19
 
20
20
  include CA::NetCDF
21
+ # The low-level calls are this class's helpers, not its interface.
22
+ private :nc_get_var, :nc_get_vara, :nc_get_vars, :nc_get_varm,
23
+ :nc_put_var, :nc_put_vara, :nc_put_vars, :nc_put_varm
21
24
 
22
25
  # @param group [Group] containing group (root or subgroup)
23
26
  # @param varid [Integer] NetCDF variable id within `group`
@@ -53,6 +56,34 @@ module CA
53
56
  # @return [Integer] number of dimensions
54
57
  def ndim = shape.length
55
58
 
59
+ # Sizes asked of the file, not by reading the variable (a CArray
60
+ # method forwarded to {#to_ca} would read all of it).
61
+ # @return [Integer]
62
+ def elements = shape.inject(1, :*)
63
+ # @return [Integer]
64
+ def size = elements
65
+ # @return [Integer, nil] the length of the first dimension
66
+ def dim0 = shape[0]
67
+ # @return [Integer, nil] the length of the second dimension
68
+ def dim1 = shape[1]
69
+ # @return [Integer, nil] the length of the third dimension
70
+ def dim2 = shape[2]
71
+ # @return [Integer, nil] the length of the fourth dimension
72
+ def dim3 = shape[3]
73
+
74
+ # The first / last element, read as a point.
75
+ # @return [Numeric, UNDEF, CATime::Element, nil]
76
+ def first
77
+ return self[] if scalar?
78
+ elements.zero? ? nil : self[0]
79
+ end
80
+
81
+ # @return [Numeric, UNDEF, CATime::Element, nil]
82
+ def last
83
+ return self[] if scalar?
84
+ elements.zero? ? nil : self[elements - 1]
85
+ end
86
+
56
87
  # @return [Boolean] true if this is a 0-D scalar variable
57
88
  def scalar? = @dimids.empty?
58
89
 
@@ -60,7 +91,17 @@ module CA
60
91
  def nc_type = @nc_type
61
92
 
62
93
  # @return [Symbol] CArray `data_type` symbol
63
- def data_type = NC_TO_CA[@nc_type]
94
+ # An enum variable has its base type's; `nil` for a type with no
95
+ # CArray counterpart (`NC_STRING`, opaque, ...).
96
+ def data_type
97
+ return @data_type if defined?(@data_type)
98
+ t = @nc_type
99
+ if t >= NC_FIRSTUSERTYPEID
100
+ _, _, base, _, klass = nc_inq_user_type(@ncid, t)
101
+ t = base if klass == NC_ENUM
102
+ end
103
+ @data_type = NC_TO_CA[t]
104
+ end
64
105
 
65
106
  # @return [File] root file
66
107
  def file = @group.file
@@ -70,14 +111,86 @@ module CA
70
111
  # @return [Object, nil]
71
112
  def att(name) = @atts[name.to_s]
72
113
 
114
+ # The `NC_*` type an attribute is stored in, or nil if there is none.
115
+ # @param name [#to_s]
116
+ # @return [Integer, nil]
117
+ def att_type(name)
118
+ return nil unless @atts.key?(name.to_s)
119
+ @group.file.ensure_open
120
+ nc_inq_atttype(@ncid, @varid, name.to_s)
121
+ end
122
+
73
123
  # The variable's dimensions, in order. A dim may live in an ancestor
74
124
  # group (netcdf-4 dim inheritance), so resolve by name up the tree.
75
125
  # @return [Array<Dim>]
76
- def dims = @dimids.map { |d| @group.resolve_dim(nc_inq_dimname(@ncid, d)) }
126
+ # A dimension is found by its id, so a same-named one defined later
127
+ # in a subgroup does not stand in for it.
128
+ def dims
129
+ @group.file.ensure_open
130
+ @dimids.map { |d| @group.dim_by_id(d) || @group.resolve_dim(nc_inq_dimname(@ncid, d)) }
131
+ end
77
132
 
78
133
  # @return [Object] the codec object (Identity / CF / COARDS / custom)
79
134
  def codec = @group.codec
80
135
 
136
+ # The variable's storage and filter settings, in the keyword form
137
+ # {Group#def_var} takes, so that another variable can be defined
138
+ # with the same ones. Only what differs from a new variable's
139
+ # default is listed: chunk sizes, the compressors and shuffle,
140
+ # fletcher32, a byte order other than the host's, and no_fill. A
141
+ # file in a classic format has none of these, and gives `{}`.
142
+ # @return [Hash{Symbol => Object}]
143
+ def storage_options
144
+ file.ensure_open
145
+ return {} unless file.netcdf4?
146
+ opts = {}
147
+ storage, sizes = nc_inq_var_chunking(@ncid, @varid)
148
+ case storage
149
+ when NC_CHUNKED then opts[:chunks] = sizes
150
+ when NC_COMPACT then opts[:chunks] = :compact
151
+ end
152
+ shuffle, deflate, level = nc_inq_var_deflate(@ncid, @varid)
153
+ opts[:deflate] = level if deflate
154
+ opts[:shuffle] = true if shuffle
155
+ if respond_to?(:nc_inq_var_zstandard, true)
156
+ has, level = nc_inq_var_zstandard(@ncid, @varid)
157
+ opts[:zstandard] = level if has
158
+ end
159
+ if respond_to?(:nc_inq_var_bzip2, true)
160
+ has, level = nc_inq_var_bzip2(@ncid, @varid)
161
+ opts[:bzip2] = level if has
162
+ end
163
+ if respond_to?(:nc_inq_var_szip, true)
164
+ mask, pixels = nc_inq_var_szip(@ncid, @varid)
165
+ if mask != 0
166
+ coding = (mask & NC_SZIP_NN) != 0 ? :nn : :ec
167
+ opts[:szip] = { coding: coding, pixels_per_block: pixels }
168
+ end
169
+ end
170
+ if respond_to?(:nc_inq_var_blosc, true)
171
+ has, compressor, level, _blocksize, shuffle = nc_inq_var_blosc(@ncid, @varid)
172
+ if has
173
+ # blocksize is left out: libnetcdf's blosc filter stores the
174
+ # chunk's byte size in its place, not the value given.
175
+ opts[:blosc] = { compressor: BLOSC_COMPRESSOR_NAMES.fetch(compressor, compressor),
176
+ level: level,
177
+ shuffle: BLOSC_SHUFFLE_NAMES.fetch(shuffle, shuffle) }
178
+ end
179
+ end
180
+ opts[:fletcher32] = true if nc_inq_var_fletcher32(@ncid, @varid)
181
+ endian = nc_inq_var_endian(@ncid, @varid)
182
+ if endian != NC_ENDIAN_NATIVE && endian != HOST_ENDIAN
183
+ opts[:endian] = (endian == NC_ENDIAN_BIG) ? :big : :little
184
+ end
185
+ no_fill = begin
186
+ nc_inq_var_fill(@ncid, @varid)[0]
187
+ rescue NotImplementedError
188
+ false # NC_STRING: no fill setting to read
189
+ end
190
+ opts[:no_fill] = true if no_fill
191
+ return opts
192
+ end
193
+
81
194
  # @!group Read
82
195
 
83
196
  # Read (a hyperslab of) this variable with codec decoding applied.
@@ -108,7 +221,8 @@ module CA
108
221
  #
109
222
  # @param writable [Boolean] refused when true (see above).
110
223
  # @return [CArray, Numeric] decoded values (scalar for 0-D vars)
111
- # @raise [RuntimeError] when `writable: true` is given.
224
+ # @raise [RuntimeError] when `writable: true` is given, as CArray's
225
+ # own to_ca does.
112
226
  def to_ca(writable: false)
113
227
  if writable
114
228
  raise "#{self.class}#to_ca reads into a new array; it can't " \
@@ -174,7 +288,9 @@ module CA
174
288
  when CA_REG_POINT
175
289
  put_var1(info.index, encoded_scalar(value))
176
290
  when CA_REG_FLATTEN
177
- nc_put_var(@ncid, @varid, encoded_array(value))
291
+ ca = encoded_array(value)
292
+ check_write_elements(ca, @shape.inject(1, :*))
293
+ nc_put_var(@ncid, @varid, ca)
178
294
  when CA_REG_ALL
179
295
  ca = encoded_array(value)
180
296
  # Use ca.shape (not @shape) so a whole-variable write into an
@@ -185,8 +301,10 @@ module CA
185
301
  start = ca.shape.map { 0 }
186
302
  nc_put_vara(@ncid, @varid, start, ca.shape, ca)
187
303
  when CA_REG_BLOCK
188
- start, count, stride, _, reversed = block_spec(info)
189
- ca = reverse_axes(encoded_array(value, count), count, reversed)
304
+ start, count, stride, kept, reversed = block_spec(info)
305
+ encoded = encoded_array(value, count)
306
+ check_block_write_shape(encoded, kept) if value.is_a?(CArray)
307
+ ca = reverse_axes(encoded, count, reversed)
190
308
  if stride.all? { |s| s == 1 }
191
309
  nc_put_vara(@ncid, @varid, start, count, ca)
192
310
  else
@@ -208,23 +326,30 @@ module CA
208
326
  # @return [Object] the value written
209
327
  def def_att(name, value)
210
328
  @group.__transaction__ do
211
- put_var_att(@ncid, @varid, name.to_s, value, @nc_type)
329
+ put_var_att(@ncid, @varid, name.to_s, value, @nc_type, unsigned: unsigned_flag?(@atts["_Unsigned"]))
212
330
  end
213
331
  @atts = read_attributes
214
332
  value
215
333
  end
216
334
 
217
335
  # @return [String]
218
- def inspect = "#<#{self.class} #{@name}#{shape.inspect}>"
336
+ def inspect
337
+ return "#<#{self.class} #{@name} (closed)>" if @group.file.closed?
338
+ "#<#{self.class} #{@name}#{shape.inspect}>"
339
+ end
219
340
 
220
341
  # CArray methods that change their receiver without a `!` / `=` in
221
342
  # the name. Delegated, they would change a throwaway copy.
222
343
  # @api private
223
344
  IN_PLACE_METHODS = %i[
224
- fill unmask paste inherit_mask inherit_mask_replace attach
345
+ fill unmask invert_mask paste inherit_mask inherit_mask_replace attach
225
346
  load_binary set_attr
226
347
  ].freeze
227
348
 
349
+ # Operators that end in `=` but only compare.
350
+ # @api private
351
+ COMPARISON_METHODS = %i[== != <= >= ===].freeze
352
+
228
353
  # @api private
229
354
  def respond_to_missing?(id, include_private = false)
230
355
  (CArray.method_defined?(id) && !in_place?(id)) || super
@@ -236,7 +361,7 @@ module CA
236
361
  # refused: it would change the fresh array {#to_ca} returns, not the
237
362
  # file. Write with `v[…] = …`, or call it on `v.to_ca` explicitly.
238
363
  # @raise [NoMethodError] for an in-place CArray method
239
- def method_missing(id, *argv, &block)
364
+ private def method_missing(id, *argv, &block)
240
365
  return super unless CArray.method_defined?(id)
241
366
  if in_place?(id)
242
367
  raise NoMethodError.new(
@@ -249,6 +374,7 @@ module CA
249
374
 
250
375
  # @api private
251
376
  private def in_place?(id)
377
+ return false if COMPARISON_METHODS.include?(id)
252
378
  id.end_with?("!", "=") || IN_PLACE_METHODS.include?(id)
253
379
  end
254
380
 
@@ -279,9 +405,13 @@ module CA
279
405
  # through {#encoded_scalar}.
280
406
  # @api private
281
407
  private def encoded_array(value, shape = @shape)
282
- return codec.encode(self, value) if value.is_a?(CArray)
283
- ca = broadcast_scalar_to_shape(value, shape)
284
- codec.encode(self, ca)
408
+ ca = case value
409
+ when CArray then value
410
+ when CATime::Element then element_array(value, shape)
411
+ else broadcast_scalar_to_shape(value, shape)
412
+ end
413
+ refuse_timedelta(ca)
414
+ round_for_integer_variable(codec.encode(self, ca))
285
415
  end
286
416
 
287
417
  # Encode a value to a plain Numeric usable by `nc_put_var1`. Accepts
@@ -300,7 +430,7 @@ module CA
300
430
  when CATime::Element
301
431
  CA_INT64([value.value]).time(unit: value.unit)
302
432
  when CATimedelta::Element
303
- CA_INT64([value.value]).timedelta(unit: value.unit)
433
+ refuse_timedelta(value)
304
434
  when CArray
305
435
  unless value.elements == 1
306
436
  raise ArgumentError,
@@ -313,16 +443,7 @@ module CA
313
443
  w[0] = value # handles UNDEF, coerces numerics
314
444
  w
315
445
  end
316
- encoded = codec.encode(self, wrapped)
317
- # Consistency with the array-write path: for an integer target, a
318
- # float-typed CArray gets rounded (encode_scale does this when
319
- # scale/offset is present; do it unconditionally here so the point
320
- # path also rounds when the user writes 3.7 to an :int var).
321
- if encoded.is_a?(CArray) &&
322
- INTEGER_TYPES.include?(data_type) &&
323
- !INTEGER_TYPES.include?(encoded.data_type)
324
- encoded = encoded.round
325
- end
446
+ encoded = round_for_integer_variable(codec.encode(self, wrapped))
326
447
  # If the codec did NOT encode the mask into a sentinel (Identity,
327
448
  # or CF for a var without _FillValue), guard against silently
328
449
  # writing CArray's internal UNDEF representation to disk.
@@ -335,30 +456,92 @@ module CA
335
456
  encoded[0]
336
457
  end
337
458
 
459
+ # A float-typed CArray bound for an integer variable is rounded, as
460
+ # `encode_scale` rounds a packed one (CF §8.1), rather than left to
461
+ # libnetcdf's conversion, which truncates. Shared by the array and
462
+ # point write paths so `3.7` lands as `4` either way.
463
+ # @api private
464
+ private def round_for_integer_variable(encoded)
465
+ if encoded.is_a?(CArray) &&
466
+ INTEGER_TYPES.include?(data_type) &&
467
+ !INTEGER_TYPES.include?(encoded.data_type)
468
+ # A NaN left here (no codec to make it the fill) would be cast to
469
+ # a valid-looking 0.
470
+ if encoded.float? && encoded.is_nan.any
471
+ raise ArgumentError,
472
+ "cannot write NaN to integer variable #{@name}; give it a " \
473
+ "_FillValue under a CF or COARDS codec, or replace the NaNs"
474
+ end
475
+ return encoded.round
476
+ end
477
+ encoded
478
+ end
479
+
480
+ # @api private
481
+ HOST_ENDIAN = ([1].pack("S") == [1].pack("v")) ? NC_ENDIAN_LITTLE : NC_ENDIAN_BIG
482
+
483
+ # `nc_inq_var_blosc` values back to the names `def_var(blosc:)` takes
484
+ # (empty when libnetcdf is older than 4.9 and has no blosc constants).
485
+ # @api private
486
+ BLOSC_COMPRESSOR_NAMES = %i[lz lz4 lz4hc snappy zlib zstd].filter_map { |k|
487
+ c = "BLOSC_#{k.upcase}"
488
+ [::NetCDF.const_get(c), k] if ::NetCDF.const_defined?(c)
489
+ }.to_h.freeze
490
+ # @api private
491
+ BLOSC_SHUFFLE_NAMES = { NOSHUFFLE: :none, SHUFFLE: :byte, BITSHUFFLE: :bit }.filter_map { |c, k|
492
+ [::NetCDF.const_get("BLOSC_#{c}"), k] if ::NetCDF.const_defined?("BLOSC_#{c}")
493
+ }.to_h.freeze
494
+
338
495
  # @api private
339
496
  INTEGER_TYPES = %i[int8 uint8 int16 uint16 int32 uint32 int64 uint64].freeze
340
497
 
341
498
  # Widest natural type for a Ruby scalar, so wrapping a Float in a
342
- # 1-elem CArray doesn't truncate against an int variable's data_type.
499
+ # 1-elem CArray doesn't truncate against an int variable's data_type,
500
+ # and an Integer can be unpacked or reinterpreted before it is
501
+ # narrowed. An Integer outside int64 keeps the variable's type
502
+ # (uint64), and so does anything for an NC_CHAR or enum variable,
503
+ # whose buffers libnetcdf takes only in their own type.
343
504
  # @api private
505
+ INT64_RANGE = (-2**63)..(2**63 - 1)
344
506
  private def preserve_scalar_type(value)
507
+ return data_type if @nc_type == NC_CHAR || @nc_type >= NC_FIRSTUSERTYPEID
345
508
  case value
346
509
  when Float then :float64
347
- when Integer then :int64
510
+ when Integer then INT64_RANGE.cover?(value) ? :int64 : data_type
348
511
  else data_type # UNDEF, nil, ... (mask-capable of any type)
349
512
  end
350
513
  end
351
514
 
515
+ # A CATime::Element repeated over `shape`, as a CATime.
516
+ # @api private
517
+ private def element_array(elem, shape)
518
+ CArray.int64(*(shape.empty? ? [1] : shape)) { elem.value }.time(unit: elem.unit)
519
+ end
520
+
521
+ # A duration has no CF encoding here: say so rather than writing its
522
+ # ticks in whatever unit it happens to have.
523
+ # @api private
524
+ private def refuse_timedelta(value)
525
+ return unless value.is_a?(CATimedelta) || value.is_a?(CATimedelta::Element)
526
+ raise ArgumentError,
527
+ "cannot write a CATimedelta to #{@name}; write its count in the " \
528
+ "variable's units (e.g. td.ticks)"
529
+ end
530
+
352
531
  # Build a shape-matching CArray filled with `value`. CArray's
353
532
  # arity-0 block form is a documented single-evaluation fill idiom.
354
533
  # @api private
355
534
  private def broadcast_scalar_to_shape(value, shape)
535
+ # A Float goes in as float64 and an Integer as int64, so the codec
536
+ # unpacks the true value, an unsigned one is reinterpreted before
537
+ # it is narrowed, and an integer variable gets a Float rounded.
538
+ type = preserve_scalar_type(value)
356
539
  if shape.empty? # 0-D scalar variable
357
- w = CArray.new(data_type, [1])
540
+ w = CArray.new(type, [1])
358
541
  w[0] = value
359
542
  w
360
543
  else
361
- CArray.new(data_type, shape) { value }
544
+ CArray.new(type, shape) { value }
362
545
  end
363
546
  end
364
547
 
@@ -381,6 +564,11 @@ module CA
381
564
  unless argv.empty?
382
565
  raise ArgumentError, "scalar variable #{@name.inspect} takes no index"
383
566
  end
567
+ if value.is_a?(CArray) && value.elements != 1
568
+ raise ArgumentError,
569
+ "scalar variable #{@name.inspect} needs a single value; " \
570
+ "got a #{value.shape.inspect} array"
571
+ end
384
572
  ca = encoded_array(value) # 1-element CArray of var type
385
573
  nc_put_var(@ncid, @varid, ca)
386
574
  end
@@ -406,18 +594,79 @@ module CA
406
594
  out = reverse_axes(out, count, reversed)
407
595
  # drop point-indexed axes (size 1) by reshaping to the kept shape
408
596
  (kept.empty? || kept.length == count.length) ? out : out.reshape(*kept)
409
- when CA_REG_SELECT, CA_REG_GRID
597
+ when CA_REG_GRID
598
+ read_grid(argv) || nc_get_var(@ncid, @varid)[*argv]
599
+ when CA_REG_MAPPING
600
+ read_mapping(argv[0]) || nc_get_var(@ncid, @varid)[*argv]
601
+ when CA_REG_SELECT
602
+ # A boolean mask covers the whole variable.
410
603
  nc_get_var(@ncid, @varid)[*argv]
411
604
  else
412
605
  raise ArgumentError, "unsupported index for reading #{@name}"
413
606
  end
414
607
  end
415
608
 
609
+ # Index arrays per axis (`v[CA_INT([0, 7]), nil, 2..5]`): read the
610
+ # block that holds what they pick, and pick from it, rather than the
611
+ # whole variable. nil when an axis takes a form not handled here
612
+ # (the caller then reads the whole variable).
613
+ # @api private
614
+ private def read_grid(argv)
615
+ return nil unless argv.size == @shape.size
616
+ start, count, local = [], [], []
617
+ argv.each_with_index do |idx, axis|
618
+ n = @shape[axis]
619
+ case idx
620
+ when nil
621
+ start << 0; count << n; local << nil
622
+ when CArray
623
+ return nil unless INTEGER_TYPES.include?(idx.data_type)
624
+ i = idx.to_ca
625
+ i = i + (i.lt(0) * n) if i.lt(0).any # negative from the end
626
+ lo, hi = i.min, i.max
627
+ return nil unless lo.is_a?(Integer) && lo >= 0 && hi < n
628
+ start << lo; count << hi - lo + 1; local << i - lo
629
+ when Range
630
+ b = CArray.scan_index([n], [idx])
631
+ return nil unless b.type == CA_REG_BLOCK
632
+ first, len, step = b.index[0]
633
+ return nil unless step == 1
634
+ start << first; count << len; local << (0...len)
635
+ else
636
+ return nil
637
+ end
638
+ end
639
+ return nil if count.include?(0)
640
+ nc_get_vara(@ncid, @varid, start, count)[*local]
641
+ end
642
+
643
+ # A flat array of addresses (`v[CA_INT([1, 7, -1])]`) picking a few
644
+ # elements: read those one by one. nil for many of them.
645
+ # @api private
646
+ MAPPING_POINT_LIMIT = 1024
647
+ private def read_mapping(addrs)
648
+ return nil unless addrs.is_a?(CArray) && INTEGER_TYPES.include?(addrs.data_type)
649
+ return nil if addrs.elements > MAPPING_POINT_LIMIT || data_type.nil?
650
+ total = @shape.inject(1, :*)
651
+ out = CArray.new(data_type, addrs.shape)
652
+ addrs.to_a.flatten.each_with_index do |a, k|
653
+ a += total if a < 0
654
+ raise IndexError, "index #{a} is out of range for #{@name}" unless (0...total).cover?(a)
655
+ out[k] = get_var1(addr_to_index(a))
656
+ end
657
+ out
658
+ end
659
+
416
660
  # One element. An NC_CHAR element travels as a 1-char String in the
417
661
  # netcdf gem's var1 calls; here it is the byte, as it is in an array
418
662
  # read (NC_CHAR maps to :uint8).
419
663
  # @api private
420
664
  private def get_var1(index)
665
+ if @nc_type == NC_STRING
666
+ raise NotImplementedError,
667
+ "no CArray type for an NC_STRING variable; read it with " \
668
+ "NetCDF.nc_get_var, which returns Strings"
669
+ end
421
670
  v = nc_get_var1(@ncid, @varid, index)
422
671
  @nc_type == NC_CHAR && v.is_a?(String) ? (v.getbyte(0) || 0) : v
423
672
  end
@@ -439,6 +688,23 @@ module CA
439
688
  "got #{shape.inspect}"
440
689
  end
441
690
 
691
+ # A block write takes a CArray shaped like the block, as CArray's own
692
+ # block assignment does (size-1 axes aside); otherwise the cells
693
+ # would land in the wrong places, or some be dropped.
694
+ # @api private
695
+ private def check_block_write_shape(ca, kept)
696
+ return if ca.shape.reject { |n| n == 1 } == kept.reject { |n| n == 1 }
697
+ raise ArgumentError,
698
+ "shape mismatch writing #{@name}: block #{kept.inspect} <- #{ca.shape.inspect}"
699
+ end
700
+
701
+ # @api private
702
+ private def check_write_elements(ca, n)
703
+ return if ca.elements == n
704
+ raise ArgumentError,
705
+ "elements mismatch writing #{@name}: #{n} <- #{ca.elements}"
706
+ end
707
+
442
708
  # -> [start, count, stride, kept_shape, reversed]; kept_shape drops
443
709
  # point axes. libnetcdf takes only a positive stride, so a reversed
444
710
  # range (`(9..0).step(-1)`) becomes the same cells walked forward,
@@ -488,6 +754,7 @@ module CA
488
754
  # effect.
489
755
  # @api private
490
756
  private def refresh_shape
757
+ @group.file.ensure_open
491
758
  @shape = @dimids.map { |d| nc_inq_dimlen(@ncid, d) }
492
759
  end
493
760
 
@@ -503,7 +770,19 @@ module CA
503
770
  # meaning there, so it is refused instead of landing near 2**31.
504
771
  # @api private
505
772
  private def scan_index_for_write(argv)
506
- [CArray.scan_index(@shape, argv), @shape]
773
+ info = CArray.scan_index(@shape, argv)
774
+ # A range scanned against the current length is clipped to it, so
775
+ # `v[0..1] = ...` on an empty unlimited axis would write nothing.
776
+ # An index that scans the same whatever the unlimited length is
777
+ # means what it says; take it when it reaches further.
778
+ if @unlimited.any?
779
+ wide = CArray.scan_index(shape_for_write(INT_MAX_SCAN), argv) rescue nil
780
+ wider = CArray.scan_index(shape_for_write(INT_MAX_SCAN - 1), argv) rescue nil
781
+ if wide && wide == wider && wide != info
782
+ return [wide, shape_for_write(INT_MAX_SCAN)]
783
+ end
784
+ end
785
+ [info, @shape]
507
786
  rescue IndexError
508
787
  raise unless @unlimited.any?
509
788
  shape = shape_for_write(INT_MAX_SCAN)
@@ -530,7 +809,8 @@ module CA
530
809
  atts = {}
531
810
  natts.times do |i|
532
811
  name = nc_inq_attname(@ncid, @varid, i)
533
- atts[name] = nc_get_att(@ncid, @varid, name)
812
+ value = readable_att(@ncid, @varid, name, @name)
813
+ atts[name] = deep_freeze(value) unless value.equal?(UNREADABLE_ATT)
534
814
  end
535
815
  atts.freeze
536
816
  end
@@ -1,5 +1,5 @@
1
1
  module CA
2
2
  module NetCDF
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
5
5
  end