Metadata
xportrs provides rich metadata support for XPT files, ensuring CDISC compliance and data clarity.
Metadata Overview
graph TB
subgraph "Dataset Level"
A[Domain Code] --> B[Dataset Label]
end
subgraph "Variable Level"
C[Variable Name] --> D[Variable Label]
D --> E[Format]
E --> F[Informat]
F --> G[Length]
G --> H[Role]
end
Dataset Metadata
Domain Code
The domain code is the dataset name (1-8 characters):
use xportrs::{Dataset, Column, ColumnData};
fn main() -> xportrs::Result<()> {
let columns = vec![Column::new("A", ColumnData::F64(vec![Some(1.0)]))];
let dataset = Dataset::new("AE", columns)?;
// Access domain code
let code: &str = dataset.domain_code(); // "AE"
Ok(())
}
Dataset Label
The dataset label provides a description (0-40 characters):
use xportrs::{Dataset, Column, ColumnData};
fn main() -> xportrs::Result<()> {
let columns = vec![Column::new("A", ColumnData::F64(vec![Some(1.0)]))];
// Set at construction
let dataset = Dataset::with_label("AE", "Adverse Events", columns.clone())?;
// Or set later
let mut dataset = Dataset::new("AE", columns)?;
dataset.set_label("Adverse Events");
// Access
let label: Option<&str> = dataset.dataset_label();
Ok(())
}
Variable Metadata
Variable Name
Variable names follow SAS naming rules:
use xportrs::{Column, ColumnData, VariableName};
fn main() {
let data = ColumnData::String(vec![Some("001".into())]);
// Name is set at construction
let col = Column::new("USUBJID", data);
// Access name
let name: &str = col.name();
// VariableName type for validation
let var_name = VariableName::new("USUBJID");
assert_eq!(var_name.as_str(), "USUBJID");
}
Variable Label
Labels describe the variable (0-40 characters):
use xportrs::{Column, ColumnData, Label};
fn main() {
let data = ColumnData::String(vec![Some("001".into())]);
let col = Column::new("USUBJID", data)
.with_label("Unique Subject Identifier");
// Access label
if let Some(label) = col.label() {
println!("Label: {}", label);
}
// Label type
let label = Label::new("Unique Subject Identifier");
assert_eq!(label.as_str(), "Unique Subject Identifier");
}
Format
Display formats control how values are shown:
use xportrs::{Column, ColumnData, Format};
fn main() -> xportrs::Result<()> {
let data = ColumnData::F64(vec![Some(1.0)]);
// Using Format object
let col = Column::new("AESTDT", data.clone())
.with_format(Format::parse("DATE9.")?);
// Using format string
let col = Column::new("AESTDT", data)
.with_format_str("DATE9.")?;
// Access format
if let Some(format) = col.format() {
println!("Format: {}", format);
}
Ok(())
}
Informat
Input formats control how values are read:
use xportrs::{Column, ColumnData, Format};
fn main() -> xportrs::Result<()> {
let data = ColumnData::F64(vec![Some(1.0)]);
let col = Column::new("RAWDATE", data)
.with_informat(Format::parse("DATE9.")?);
if let Some(informat) = col.informat() {
println!("Informat: {}", informat);
}
Ok(())
}
Length
Explicit length for character variables:
use xportrs::{Column, ColumnData};
fn main() {
// Auto-derived from data
let col = Column::new("VAR", ColumnData::String(vec![
Some("Hello".into()), // 5 characters
Some("World".into()), // 5 characters
]));
// Length will be 5
// Explicit override
let data = ColumnData::String(vec![Some("text".into())]);
let col = Column::new("VAR", data)
.with_length(200); // Force 200 bytes
// Access
if let Some(len) = col.explicit_length() {
println!("Explicit length: {}", len);
}
}
Variable Role
Roles categorize variables per CDISC:
use xportrs::{Column, ColumnData, VariableRole};
fn main() {
let data = ColumnData::String(vec![Some("001".into())]);
let col = Column::with_role(
"USUBJID",
VariableRole::Identifier,
data,
);
// Available roles
let roles = [
VariableRole::Identifier,
VariableRole::Topic,
VariableRole::Timing,
VariableRole::Qualifier,
VariableRole::Rule,
VariableRole::Synonym,
VariableRole::Record,
];
// Access role
if let Some(role) = col.role() {
println!("Role: {:?}", role);
}
}
Metadata Types
DomainCode
use xportrs::DomainCode;
fn main() {
let code = DomainCode::new("AE");
// Access
let s: &str = code.as_str();
let code2 = DomainCode::new("AE");
let owned: String = code2.into_inner();
// Traits
assert_eq!(code, DomainCode::new("AE"));
println!("{}", code); // "AE"
}
Label
use xportrs::Label;
fn main() {
let label = Label::new("Adverse Events");
// Access
let s: &str = label.as_str();
let label2 = Label::new("AE");
let owned: String = label2.into_inner();
// From string
let label: Label = "Test".into();
}
VariableName
use xportrs::VariableName;
fn main() {
let name = VariableName::new("USUBJID");
// Access
let s: &str = name.as_str();
let name2 = VariableName::new("TEST");
let owned: String = name2.into_inner();
// Validation (at construction or later)
// Names are uppercased automatically
let name = VariableName::new("usubjid");
assert_eq!(name.as_str(), "USUBJID");
}
Metadata in XPT Files
NAMESTR Record Storage
Field Offset Size Description
nname 8-15 8 Variable name
nlabel 16-55 40 Variable label
nform 56-63 8 Format name
nfl 64-65 2 Format length
nfd 66-67 2 Format decimals
nfj 68-69 2 Format justification
niform 72-79 8 Informat name
nifl 80-81 2 Informat length
nifd 82-83 2 Informat decimals
Reading Metadata
use xportrs::Xpt;
fn main() -> xportrs::Result<()> {
let dataset = Xpt::read("ae.xpt")?;
// Dataset metadata
println!("Domain: {}", dataset.domain_code());
if let Some(label) = dataset.dataset_label() {
println!("Label: {}", label);
}
// Variable metadata
for col in dataset.columns() {
println!("\n{}", col.name());
if let Some(label) = col.label() {
println!(" Label: {}", label);
}
if let Some(format) = col.format() {
println!(" Format: {}", format);
}
if let Some(informat) = col.informat() {
println!(" Informat: {}", informat);
}
if let Some(len) = col.explicit_length() {
println!(" Length: {}", len);
}
if let Some(role) = col.role() {
println!(" Role: {:?}", role);
}
}
Ok(())
}
Preserving Metadata on Roundtrip
use xportrs::Xpt;
fn main() -> xportrs::Result<()> {
// Read
let original = Xpt::read("ae.xpt")?;
// Modify (metadata preserved)
// ...
// Write
Xpt::writer(original.clone())
.finalize()?
.write_path("ae_modified.xpt")?;
// Verify
let reloaded = Xpt::read("ae_modified.xpt")?;
assert_eq!(reloaded.dataset_label(), original.dataset_label());
Ok(())
}
Metadata and Define-XML
[!IMPORTANT] Variable labels in XPT files should match those in define.xml. Pinnacle 21 validates this consistency.
use xportrs::{Dataset, Column, ColumnData};
fn main() -> xportrs::Result<()> {
let data = ColumnData::String(vec![Some("test".into())]);
// Create dataset with labels matching define.xml
let dataset = Dataset::with_label("AE", "Adverse Events", vec![
Column::new("STUDYID", data.clone())
.with_label("Study Identifier"), // Must match define.xml
Column::new("USUBJID", data)
.with_label("Unique Subject Identifier"), // Must match define.xml
// ...
])?;
Ok(())
}
Best Practices
- Always include labels: Labels help reviewers understand data
- Use standard formats: DATE9., DATETIME20., $CHARn.
- Set explicit lengths: Control character variable lengths
- Assign roles: Categorize variables per CDISC
- Verify roundtrip: Ensure metadata survives read/write cycles
use xportrs::{Column, ColumnData, Format, VariableRole};
// Complete metadata example
let col = Column::with_role(
"AESTDTC",
VariableRole::Timing,
ColumnData::String(vec![Some("2024-01-15".into())]),
)
.with_label("Start Date/Time of Adverse Event")
.with_format(Format::character(19))
.with_length(19);