neu-mods 0.4.0 → 0.5.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: 89680abe072d9ece72ed12e15a8301aca55c7a8a3ec5f6ee9fb092ebd507eeac
4
- data.tar.gz: d65dddefd46aa15b4408cd1ba952faf24d603edfb59b5b1b556e46ea982875d7
3
+ metadata.gz: 1469790b96ff15516d9766db6673159fb9ad14128e02c4399842580c198a1740
4
+ data.tar.gz: 97c442fbd0765c6de753bbb140da3fa2ac61fedcb63104d38801c569a876ddcd
5
5
  SHA512:
6
- metadata.gz: 882dd1f0cd4f57a90d55c1c06ba67e3f5595e68fe8908eb40069d1dbf9215e72685ddf48c832ebe48b510aac1063cfe2ed251c7dc5dd06130b56fd357c432237
7
- data.tar.gz: fcb62c4fe88773b8224938a35fc254a7c0eb22845e3084f257b3fa1ed8c49b95da7aa6c81d7531868cfc0d772db594bd2098884dce8f4a4bc98ea37358f359d7
6
+ metadata.gz: 5b2fdf0e42f5fd65fccbc732ddf4a15b3f311f4719a3a95a294517da643c8005dcbb3c25a7b3e2b1e7afcb67c1f29f9cecacf7ec157640ef3a2b2d56e13197ac
7
+ data.tar.gz: 4ffc16684b6176a667fcf62dadc6e20c5fed2733c1f6bdcd39df345fe6df77f5cd412f62fe879676f1075f7c690ca6cb368b6876bfca0e4a98477af8112731c2
data/.version CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.5.0
data/README.md CHANGED
@@ -33,6 +33,11 @@ doc.title_parts # => { non_sort:, subtitle:, title:, part_name:, part_number:
33
33
  doc.abstract # => normalized, paragraph-joined String
34
34
  doc.topical_subjects # => ["Civil society", ...] (every <topic>, for the access copy)
35
35
  doc.keywords # => [...] (only the editable attribute-free keyword subjects)
36
+ doc.date_created_with_precision
37
+ # => [DateTime, "year"|"month"|"day"] w3cdtf YYYY, YYYY-MM
38
+ # and YYYY-MM-DD all parse; the precision says which shape
39
+ # the record declared, so display cannot invent a month or
40
+ # a day the record never claimed
36
41
  doc.to_h # => full projection, keyed to Atlas's Metadata::MODS attributes
37
42
 
38
43
  # Pure title composition (no document needed) — for callers that already hold
@@ -171,32 +171,48 @@ module NEU
171
171
  node && clean(node.text)
172
172
  end
173
173
 
174
- # Parsed dateCreated, or nil if no originInfo/dateCreated, or "" if present
175
- # but unparseable (mirrors Atlas's safe_date_parse rescue).
176
- def date_created
174
+ # The three w3cdtf date shapes a dateCreated may stop at: year, year-month,
175
+ # or a full date. Matching the shape explicitly, rather than widening
176
+ # DateTime.parse, is what lets the declared precision fall out of the parse
177
+ # instead of being guessed after it.
178
+ W3CDTF_DATE = /\A(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?\z/
179
+
180
+ # Parsed dateCreated paired with the granularity the record declared, as
181
+ # [value, precision]. value is nil if no originInfo/dateCreated, or "" if
182
+ # present but unparseable (mirrors Atlas's safe_date_parse rescue).
183
+ # precision is "year", "month" or "day", and nil whenever value is not a
184
+ # DateTime.
185
+ #
186
+ # The precision has to be captured here, at the only point where the shape
187
+ # is still visible: a year-only date parses to January 1st, and no consumer
188
+ # downstream can tell that month and day from a record that claimed them.
189
+ # A preservation repository must not project a precision it was not given.
190
+ def date_created_with_precision
177
191
  node = doc.at_xpath("/mods:mods/mods:originInfo/mods:dateCreated", NAMESPACE)
178
- return nil unless node
192
+ return [nil, nil] unless node
179
193
 
180
194
  str = NEU::MODS.canonical_ws(node.text)
181
- return nil if str.empty?
195
+ return [nil, nil] if str.empty?
182
196
 
183
- begin
184
- DateTime.parse(str)
185
- rescue Date::Error
186
- ""
187
- end
197
+ parse_w3cdtf(str)
188
198
  end
189
199
 
200
+ def date_created = date_created_with_precision.first
201
+ def date_created_precision = date_created_with_precision.last
202
+
190
203
  # --- Full projection -----------------------------------------------------
191
204
 
192
205
  # The complete read projection, keyed to Atlas's Metadata::MODS attribute
193
206
  # names -- a drop-in source for `convert_xml_to_json`.
194
207
  def to_h
208
+ date, date_precision = date_created_with_precision
209
+
195
210
  {
196
211
  main_title: access_title_parts,
197
212
  names: names,
198
213
  languages: languages,
199
- date_created: date_created,
214
+ date_created: date,
215
+ date_created_precision: date_precision,
200
216
  resource_type: resource_type,
201
217
  genres: genres,
202
218
  format: format,
@@ -223,6 +239,26 @@ module NEU
223
239
 
224
240
  # --- helpers -------------------------------------------------------------
225
241
 
242
+ # A shape-matched but impossible date (2026-13, 2026-02-30) reaches DateTime
243
+ # and raises; it falls to the "" sentinel like any other unparseable value.
244
+ # Anything outside the three shapes keeps the old permissive parse, so a
245
+ # timestamp still projects as a full date.
246
+ def parse_w3cdtf(str)
247
+ m = W3CDTF_DATE.match(str)
248
+ return [DateTime.parse(str), "day"] unless m
249
+
250
+ precision = if m[3]
251
+ "day"
252
+ elsif m[2]
253
+ "month"
254
+ else
255
+ "year"
256
+ end
257
+ [DateTime.new(m[1].to_i, (m[2] || 1).to_i, (m[3] || 1).to_i), precision]
258
+ rescue Date::Error
259
+ ["", nil]
260
+ end
261
+
226
262
  def text_at(xpath)
227
263
  node = doc.at_xpath(xpath, NAMESPACE)
228
264
  node ? NEU::MODS.canonical_ws(node.text) : ""
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: neu-mods
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-22 00:00:00.000000000 Z
11
+ date: 2026-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri