///|
fn transform_same_role(a : Role, b : Role) -> Bool {
match (a, b) {
(Dimension, Dimension)
| (TimeDimension, TimeDimension)
| (Measure, Measure) => true
(Attribute(x), Attribute(y)) => x == y
_ => false
}
}
///|
fn transform_same_component(
a : Component,
b : Component,
limits : Limits,
representation~ : Bool = true,
) -> Unit raise SdmxError {
if a.id != b.id ||
a.name != b.name ||
a.required != b.required ||
!transform_same_role(a.role, b.role) {
raise Invalid(
path=b.id,
reason="transformation changes component metadata or attachment",
)
}
if representation &&
canonical_json(representation_json(a.representation), limits) !=
canonical_json(representation_json(b.representation), limits) {
raise Invalid(
path=b.id,
reason="transformation changes representation without explicit recoding",
)
}
}
///|
fn transform_same_columns(
a : Array[Component],
b : Array[Component],
limits : Limits,
) -> Unit raise SdmxError {
if a.length() != b.length() {
raise Invalid(path="transformation", reason="component count differs")
}
for i, c in a {
transform_same_component(c, b[i], limits)
}
}
///|
/// Change dimension order using an explicitly supplied target structure.
/// No dimension, measure, attachment, representation or metadata is removed.
pub fn Dataset::reorder_dimensions(
self : Dataset,
target : Structure,
) -> Dataset raise SdmxError {
if self.structure.dimensions.length() != target.dimensions.length() {
raise Invalid(
path="dimensions",
reason="reordering must retain all identifying dimensions",
)
}
transform_same_columns(
self.structure.measures,
target.measures,
target.limits,
)
transform_same_columns(
self.structure.attributes,
target.attributes,
target.limits,
)
let positions : Array[Int32] = []
for component in target.dimensions {
let old = match self.structure.positions.get(component.id) {
Some(i) => i
None =>
raise Invalid(path=component.id, reason="not an original dimension")
}
transform_same_component(
self.structure.dimensions[old],
component,
target.limits,
)
positions.push(old)
}
let rows : Array[Observation] = []
for row in self.rows {
rows.push(
Observation::new(
target,
positions.map(p => row.key[p]),
row.measures,
row.attributes,
),
)
}
Dataset::new(target, rows)
}
///|
pub struct CodeMapping {
priv dimension : String
priv replacements : Map[String, String]
}
///|
/// Unspecified codes retain their identity. Only coded identifying dimensions
/// may be recoded: measures and unit/other attributes require semantic conversion.
pub fn CodeMapping::new(
dimension : String,
pairs : Array[(String, String)],
limits~ : Limits = Limits::standard(),
) -> CodeMapping raise SdmxError {
identifier(dimension, "mapping.dimension")
if pairs.length() > limits.max_codes {
raise Limit(resource="mapping entries", maximum=limits.max_codes)
}
let replacements : Map[String, String] = Map([])
for (source, destination) in pairs {
check_field(source, limits, "mapping.source")
check_field(destination, limits, "mapping.destination")
if source.is_empty() || destination.is_empty() {
raise Invalid(path=dimension, reason="empty mapping code")
}
if replacements.contains(source) {
raise Invalid(path=dimension, reason="duplicate mapping source")
}
replacements[source] = destination
}
{ dimension, replacements, }
}
///|
pub fn CodeMapping::dimension(self : CodeMapping) -> String {
self.dimension
}
///|
fn transform_recode_domain(
before : Component,
after : Component,
mapping : CodeMapping,
) -> Unit raise SdmxError {
let (old_codes, new_codes) = match
(before.role, before.representation, after.representation) {
(Dimension, Coded(a), Coded(b)) => (a, b)
_ =>
raise Invalid(
path=before.id,
reason="recoding requires a coded non-time dimension",
)
}
for source in mapping.replacements.keys() {
if !old_codes.contains(source) {
raise Invalid(
path=before.id,
reason="mapping source is outside original code list",
)
}
}
let seen : Map[String, String] = Map([])
for (source, _) in old_codes.entries() {
let destination = mapping.replacements.get(source).unwrap_or(source)
if !new_codes.contains(destination) {
raise Invalid(
path=before.id,
reason="mapped code is outside target code list",
)
}
if seen.contains(destination) {
raise Conflict(
key=destination,
reason="code mapping merges distinct source codes",
)
}
seen[destination] = source
}
}
///|
/// Recode one or more coded dimensions without changing row identity cardinality.
/// The target structure is explicit even for a same-version code-label migration.
/// Mapping validation covers the full source domain, including unobserved codes.
pub fn Dataset::recode(
self : Dataset,
target : Structure,
mappings : Array[CodeMapping],
) -> Dataset raise SdmxError {
if mappings.length() > self.structure.dimensions.length() {
raise Invalid(path="mappings", reason="more mappings than dimensions")
}
if self.structure.dimensions.length() != target.dimensions.length() {
raise Invalid(
path="dimensions",
reason="recoding must retain all dimensions",
)
}
transform_same_columns(
self.structure.measures,
target.measures,
target.limits,
)
transform_same_columns(
self.structure.attributes,
target.attributes,
target.limits,
)
let by_position : Map[Int32, CodeMapping] = Map([])
for mapping in mappings {
let position = match self.structure.positions.get(mapping.dimension) {
Some(i) => i
None =>
raise Invalid(
path=mapping.dimension,
reason="mapping is not for a dimension",
)
}
if by_position.contains(position) {
raise Invalid(
path=mapping.dimension,
reason="duplicate dimension mapping",
)
}
by_position[position] = mapping
}
for i, before in self.structure.dimensions {
let after = target.dimensions[i]
let explicit = by_position.contains(i)
transform_same_component(
before,
after,
target.limits,
representation=!explicit,
)
if by_position.get(i) is Some(mapping) {
transform_recode_domain(before, after, mapping)
}
}
let rows : Array[Observation] = []
for row in self.rows {
let key = row.key.copy()
for position, mapping in by_position {
key[position] = mapping.replacements
.get(key[position])
.unwrap_or(key[position])
}
rows.push(Observation::new(target, key, row.measures, row.attributes))
}
Dataset::new(target, rows)
}
///|
fn transform_projection_positions(
original : Array[Component],
selected : Array[String],
target : Array[Component],
limits : Limits,
) -> Array[Int32] raise SdmxError {
if selected.length() != target.length() {
raise Invalid(
path="projection",
reason="selected fields differ from target structure",
)
}
let available : Map[String, Int32] = Map([])
for i, component in original {
available[component.id] = i
}
let seen : Map[String, Bool] = Map([])
let positions : Array[Int32] = []
for i, id in selected {
if seen.contains(id) {
raise Invalid(path=id, reason="duplicate projected component")
}
let position = match available.get(id) {
Some(p) => p
None =>
raise Invalid(path=id, reason="component is not in the selected role")
}
seen[id] = true
transform_same_component(original[position], target[i], limits)
positions.push(position)
}
for component in original {
if component.required && !seen.contains(component.id) {
raise Invalid(
path=component.id,
reason="projection would drop required metadata or measure",
)
}
}
positions
}
///|
/// Project explicit measure/attribute lists; all dimensions and every required
/// measure/attribute must remain. Optional metadata is dropped only by omission
/// from the explicit list; retained metadata and attachments cannot be rewritten.
pub fn Dataset::project(
self : Dataset,
target : Structure,
measures : Array[String],
attributes : Array[String],
) -> Dataset raise SdmxError {
transform_same_columns(
self.structure.dimensions,
target.dimensions,
target.limits,
)
let measure_positions = transform_projection_positions(
self.structure.measures,
measures,
target.measures,
target.limits,
)
let attribute_positions = transform_projection_positions(
self.structure.attributes,
attributes,
target.attributes,
target.limits,
)
let rows : Array[Observation] = []
for row in self.rows {
rows.push(
Observation::new(
target,
row.key,
measure_positions.map(i => row.measures[i]),
attribute_positions.map(i => row.attributes[i]),
),
)
}
Dataset::new(target, rows)
}