Skip to content

Commit db7cb09

Browse files
committed
feat: Add Cspace::MovementReferenceNumber transform
1 parent 638197f commit db7cb09

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# frozen_string_literal: true
2+
3+
module Kiba
4+
module Extend
5+
module Transforms
6+
module Cspace
7+
# Generates typical pattern of movementReferenceNumbers based on
8+
# year in location date, where available. For each year, the
9+
# procedure data is sorted by the full date value, and the
10+
# reference number value is incremented.
11+
#
12+
# The typical number pattern used is the location record pattern from
13+
# the number autogenerator in Location/Movement/Inventory (LMI) the
14+
# UI. The pattern is:
15+
#
16+
# - LOC (customizable via the `prefix` parameter)
17+
# - 4-digit year (for rows with no date, the value given for
18+
# `nodateval` will be inserted here)
19+
# - .1 (this is one of those meaningless id number segments that never
20+
# gets incremented, but clients like to have it there to match the
21+
# default autogenerator, so here we are... customize this if desired
22+
# via the `initialsegment` param)
23+
# - .
24+
# - autoincrementing digit
25+
#
26+
# ## ASSUMPTIONS OF THIS TRANSFORM
27+
#
28+
# - You will have already reduced the output to one row per LMI to be
29+
# created in CollectionSpace
30+
# - You will have used {DateValue::ForceDayPrecision} or some other
31+
# method to create a field containing only blank values and valid
32+
# ISO 8601 dates to be mapped to the unstructured date `locationdate`
33+
# field in CollectionSpace
34+
#
35+
# ## SORTING, or, how the end digit gets incremented within a year
36+
#
37+
# During the first pass over the data, we populate `holder` hash. Keys
38+
# are the years extracted from the `datefield` values in all rows.
39+
# Values are the rows from whom that year was extracted.
40+
#
41+
# In the final pass, a sorter is applied to the Array of rows for each
42+
# year key.
43+
#
44+
# The default sorter:
45+
#
46+
# - parses the `datefield` value to create a sortable Ruby Date if a
47+
# value is present; uses `0001-01-01` if not
48+
# - sorts by this Ruby Date
49+
# - if a number of rows have the same date, they'll be in the same order
50+
# as they appear in the data before this transform is applied
51+
#
52+
# If your data contains full time stamps or some other sequential value
53+
# you would like to use to sort, you can pass a custom `sorter` Lamda.
54+
# This Lambda should take two positional arguments. The transform
55+
# passes the Array of rows for the year key as the first argument,
56+
# and the `datefield` attr value as the second. See the custom sorter
57+
# example for how you can indicate the `datefield` attr won't be used.
58+
# It should return the Array of rows, sorted as desired.
59+
# @example With default sorter
60+
# # Used in pipeline as:
61+
# # transform Cspace::MovementReferenceNumber,
62+
# # datefield: :d,
63+
# # target: :tada
64+
# xform = Cspace::MovementReferenceNumber.new(
65+
# datefield: :d,
66+
# target: :tada
67+
# )
68+
# input = [
69+
# {d: "2007-12-13", i: "a"},
70+
# {d: "2000-05-23", i: "z"},
71+
# {d: "2000-01-01", i: "c"},
72+
# {d: "", i: "q"},
73+
# {d: "2000-05-23", i: "g"},
74+
# {d: "2007-10-31", i: "f"},
75+
# {d: "2000-12-14", i: "h"},
76+
# {d: "2000-05-23", i: "d"},
77+
# {d: nil, i: "y"},
78+
# {d: "2017-02-04", i: "x"}
79+
# ]
80+
# result = Kiba::StreamingRunner.transform_stream(input, xform)
81+
# .map{ |row| row }
82+
# expected = [
83+
# {d: "2007-10-31", i: "f", tada: "LOC2007.1.1"},
84+
# {d: "2007-12-13", i: "a", tada: "LOC2007.1.2"},
85+
# {d: "2000-01-01", i: "c", tada: "LOC2000.1.1"},
86+
# {d: "2000-05-23", i: "z", tada: "LOC2000.1.2"},
87+
# {d: "2000-05-23", i: "g", tada: "LOC2000.1.3"},
88+
# {d: "2000-05-23", i: "d", tada: "LOC2000.1.4"},
89+
# {d: "2000-12-14", i: "h", tada: "LOC2000.1.5"},
90+
# {d: "", i: "q", tada: "LOC0000.1.1"},
91+
# {d: nil, i: "y", tada: "LOC0000.1.2"},
92+
# {d: "2017-02-04", i: "x", tada: "LOC2017.1.1"}
93+
# ]
94+
# expect(result).to eq(expected)
95+
# @example With custom sorter
96+
# # Used in pipeline as:
97+
# # transform Cspace::MovementReferenceNumber,
98+
# # datefield: :d,
99+
# # target: :tada,
100+
# # sorter: ->(r, _na) do
101+
# # r.sort_by { |row| row[:i] }
102+
# # end
103+
# xform = Cspace::MovementReferenceNumber.new(
104+
# datefield: :d,
105+
# target: :tada,
106+
# sorter: ->(r, _na) do
107+
# r.sort_by { |row| row[:i] }
108+
# end
109+
# )
110+
# input = [
111+
# {d: "2007-12-13", i: "a"},
112+
# {d: "2000-05-23", i: "z"},
113+
# {d: "2000-01-01", i: "c"},
114+
# {d: "", i: "q"},
115+
# {d: "2000-05-23", i: "g"},
116+
# {d: "2007-10-31", i: "f"},
117+
# {d: "2000-12-14", i: "h"},
118+
# {d: "2000-05-23", i: "d"},
119+
# {d: nil, i: "y"},
120+
# {d: "2017-02-04", i: "x"}
121+
# ]
122+
# result = Kiba::StreamingRunner.transform_stream(input, xform)
123+
# .map{ |row| row }
124+
# expected = [
125+
# {d: "2007-12-13", i: "a", tada: "LOC2007.1.1"},
126+
# {d: "2007-10-31", i: "f", tada: "LOC2007.1.2"},
127+
# {d: "2000-01-01", i: "c", tada: "LOC2000.1.1"},
128+
# {d: "2000-05-23", i: "d", tada: "LOC2000.1.2"},
129+
# {d: "2000-05-23", i: "g", tada: "LOC2000.1.3"},
130+
# {d: "2000-12-14", i: "h", tada: "LOC2000.1.4"},
131+
# {d: "2000-05-23", i: "z", tada: "LOC2000.1.5"},
132+
# {d: "", i: "q", tada: "LOC0000.1.1"},
133+
# {d: nil, i: "y", tada: "LOC0000.1.2"},
134+
# {d: "2017-02-04", i: "x", tada: "LOC2017.1.1"}
135+
# ]
136+
# expect(result).to eq(expected)
137+
class MovementReferenceNumber
138+
# This sorter is used if no custom sorter is passed in when
139+
# initializing the transform
140+
DEFAULT_SORTER = ->(rows, datefield) do
141+
rows.sort_by do |row|
142+
dateval = row[datefield]
143+
dateval.blank? ? Date.new(1, 1, 1) : Date.parse(dateval)
144+
end
145+
end
146+
147+
# @param datefield [Symbol] field containing valid ISO 8601 dates and
148+
# blank values only
149+
# @param prefix [String] initial segment of generated value
150+
# @param nodateval [String] value inserted instead of the year, if
151+
# `datefield` is blank
152+
# @param initialsegment [String] the first segment of number after the
153+
# year or `nodateval`
154+
# @param target [Symbol] field in which to write the generated value
155+
# @param sorter [Lambda] taking `rows` and `datefield` args,
156+
# and returning sorted rows
157+
def initialize(datefield:,
158+
prefix: "LOC",
159+
nodateval: "0000",
160+
initialsegment: ".1",
161+
target: :movementreferencenumber,
162+
sorter: DEFAULT_SORTER)
163+
@datefield = datefield
164+
@prefix = prefix
165+
@nodateval = nodateval
166+
@initialsegment = initialsegment
167+
@target = target
168+
@nodatesortval = nodatesortval
169+
@sorter = sorter
170+
@holder = {}
171+
end
172+
173+
def process(row)
174+
dateval = row[datefield]
175+
year = dateval.blank? ? nodateval : dateval[0..3]
176+
populate_holder(row, year)
177+
178+
nil
179+
end
180+
181+
def close
182+
holder.map { |year, data| generate_for_year(year, data) }
183+
.flatten
184+
.each { |row| yield row }
185+
end
186+
187+
private
188+
189+
attr_reader :datefield, :prefix, :nodateval, :initialsegment,
190+
:target, :nodatesortval, :sorter, :holder
191+
192+
def parse_date(dateval)
193+
return "" if dateval.blank?
194+
195+
Date.parse(dateval)
196+
rescue
197+
""
198+
end
199+
200+
def populate_holder(row, year)
201+
holder[year] = [] unless holder.key?(year)
202+
holder[year] << row
203+
end
204+
205+
def generate_for_year(year, data)
206+
base = "#{prefix}#{year}.1."
207+
counter = 0
208+
sorter.call(data, datefield)
209+
.map do |row|
210+
counter += 1
211+
row[target] = "#{base}#{counter}"
212+
213+
row
214+
end
215+
end
216+
end
217+
end
218+
end
219+
end
220+
end

0 commit comments

Comments
 (0)