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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -2
- data/CONTRIBUTING.md +3 -3
- data/README.md +53 -21
- data/bin/ncheader +43 -8
- data/bin/ncvar +48 -12
- data/carray-netcdf.gemspec +4 -4
- data/docs/00_introduction.md +19 -30
- data/docs/01_getting_started.md +16 -25
- data/docs/02_reading.md +26 -26
- data/docs/03_writing.md +53 -44
- data/docs/04_codec.md +74 -82
- data/docs/05_groups.md +5 -5
- data/docs/06_low_level_api.md +161 -0
- data/docs/07_cookbook.md +167 -126
- data/docs/08_cheatsheet.md +77 -52
- data/docs/09_cli.md +4 -4
- data/lib/carray/netcdf/codec.rb +223 -29
- data/lib/carray/netcdf/dim.rb +8 -5
- data/lib/carray/netcdf/file.rb +184 -57
- data/lib/carray/netcdf/group.rb +301 -43
- data/lib/carray/netcdf/var.rb +311 -31
- data/lib/carray/netcdf/version.rb +1 -1
- data/lib/carray/netcdf.rb +99 -17
- metadata +7 -7
- data/docs/06_layer1.md +0 -160
data/lib/carray/netcdf/codec.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
|
|
1
3
|
module CA
|
|
2
4
|
module NetCDF
|
|
3
5
|
# Value codec — the encode/decode half of a NetCDF "convention".
|
|
@@ -57,10 +59,15 @@ module CA
|
|
|
57
59
|
when :auto
|
|
58
60
|
# The whole string first, then each of its tokens, so
|
|
59
61
|
# "ACDD-1.3, CF-1.8" still finds CF.
|
|
62
|
+
# A non-String attribute (an NC_STRING array, a number) is
|
|
63
|
+
# tried only through its String tokens.
|
|
60
64
|
conv = file.att("Conventions")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
conv = nil unless conv.is_a?(String)
|
|
66
|
+
hit = (conv && @registry.find { |c| c.claims?(conv) }) ||
|
|
67
|
+
@registry.find { |c| file.conventions.any? { |t| c.claims?(t) } }
|
|
68
|
+
return build(hit) if hit
|
|
69
|
+
warn_cf_without_conventions(file) if conv.nil? && file.conventions.empty?
|
|
70
|
+
Identity
|
|
64
71
|
when :identity, false, nil
|
|
65
72
|
Identity
|
|
66
73
|
when Symbol
|
|
@@ -82,6 +89,23 @@ module CA
|
|
|
82
89
|
|
|
83
90
|
private
|
|
84
91
|
|
|
92
|
+
# Attributes that make a variable look CF-encoded.
|
|
93
|
+
CF_LOOKING_ATTS = %w[_FillValue missing_value scale_factor add_offset].freeze
|
|
94
|
+
|
|
95
|
+
# xarray and netCDF4-python write CF attributes but no Conventions,
|
|
96
|
+
# so such a file reads as stored. Say so once, and how to decode it.
|
|
97
|
+
def warn_cf_without_conventions(file)
|
|
98
|
+
v = file.vars.each_value.find do |var|
|
|
99
|
+
atts = var.atts
|
|
100
|
+
CF_LOOKING_ATTS.any? { |k| atts.key?(k) } ||
|
|
101
|
+
(atts["units"].is_a?(String) && atts["units"].include?(" since "))
|
|
102
|
+
end
|
|
103
|
+
return unless v
|
|
104
|
+
warn "carray-netcdf: #{file.filename} has CF attributes (on #{v.name.inspect}) " \
|
|
105
|
+
"but no Conventions attribute naming CF or COARDS, so values are read " \
|
|
106
|
+
"as stored; open it with codec: :cf to decode them"
|
|
107
|
+
end
|
|
108
|
+
|
|
85
109
|
def codec?(obj) = obj.respond_to?(:decode) && obj.respond_to?(:encode)
|
|
86
110
|
|
|
87
111
|
def symbol_class(sym)
|
|
@@ -142,7 +166,7 @@ module CA
|
|
|
142
166
|
ca = decode_unsigned(var, ca) # signed disk -> unsigned user
|
|
143
167
|
ca = decode_fill(var, a, ca) # (in unsigned/packed space)
|
|
144
168
|
ca = decode_valid(var, a, ca) if @mask_valid_range # (same)
|
|
145
|
-
ca = decode_scale(a, ca)
|
|
169
|
+
ca = decode_scale(var, a, ca) # packed -> physical
|
|
146
170
|
ca = decode_time(var, a, ca) if time_units?(a["units"])
|
|
147
171
|
ca
|
|
148
172
|
end
|
|
@@ -154,6 +178,7 @@ module CA
|
|
|
154
178
|
def encode(var, ca)
|
|
155
179
|
a = var.atts
|
|
156
180
|
ca = encode_time(var, a, ca) if time_units?(a["units"])
|
|
181
|
+
ca = mask_nan(var, ca)
|
|
157
182
|
ca = encode_scale(var, a, ca)
|
|
158
183
|
ca = encode_fill(var, a, ca)
|
|
159
184
|
ca = encode_unsigned(var, ca) # unsigned user -> signed disk
|
|
@@ -285,15 +310,36 @@ module CA
|
|
|
285
310
|
|
|
286
311
|
# `unpacked = packed * scale_factor + add_offset` (CF §8.1)
|
|
287
312
|
# @api private
|
|
288
|
-
private def decode_scale(atts, ca)
|
|
313
|
+
private def decode_scale(var, atts, ca)
|
|
289
314
|
sf = atts["scale_factor"]
|
|
290
315
|
ao = atts["add_offset"]
|
|
316
|
+
return ca unless sf || ao
|
|
317
|
+
# CF: the unpacked type is the attributes'. float32 attributes
|
|
318
|
+
# unpack in float32, as xarray and netCDF4-python do.
|
|
319
|
+
if !ca.float? || ca.data_type == :float32
|
|
320
|
+
names = %w[scale_factor add_offset].select { |k| atts.key?(k) }
|
|
321
|
+
if names.all? { |k| var.respond_to?(:att_type) && var.att_type(k) == ::NetCDF::NC_FLOAT }
|
|
322
|
+
return unpack(ca.to_type(:float32), sf, ao)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
# Integer attributes keep integer arithmetic in the packed type,
|
|
326
|
+
# which overflows (a short times 200); widen to int64 first.
|
|
327
|
+
if INTEGER_TYPES.include?(ca.data_type) && (sf.is_a?(Integer) || ao.is_a?(Integer))
|
|
328
|
+
ca = ca.to_type(:int64)
|
|
329
|
+
end
|
|
330
|
+
unpack(ca, sf, ao)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# @api private
|
|
334
|
+
private def unpack(ca, sf, ao)
|
|
291
335
|
ca = ca * sf if sf
|
|
292
336
|
ca = ca + ao if ao
|
|
293
337
|
ca
|
|
294
338
|
end
|
|
295
339
|
|
|
296
340
|
INTEGER_TYPES = %i[int8 uint8 int16 uint16 int32 uint32 int64 uint64].freeze
|
|
341
|
+
INT64_MIN = -2**63
|
|
342
|
+
INT64_MAX = 2**63 - 1
|
|
297
343
|
|
|
298
344
|
# `packed = round((unpacked - add_offset) / scale_factor)`
|
|
299
345
|
# (A bare cast to an integer variable would truncate:
|
|
@@ -303,18 +349,41 @@ module CA
|
|
|
303
349
|
sf = atts["scale_factor"]
|
|
304
350
|
ao = atts["add_offset"]
|
|
305
351
|
return ca unless sf || ao
|
|
352
|
+
# Integer values would be divided as integers (truncating) and
|
|
353
|
+
# could overflow their type; work in float64 and round below.
|
|
354
|
+
ca = ca.to_type(:float64) if INTEGER_TYPES.include?(ca.data_type)
|
|
306
355
|
ca = ca - ao if ao
|
|
307
356
|
ca = ca / sf if sf
|
|
308
357
|
ca = ca.round if INTEGER_TYPES.include?(var.data_type)
|
|
309
358
|
ca
|
|
310
359
|
end
|
|
311
360
|
|
|
361
|
+
# NaN is how float data marks a missing value (xarray writes it so).
|
|
362
|
+
# An integer variable cannot hold it — cast, it would become a
|
|
363
|
+
# valid-looking 0 — so it is masked here and written as the fill.
|
|
364
|
+
# @api private
|
|
365
|
+
private def mask_nan(var, ca)
|
|
366
|
+
return ca unless ca.is_a?(CArray) && ca.float? && INTEGER_TYPES.include?(var.data_type)
|
|
367
|
+
nan = ca.is_nan
|
|
368
|
+
return ca unless nan.any
|
|
369
|
+
ca = ca.copy
|
|
370
|
+
ca[nan] = UNDEF
|
|
371
|
+
ca
|
|
372
|
+
end
|
|
373
|
+
|
|
312
374
|
# ---- mask <-> fill --------------------------------------------------
|
|
313
375
|
|
|
314
376
|
# @api private
|
|
315
377
|
private def encode_fill(var, atts, ca)
|
|
316
378
|
return ca unless ca.is_a?(CArray) && ca.has_mask?
|
|
317
|
-
|
|
379
|
+
# Values still in the signed on-disk type take the stored fill;
|
|
380
|
+
# unsigned ones take it reinterpreted, as fill_values gives it.
|
|
381
|
+
fv = if unsigned?(var) && ca.data_type == var.data_type
|
|
382
|
+
atts["_FillValue"] || Array(atts["missing_value"]).first
|
|
383
|
+
else
|
|
384
|
+
fill_values(var, atts).first
|
|
385
|
+
end
|
|
386
|
+
fv or
|
|
318
387
|
raise ArgumentError,
|
|
319
388
|
"variable '#{var.name}' has masked values but no _FillValue " \
|
|
320
389
|
"to encode them"
|
|
@@ -383,10 +452,55 @@ module CA
|
|
|
383
452
|
return nil unless m
|
|
384
453
|
count = (m[1] || "1").to_i
|
|
385
454
|
base = UNIT_WORDS[m[2].downcase] or return nil
|
|
386
|
-
|
|
455
|
+
# UDUNITS writes short years ("days since 1-1-1"); CATime would
|
|
456
|
+
# read a one- or two-digit year as 20xx / 19xx, so pad it.
|
|
457
|
+
ref = ref.strip.sub(/\A(-?)(\d{1,3})(?=-)/) { "#{$1}#{$2.rjust(4, '0')}" }
|
|
458
|
+
[count, base, ref]
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# The unit to work in for a reference time: `base` when the
|
|
462
|
+
# reference falls on its grid, else the first finer fixed-length
|
|
463
|
+
# unit it falls on (`"days since 2000-01-01 12:00"` works in hours).
|
|
464
|
+
# nil when there is none — a `:Y` / `:M` base whose reference is not
|
|
465
|
+
# the head of a year / month. Cached per instance.
|
|
466
|
+
# @api private
|
|
467
|
+
private def origin_unit(base, ref)
|
|
468
|
+
@origin_units ||= {}
|
|
469
|
+
key = [base, ref]
|
|
470
|
+
return @origin_units[key] if @origin_units.key?(key)
|
|
471
|
+
units = NS_PER_UNIT.key?(base) ? NS_PER_UNIT.keys.drop_while { |u| u != base } : [base]
|
|
472
|
+
@origin_units[key] = units.find { |u| origin_on_grid?(u, ref) }
|
|
387
473
|
end
|
|
388
474
|
|
|
389
|
-
#
|
|
475
|
+
# The first day of the Gregorian calendar; under `standard` /
|
|
476
|
+
# `gregorian` (CF's mixed calendar) a date before it is Julian.
|
|
477
|
+
GREGORIAN_REFORM = Date.new(1582, 10, 15, Date::GREGORIAN)
|
|
478
|
+
|
|
479
|
+
# A reference time before the reform, in the mixed calendar, is a
|
|
480
|
+
# Julian date. CATime is proleptic Gregorian, so the same day is
|
|
481
|
+
# relabelled (`1-1-1` is `0000-12-30`); every instant from the reform
|
|
482
|
+
# on then comes out as cftime / UDUNITS give it. -> [ref, shifted?]
|
|
483
|
+
# @api private
|
|
484
|
+
private def mixed_calendar_reference(ref, calendar)
|
|
485
|
+
return [ref, false] unless calendar.nil? || %w[standard gregorian].include?(calendar.to_s.strip.downcase)
|
|
486
|
+
m = ref.match(/\A(-?\d+)-(\d{1,2})-(\d{1,2})(.*)\z/m) or return [ref, false]
|
|
487
|
+
y, mo, d = m[1].to_i, m[2].to_i, m[3].to_i
|
|
488
|
+
return [ref, false] unless Date.valid_date?(y, mo, d, Date::JULIAN)
|
|
489
|
+
julian = Date.new(y, mo, d, Date::JULIAN)
|
|
490
|
+
return [ref, false] unless julian < GREGORIAN_REFORM
|
|
491
|
+
g = julian.gregorian
|
|
492
|
+
return [format("%04d-%02d-%02d%s", g.year, g.month, g.day, m[4]), true]
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# @api private
|
|
496
|
+
private def origin_on_grid?(unit, ref)
|
|
497
|
+
CA_INT64([0]).time(unit: unit, origin: ref)
|
|
498
|
+
return true
|
|
499
|
+
rescue ArgumentError
|
|
500
|
+
return false
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# See docs/04_codec.md §"N-of-a-unit intervals" for why we fold
|
|
390
504
|
# count into the raw values instead of storing at `count × base`
|
|
391
505
|
# resolution.
|
|
392
506
|
# @api private
|
|
@@ -409,15 +523,61 @@ module CA
|
|
|
409
523
|
return ca
|
|
410
524
|
end
|
|
411
525
|
count, base, ref = parsed
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
526
|
+
ref, shifted = mixed_calendar_reference(ref, calendar)
|
|
527
|
+
unit = origin_unit(base, ref)
|
|
528
|
+
unless unit
|
|
529
|
+
warn_once(:origin, units,
|
|
530
|
+
"variable #{var.name.inspect} has units #{units.inspect} " \
|
|
531
|
+
"whose reference time is not on the #{base} grid; " \
|
|
532
|
+
"raw values returned")
|
|
533
|
+
return ca
|
|
534
|
+
end
|
|
535
|
+
# Values in `count × base` steps, counted in `unit`.
|
|
536
|
+
f = count * (NS_PER_UNIT.key?(base) ? NS_PER_UNIT[base] / NS_PER_UNIT[unit] : 1)
|
|
537
|
+
if ca.float?
|
|
538
|
+
vals = f == 1 ? ca : ca * f
|
|
539
|
+
tick_unit, ticks = time_ticks(vals, unit)
|
|
540
|
+
return check_reform(var, ticks.time(unit: tick_unit, origin: ref), shifted)
|
|
416
541
|
end
|
|
417
|
-
ints =
|
|
418
|
-
|
|
542
|
+
ints = ca.data_type == :int64 ? ca : ca.int64
|
|
543
|
+
# xarray writes NaT as the smallest int64, with no _FillValue.
|
|
544
|
+
if ca.data_type == :int64 && ! atts.key?("_FillValue")
|
|
545
|
+
nat = ints.eq(INT64_MIN)
|
|
546
|
+
if nat.any
|
|
547
|
+
ints = ints.copy
|
|
548
|
+
ints[nat] = UNDEF
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
unless f == 1
|
|
552
|
+
# Counted in a finer unit, a value may no longer fit int64.
|
|
553
|
+
limit = ints.abs.max
|
|
554
|
+
if limit.is_a?(Integer) && limit > INT64_MAX / f
|
|
555
|
+
warn_once(:overflow, units,
|
|
556
|
+
"variable #{var.name.inspect} has values too large to count " \
|
|
557
|
+
"in #{unit} for units #{units.inspect}; raw values returned")
|
|
558
|
+
return ca
|
|
559
|
+
end
|
|
560
|
+
ints = ints * f
|
|
561
|
+
end
|
|
562
|
+
check_reform(var, ints.time(unit: unit, origin: ref), shifted)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# With a pre-reform reference, an instant before the reform is a
|
|
566
|
+
# Julian date that CATime shows as proleptic Gregorian. Say so.
|
|
567
|
+
# @api private
|
|
568
|
+
private def check_reform(var, t, shifted)
|
|
569
|
+
if shifted && (t < reform_instant).any
|
|
570
|
+
warn_once(:reform, var.name,
|
|
571
|
+
"variable #{var.name.inspect} has times before 1582-10-15 " \
|
|
572
|
+
"in the mixed Julian/Gregorian calendar; they are shown as " \
|
|
573
|
+
"proleptic Gregorian, not as the Julian dates the file means")
|
|
574
|
+
end
|
|
575
|
+
t
|
|
419
576
|
end
|
|
420
577
|
|
|
578
|
+
# @api private
|
|
579
|
+
private def reform_instant = (@reform_instant ||= CArray.time(["1582-10-15"], unit: :D)[0])
|
|
580
|
+
|
|
421
581
|
# Length of each fixed-length unit, in ns, finest last. `:Y` / `:M`
|
|
422
582
|
# have no fixed length, so a fraction of one can't be carried to a
|
|
423
583
|
# finer unit.
|
|
@@ -436,8 +596,11 @@ module CA
|
|
|
436
596
|
eps = vals.data_type == :float32 ? 1.2e-7 : 2.3e-16
|
|
437
597
|
limit = vals.abs.max
|
|
438
598
|
limit = 0.0 unless limit.is_a?(Numeric) # all masked
|
|
599
|
+
# Whole within the rounding error of the values themselves; an
|
|
600
|
+
# absolute floor would round away real fractions (50 ms of a day).
|
|
601
|
+
scale = [limit, 1.0].max
|
|
439
602
|
whole = ->(x, f) { d = (x - x.round).abs.max
|
|
440
|
-
!d.is_a?(Numeric) || d <=
|
|
603
|
+
!d.is_a?(Numeric) || d <= 4 * scale * f * eps }
|
|
441
604
|
return [base, vals.round.int64] if whole.(vals, 1) || !NS_PER_UNIT.key?(base)
|
|
442
605
|
best = [base, vals]
|
|
443
606
|
NS_PER_UNIT.each_key.drop_while { |u| u != base }.drop(1).each do |u|
|
|
@@ -465,21 +628,52 @@ module CA
|
|
|
465
628
|
"cannot encode CATime into variable #{var.name.inspect}: " \
|
|
466
629
|
"units #{units.inspect} is not a CF time-since spec"
|
|
467
630
|
count, base, ref = parsed
|
|
631
|
+
ref, shifted = mixed_calendar_reference(ref, calendar)
|
|
632
|
+
if shifted && (ca < reform_instant).any
|
|
633
|
+
raise ArgumentError,
|
|
634
|
+
"cannot encode CATime into variable #{var.name.inspect}: instants " \
|
|
635
|
+
"before 1582-10-15 would be Julian dates in its mixed calendar; " \
|
|
636
|
+
"use calendar: \"proleptic_gregorian\""
|
|
637
|
+
end
|
|
638
|
+
unit = origin_unit(base, ref) or
|
|
639
|
+
raise ArgumentError,
|
|
640
|
+
"cannot encode CATime into variable #{var.name.inspect}: " \
|
|
641
|
+
"the reference time of units #{units.inspect} is not on the #{base} grid"
|
|
642
|
+
return encode_calendar_time(var, ca, count, base, ref, units) unless NS_PER_UNIT.key?(base)
|
|
643
|
+
# A month or year instant is a real instant on a finer grid.
|
|
644
|
+
ca = ca.to_unit(unit) unless NS_PER_UNIT.key?(ca.unit.base)
|
|
468
645
|
fine = ca.unit.base
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
646
|
+
work = finer_unit?(fine, unit) ? fine : unit
|
|
647
|
+
# One `count × base` step, in `work` ticks.
|
|
648
|
+
f = NS_PER_UNIT[base] / NS_PER_UNIT[work] * count
|
|
649
|
+
ticks = ca.timesteps(unit: work, origin: ref)
|
|
650
|
+
return (f == 1 ? ticks : ticks.float64 / f) unless INTEGER_TYPES.include?(var.data_type)
|
|
651
|
+
return ticks if f == 1
|
|
652
|
+
unless (ticks % f).eq(0).all
|
|
653
|
+
raise ArgumentError,
|
|
654
|
+
"cannot encode CATime into integer variable #{var.name.inspect}: " \
|
|
655
|
+
"some instants fall between the steps of units #{units.inspect}"
|
|
656
|
+
end
|
|
657
|
+
ticks / f
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Encode into `months since` / `years since` units. A month or year
|
|
661
|
+
# has no fixed length, so an instant between two steps can't be
|
|
662
|
+
# written, even into a float variable, and raises.
|
|
663
|
+
# @api private
|
|
664
|
+
private def encode_calendar_time(var, ca, count, base, ref, units)
|
|
665
|
+
warn_once(:calendar_units, units,
|
|
666
|
+
"variable #{var.name.inspect} is written in #{units.inspect} as " \
|
|
667
|
+
"calendar months / years; xarray and cftime refuse such units under " \
|
|
668
|
+
"this calendar (UDUNITS takes a month as 30.436875 days), so other " \
|
|
669
|
+
"tools may not read it")
|
|
670
|
+
on_grid = ca.to_unit(base).to_unit(ca.unit.base)
|
|
671
|
+
unless on_grid.ticks.eq(ca.ticks).all
|
|
672
|
+
raise ArgumentError,
|
|
673
|
+
"cannot encode CATime into variable #{var.name.inspect}: " \
|
|
674
|
+
"some instants fall between the steps of units #{units.inspect}"
|
|
481
675
|
end
|
|
482
|
-
ticks = ca.timesteps(unit: base, origin: ref)
|
|
676
|
+
ticks = ca.to_unit(base).timesteps(unit: base, origin: ref)
|
|
483
677
|
return ticks if count == 1
|
|
484
678
|
unless (ticks % count).eq(0).all
|
|
485
679
|
raise ArgumentError,
|
|
@@ -498,7 +692,7 @@ module CA
|
|
|
498
692
|
|
|
499
693
|
# @api private
|
|
500
694
|
private def supported_calendar?(cal)
|
|
501
|
-
cal.nil? || SUPPORTED_CALENDARS.include?(cal.to_s)
|
|
695
|
+
cal.nil? || SUPPORTED_CALENDARS.include?(cal.to_s.strip.downcase)
|
|
502
696
|
end
|
|
503
697
|
|
|
504
698
|
# Per-instance cache (a new CF instance is built per
|
data/lib/carray/netcdf/dim.rb
CHANGED
|
@@ -5,6 +5,9 @@ module CA
|
|
|
5
5
|
class Dim
|
|
6
6
|
|
|
7
7
|
include CA::NetCDF
|
|
8
|
+
# The low-level calls are this class's helpers, not its interface.
|
|
9
|
+
private :nc_get_var, :nc_get_vara, :nc_get_vars, :nc_get_varm,
|
|
10
|
+
:nc_put_var, :nc_put_vara, :nc_put_vars, :nc_put_varm
|
|
8
11
|
|
|
9
12
|
# @param group [Group] containing group
|
|
10
13
|
# @param dimid [Integer] NetCDF dimension id within `group`
|
|
@@ -17,11 +20,10 @@ module CA
|
|
|
17
20
|
@name = nc_inq_dimname(@ncid, @dimid)
|
|
18
21
|
# Whether a dim is unlimited is a define-time property and doesn't
|
|
19
22
|
# change over the file's lifetime — cache it once. Asked of the
|
|
20
|
-
# dim's own group, which is where netcdf-4 keeps it
|
|
21
|
-
#
|
|
22
|
-
# further one is known here only when this gem defined it.
|
|
23
|
+
# dim's own group, which is where netcdf-4 keeps it; a NetCDF-4
|
|
24
|
+
# group can have several.
|
|
23
25
|
@unlimited = if unlimited.nil?
|
|
24
|
-
|
|
26
|
+
(nc_inq_unlimdims(@ncid) rescue []).include?(@dimid)
|
|
25
27
|
else
|
|
26
28
|
unlimited
|
|
27
29
|
end
|
|
@@ -37,7 +39,7 @@ module CA
|
|
|
37
39
|
# dim grows as writes come in, so a value cached at open time goes
|
|
38
40
|
# stale.
|
|
39
41
|
# @return [Integer]
|
|
40
|
-
def length = nc_inq_dimlen(@ncid, @dimid)
|
|
42
|
+
def length = (@group.file.ensure_open; nc_inq_dimlen(@ncid, @dimid))
|
|
41
43
|
|
|
42
44
|
# @return [Boolean] true if this is the unlimited dimension
|
|
43
45
|
def unlimited? = @unlimited
|
|
@@ -60,6 +62,7 @@ module CA
|
|
|
60
62
|
|
|
61
63
|
# @return [String]
|
|
62
64
|
def inspect
|
|
65
|
+
return "#<#{self.class} #{@name} (closed)>" if @group.file.closed?
|
|
63
66
|
"#<#{self.class} #{@name}=#{length}#{@unlimited ? '(unlimited)' : ''}>"
|
|
64
67
|
end
|
|
65
68
|
end
|