@@ -72,6 +72,7 @@ spec_to_metacore <- function(path, quiet = deprecated(), where_sep_sheet = TRUE,
7272 codelist <- spec_type_to_codelist(doc )
7373
7474 # Define.xml-only tables — skipped when define_fields = FALSE
75+ study_level <- if (define_fields ) spec_type_to_study_level(doc ) else NULL
7576 documents <- if (define_fields ) spec_type_to_documents(doc ) else NULL
7677 comments <- if (define_fields ) spec_type_to_comments(doc ) else NULL
7778 supp <- base_column_schema()$ .supp
@@ -96,6 +97,7 @@ spec_to_metacore <- function(path, quiet = deprecated(), where_sep_sheet = TRUE,
9697 mc <- metacore(
9798 ds_spec , ds_vars , var_spec , value_spec , derivations , codelist ,
9899 supp = supp ,
100+ study_level = study_level ,
99101 documents = documents ,
100102 comments = comments ,
101103 define_fields = define_fields ,
@@ -1267,6 +1269,164 @@ spec_type_to_comments <- function(
12671269 reorder_by_schema(" comments" )
12681270}
12691271
1272+ # ' Spec to study_level
1273+ # '
1274+ # ' `r lifecycle::badge("experimental")`
1275+ # ' Builds the `study_level` table from a named list of Excel sheets produced by
1276+ # ' [read_all_sheets()]. `study_level` holds **one row of study-wide metadata**
1277+ # ' used when generating Define.xml 2.0 output.
1278+ # '
1279+ # ' @details
1280+ # ' ## How it works
1281+ # '
1282+ # ' The function locates a sheet whose name matches `sheet` (typically `"Define"`
1283+ # ' or `"Study"`). It then either **pivots** the sheet (the default) or reads
1284+ # ' columns directly, depending on `pivot`.
1285+ # '
1286+ # ' ## Pivoted format (`pivot = TRUE`, default)
1287+ # '
1288+ # ' The sheet contains two columns: one whose heading matches `[Aa]ttribute`
1289+ # ' (holding the field names) and one whose heading matches `[Vv]alue` (holding
1290+ # ' the corresponding values). For example:
1291+ # '
1292+ # ' | Attribute | Value |
1293+ # ' |:-----------------|:----------------------|
1294+ # ' | StudyName | CDISC Pilot |
1295+ # ' | StudyDescription | Phase III efficacy... |
1296+ # ' | ProtocolName | CDISCPILOT01 |
1297+ # '
1298+ # ' The function pivots this into a single-row tibble with one column per
1299+ # ' attribute, then renames columns to the standard schema names using the
1300+ # ' regular expressions in `cols`. Any attribute not matched by a `cols` entry is
1301+ # ' discarded; any schema column not found in the sheet is filled with `NA`.
1302+ # '
1303+ # ' ## Columnar format (`pivot = FALSE`)
1304+ # '
1305+ # ' The sheet already stores each field as its own column (one row of data). The
1306+ # ' regexes in `cols` are matched against the **column headings** of the sheet
1307+ # ' directly, and the matching columns are selected and renamed. This is the
1308+ # ' same approach used by all other `spec_type_to_*` builders.
1309+ # '
1310+ # ' ## Output columns
1311+ # '
1312+ # ' | Column | Description |
1313+ # ' |:-------|:------------|
1314+ # ' | `study_name` | Short study identifier |
1315+ # ' | `study_description` | Human-readable study description |
1316+ # ' | `protocol_name` | Protocol number or name |
1317+ # ' | `standard_name` | CDISC standard name (e.g. `"ADaM"`) |
1318+ # ' | `standard_version` | Version of the standard (e.g. `"1.0"`) |
1319+ # ' | `define_version` | Define-XML version (e.g. `"2.0"`) |
1320+ # ' | `language` | Language code (e.g. `"en"`) |
1321+ # '
1322+ # ' @param doc Named list of data frames produced by [read_all_sheets()].
1323+ # ' @param cols Named character vector of regular expressions.
1324+ # ' - When `pivot = TRUE` (default): regexes are matched against the **pivoted
1325+ # ' attribute values** (which become column names after the pivot). Any
1326+ # ' attribute not matched is silently discarded; missing schema columns are
1327+ # ' filled with `NA`.
1328+ # ' - When `pivot = FALSE`: regexes are matched against the **sheet's column
1329+ # ' headings**, identical to other `spec_type_to_*` builders. Only the
1330+ # ' columns listed in `cols` are required; missing schema columns are filled
1331+ # ' with `NA`.
1332+ # '
1333+ # ' Names must be a subset of the `study_level` schema column names:
1334+ # ' `study_name`, `study_description`, `protocol_name`, `standard_name`,
1335+ # ' `standard_version`, `define_version`, `language`. A minimum of
1336+ # ' `study_name`, `study_description`, and `protocol_name` should be supplied.
1337+ # ' @param sheet Regular expression used to identify the study-level sheet.
1338+ # ' Defaults to matching sheets named `"Define"` or `"Study"`.
1339+ # ' @param pivot Logical; when `TRUE` (default) the sheet is assumed to be in
1340+ # ' attribute–value format and is pivoted to a single wide row before column
1341+ # ' mapping. Set to `FALSE` when the sheet already stores study metadata as
1342+ # ' named columns.
1343+ # '
1344+ # ' @return A one-row tibble formatted for the `study_level` slot of a
1345+ # ' [MetacoreDefine] object.
1346+ # ' @export
1347+ # '
1348+ # ' @family spec builders
1349+ spec_type_to_study_level <- function (
1350+ doc ,
1351+ cols = c(
1352+ " study_name" = " [Ss]tudy.?[Nn]ame" ,
1353+ " study_description" = " [Ss]tudy.?[Dd]escription" ,
1354+ " protocol_name" = " [Pp]rotocol.?[Nn]ame" ,
1355+ " standard_name" = " [Ss]tandard.?[Nn]ame" ,
1356+ " standard_version" = " [Ss]tandard.?[Vv]ersion" ,
1357+ " define_version" = " [Dd]efine.?[Vv]ersion" ,
1358+ " language" = " [Ll]anguage"
1359+ ),
1360+ sheet = " [Dd]efine|[Ss]tudy" ,
1361+ pivot = TRUE ) {
1362+
1363+ valid_names <- names(define_column_schema()$ .study_level )
1364+ if (! all(names(cols ) %in% valid_names ) || is.null(names(cols ))) {
1365+ cli_abort(c(
1366+ " x" = " Incorrect column names supplied for {.var study_level}" ,
1367+ " i" = " The column vector {.arg cols} must be named with a subset of {.val {valid_names}}"
1368+ ))
1369+ }
1370+
1371+ if (! is.null(sheet )) {
1372+ sheet_ls <- str_subset(names(doc ), sheet )
1373+ if (length(sheet_ls ) == 0 ) {
1374+ cli_abort(" No sheet matching {.val {sheet}} was found in the specification." )
1375+ }
1376+ if (length(sheet_ls ) > 1 ) {
1377+ cli_warn(c(
1378+ " Multiple sheets match the sheet pattern." ,
1379+ " i" = " Using the first match: {.val {sheet_ls[1]}}."
1380+ ))
1381+ sheet_ls <- sheet_ls [1 ]
1382+ }
1383+ doc <- doc [sheet_ls ]
1384+ }
1385+
1386+ sheet_data <- doc [[1 ]]
1387+
1388+ if (pivot ) {
1389+ col_names <- names(sheet_data )
1390+ attr_col <- col_names [str_detect(col_names , " [Aa]ttribute" )]
1391+ val_col <- col_names [str_detect(col_names , " [Vv]alue" )]
1392+
1393+ if (length(attr_col ) == 0 ) {
1394+ cli_abort(c(
1395+ " x" = " Could not find an attribute column in sheet {.val {names(doc)[1]}}." ,
1396+ " i" = " Expected a column matching {.val Attribute} when {.arg pivot = TRUE}."
1397+ ))
1398+ }
1399+ if (length(val_col ) == 0 ) {
1400+ cli_abort(c(
1401+ " x" = " Could not find a value column in sheet {.val {names(doc)[1]}}." ,
1402+ " i" = " Expected a column matching {.val Value} when {.arg pivot = TRUE}."
1403+ ))
1404+ }
1405+
1406+ sheet_data <- sheet_data | >
1407+ select(attribute = all_of(attr_col [1 ]), value = all_of(val_col [1 ])) | >
1408+ filter(! is.na(.data $ attribute )) | >
1409+ tidyr :: pivot_wider(names_from = attribute , values_from = value )
1410+ }
1411+
1412+ # Match each cols regex against the (possibly pivoted) column names and
1413+ # extract the matching column, filling NA for any unmatched entry.
1414+ pivot_names <- names(sheet_data )
1415+ out <- imap(cols , function (regex , output_name ) {
1416+ match_idx <- which(str_detect(pivot_names , regex ))
1417+ if (length(match_idx ) > 0 ) {
1418+ sheet_data [[pivot_names [match_idx [1 ]]]]
1419+ } else {
1420+ rep(NA_character_ , nrow(sheet_data ))
1421+ }
1422+ }) | >
1423+ tibble :: as_tibble()
1424+
1425+ fill_cols(out , define_column_schema()$ .study_level ) | >
1426+ reorder_by_schema(" study_level" )
1427+ }
1428+
1429+
12701430# ' Spec to supp
12711431# '
12721432# ' `r lifecycle::badge("experimental")`
0 commit comments