tests/testthat/test-iupac-to-structure.R

test_that("known monosaccharides retain their identity without substituents", {
  monos <- available_monosaccharides()
  glycans <- as_glycan_structure(paste0(monos, "(?1-"))
  graphs <- as.list(glycans)

  expect_identical(
    purrr::map_chr(graphs, \(graph) igraph::vertex_attr(graph, "mono")),
    monos
  )
  expect_identical(
    purrr::map_chr(graphs, \(graph) igraph::vertex_attr(graph, "sub")),
    rep("", length(monos))
  )
})

test_that("as_glycan_structure.character parses simple IUPAC-condensed strings", {
  # Single monosaccharide
  glycan1 <- as_glycan_structure("Man(?1-")
  expect_s3_class(glycan1, "glyrepr_structure")
  expect_equal(length(glycan1), 1)
  expect_equal(structure_to_iupac(glycan1), "Man(?1-")

  # Two monosaccharides
  glycan2 <- as_glycan_structure("Gal(b1-3)GalNAc(?1-")
  expect_s3_class(glycan2, "glyrepr_structure")
  expect_equal(length(glycan2), 1)
  expect_equal(structure_to_iupac(glycan2), "Gal(b1-3)GalNAc(?1-")

  # With explicit anomer
  glycan3 <- as_glycan_structure("Gal(b1-3)GalNAc(a1-")
  expect_s3_class(glycan3, "glyrepr_structure")
  expect_equal(structure_to_iupac(glycan3), "Gal(b1-3)GalNAc(a1-")
})

test_that("as_glycan_structure.character infers reducing-end anomer positions", {
  iupacs <- c("Man", "Neu5Ac", "Gal(b1-3)GalNAc")

  glycans <- as_glycan_structure(iupacs)

  expect_equal(
    structure_to_iupac(glycans),
    c("Man(?1-", "Neu5Ac(?2-", "Gal(b1-3)GalNAc(?1-")
  )
})

test_that("ordinary IUPAC parsing bypasses floating-part splitting", {
  testthat::local_mocked_bindings(
    split_floating_iupac = function(...) {
      stop("floating parser should not run")
    }
  )

  glycan <- as_glycan_structure("Gal(b1-3)GalNAc(a1-")

  expect_identical(
    unname(structure_to_iupac(glycan)),
    "Gal(b1-3)GalNAc(a1-"
  )
})

test_that("as_glycan_structure.character parses branched structures", {
  # Simple branched structure
  iupac <- "Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc(?1-"
  glycan <- as_glycan_structure(iupac)
  expect_s3_class(glycan, "glyrepr_structure")
  expect_equal(
    structure_to_iupac(glycan),
    "Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc(?1-"
  )
})

test_that("as_glycan_structure.character parses floating parts", {
  iupacs <- c(
    "{Neu5Ac(a2-3)}Gal(b1-3)GalNAc(a1-",
    "{Neu5Ac(a2-6)|2,3}Gal(b1-3)GalNAc(a1-",
    "{Neu5Ac(a2-3)|2,5}Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc(b1-",
    "{Neu5Ac(a2-3)Gal(b1-4)|3,4}Glc(b1-3)GalNAc(a1-"
  )

  glycans <- as_glycan_structure(iupacs)
  graphs <- as.list(glycans)

  expect_identical(unname(structure_to_iupac(glycans)), iupacs)
  expect_equal(
    purrr::map(graphs, ~ .x$floating_parts[[1]]$parents),
    list(integer(), c(2L, 3L), c(2L, 5L), c(3L, 4L))
  )
  expect_equal(
    purrr::map_int(graphs, ~ .x$floating_parts[[1]]$root),
    c(1L, 1L, 1L, 2L)
  )
  expect_equal(
    purrr::map(graphs, ~ .x$floating_parts[[1]]$nodes),
    list(1L, 1L, 1L, c(1L, 2L))
  )
  expect_equal(
    igraph::V(graphs[[4]])$mono,
    c("Neu5Ac", "Gal", "Glc", "GalNAc")
  )
})

test_that("floating parent indices follow canonicalized main-tree order", {
  glycan <- as_glycan_structure(
    paste0(
      "{Neu5Ac(a2-4)|2,4}",
      "Man(a1-6)[Man(a1-3)]Man(b1-4)GlcNAc(b1-4)GlcNAc(b1-"
    )
  )

  expect_identical(
    unname(structure_to_iupac(glycan)),
    paste0(
      "{Neu5Ac(a2-4)|3,4}",
      "Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc(b1-"
    )
  )
})

test_that("as_glycan_structure.character handles substituents", {
  # With substituents
  iupac <- "Man3S(a1-2)Gal6Ac(?1-"
  glycan <- as_glycan_structure(iupac)
  expect_s3_class(glycan, "glyrepr_structure")

  # Check that substituents are preserved
  graph <- get_structure_graphs(glycan, return_list = FALSE)
  expect_equal(igraph::V(graph)$sub, c("3S", "6Ac"))
})

test_that("as_glycan_structure.character handles Neu5Ac correctly", {
  # Neu5Ac with explicit anomer
  glycan1 <- as_glycan_structure("Neu5Ac(?2-")
  expect_equal(structure_to_iupac(glycan1), "Neu5Ac(?2-")

  # Neu5Ac with explicit anomer
  glycan2 <- as_glycan_structure("Neu5Ac(a2-")
  expect_equal(structure_to_iupac(glycan2), "Neu5Ac(a2-")

  # Neu5Ac with substituent
  glycan3 <- as_glycan_structure("Neu5Ac9Ac(?2-")
  graph <- get_structure_graphs(glycan3, return_list = FALSE)
  expect_equal(igraph::V(graph)$mono, "Neu5Ac")
  expect_equal(igraph::V(graph)$sub, "9Ac")
})

test_that("as_glycan_structure.character parses digit-leading monosaccharides", {
  monos <- available_monosaccharides("concrete")
  monos <- monos[stringr::str_detect(monos, "^[0-9]")]
  iupacs <- paste0(monos, "(?", infer_anomer_pos(monos), "-")

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_equal(purrr::map_chr(graphs, ~ igraph::V(.x)$mono), monos)
  expect_equal(
    purrr::map_chr(graphs, ~ igraph::V(.x)$sub),
    rep("", length(monos))
  )
  expect_equal(structure_to_iupac(glycans), iupacs)
  expect_equal(
    structure_to_iupac(as_glycan_structure("6dGul(b1-")),
    "6dGul(b1-"
  )
})


test_that("as_glycan_structure.character parses every furanose form", {
  monos <- unname(furanose_monosaccharides)
  iupacs <- paste0(monos, "(?", infer_anomer_pos(monos), "-")

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_identical(
    purrr::map_chr(graphs, ~ igraph::V(.x)$mono),
    monos
  )
  expect_identical(unname(structure_to_iupac(glycans)), iupacs)
})


test_that("as_glycan_structure.character parses every unusual configuration", {
  monos <- unname(unusual_configuration_monosaccharides)
  iupacs <- paste0(monos, "(?", infer_anomer_pos(monos), "-")

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_identical(
    purrr::map_chr(graphs, ~ igraph::V(.x)$mono),
    monos
  )
  expect_identical(unname(structure_to_iupac(glycans)), iupacs)
})


test_that("unusual configurations support branches and omitted anomers", {
  iupac <- "D-Fuc(a1-2)[L-Gul(b1-3)]Gal(?1-"

  expect_identical(
    unname(structure_to_iupac(as_glycan_structure(iupac))),
    iupac
  )
  expect_identical(
    unname(structure_to_iupac(as_glycan_structure("L-6dGul"))),
    "L-6dGul(?1-"
  )
})


test_that("unusual configurations retain substituents", {
  iupacs <- c(
    "D-Fuc3S(a1-",
    "L-Neu5Ac9Ac(a2-",
    "L-Neuf5Gc9Ac(a2-",
    "L-Neu4Ac5Ac(a2-",
    "L-Neuf4Ac5Gc(a2-"
  )
  expected_iupacs <- c(
    iupacs[1:3],
    "L-Neu5Ac4Ac(a2-",
    "L-Neuf5Gc4Ac(a2-"
  )

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_identical(
    purrr::map_chr(graphs, ~ igraph::V(.x)$mono),
    c("D-Fuc", "L-Neu5Ac", "L-Neuf5Gc", "L-Neu5Ac", "L-Neuf5Gc")
  )
  expect_identical(
    purrr::map_chr(graphs, ~ igraph::V(.x)$sub),
    c("3S", "9Ac", "9Ac", "4Ac", "4Ac")
  )
  expect_identical(unname(structure_to_iupac(glycans)), expected_iupacs)
})


test_that("redundant natural configuration prefixes are rejected", {
  errors <- purrr::map(
    c("L-Fuc(a1-", "D-Gul(b1-", "D-Neu5Ac(a2-"),
    ~ tryCatch(as_glycan_structure(.x), error = identity)
  )

  expect_identical(purrr::map_lgl(errors, inherits, "error"), rep(TRUE, 3))
  expect_match(
    purrr::map_chr(errors, conditionMessage),
    "Unknown monosaccharide"
  )
})


test_that("furanose forms retain additional substituents", {
  iupacs <- c(
    "Galf3Me(b1-",
    "GlcfNAc6Ac(b1-",
    "Neuf5Ac9Ac(a2-",
    "Neuf4Ac5Gc(a2-"
  )

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_identical(
    purrr::map_chr(graphs, ~ igraph::V(.x)$mono),
    c("Galf", "GlcfNAc", "Neuf5Ac", "Neuf5Gc")
  )
  expect_identical(
    purrr::map_chr(graphs, ~ igraph::V(.x)$sub),
    c("3Me", "6Ac", "9Ac", "4Ac")
  )
  expect_identical(
    unname(structure_to_iupac(glycans)),
    c(iupacs[1:3], "Neuf5Gc4Ac(a2-")
  )
})


test_that("as_glycan_structure.character handles unknown linkages", {
  # Unknown linkages
  iupac <- "Man(a1-?)Man(?1-3)Man(?1-"
  glycan <- as_glycan_structure(iupac)
  expect_s3_class(glycan, "glyrepr_structure")
  expect_equal(structure_to_iupac(glycan), "Man(a1-?)Man(?1-3)Man(?1-")

  shorthand <- as_glycan_structure("Gal(?-?)GalNAc(?1-")
  expect_equal(structure_to_iupac(shorthand), "Gal(??-?)GalNAc(?1-")
})

test_that("as_glycan_structure.character handles multiple linkages", {
  # Multiple linkages
  iupac <- "Neu5Ac(a2-3/6)Gal(?1-"
  glycan <- as_glycan_structure(iupac)
  expect_s3_class(glycan, "glyrepr_structure")
  expect_equal(structure_to_iupac(glycan), "Neu5Ac(a2-3/6)Gal(?1-")

  unknown_choices <- c(
    "Gal(b1-4/?)GlcNAc(?1-",
    "Gal(b1-?/4)GlcNAc(?1-",
    "Gal(b1-3/?/6)GlcNAc(?1-"
  )
  normalized <- as_glycan_structure(unknown_choices)
  expect_equal(
    structure_to_iupac(normalized),
    rep("Gal(b1-?)GlcNAc(?1-", length(unknown_choices))
  )
})

test_that("as_glycan_structure.character works with vectors", {
  # Multiple IUPAC strings
  iupacs <- c("Man(?1-", "Gal(b1-3)GalNAc(?1-", "Neu5Ac(a2-")
  glycans <- as_glycan_structure(iupacs)
  expect_s3_class(glycans, "glyrepr_structure")
  expect_equal(length(glycans), 3)

  # Check each one
  expect_equal(structure_to_iupac(glycans)[1], "Man(?1-")
  expect_equal(structure_to_iupac(glycans)[2], "Gal(b1-3)GalNAc(?1-")
  expect_equal(structure_to_iupac(glycans)[3], "Neu5Ac(a2-")
})

test_that("as_glycan_structure.character handles complex O-glycan", {
  # Complex O-glycan structure
  iupac <- "Neu5Ac(a2-3)Gal(b1-4)[Fuc(a1-3)]GlcNAc(b1-6)[Neu5Ac(a2-3)Gal(b1-3)]GalNAc(?1-"
  glycan <- as_glycan_structure(iupac)
  expect_s3_class(glycan, "glyrepr_structure")
  expect_equal(length(glycan), 1)

  # Check the structure has the correct number of nodes
  graph <- get_structure_graphs(glycan, return_list = FALSE)
  expect_equal(igraph::vcount(graph), 7) # 7 monosaccharides
})

test_that("as_glycan_structure.character error handling", {
  # Empty string
  expect_error(as_glycan_structure(""), "Cannot parse empty")

  # NA is treated as a missing structure
  expect_true(is.na(as_glycan_structure(NA_character_)))

  # Invalid format - unknown monosaccharide
  expect_error(as_glycan_structure("invalid_format"), "Could not parse")
})

test_that("as_glycan_structure.character round-trip consistency", {
  # Test round-trip: structure -> IUPAC -> structure
  original_structures <- c(
    o_glycan_core_1(),
    n_glycan_core()
  )

  for (i in seq_along(original_structures)) {
    # Convert to IUPAC
    iupac <- structure_to_iupac(original_structures[[i]])

    # Parse back
    parsed <- as_glycan_structure(iupac)

    # Should be identical
    expect_equal(structure_to_iupac(parsed), iupac)
  }
})

# Edge cases and boundary conditions ----------------------------------------

test_that("as_glycan_structure.character handles whitespace", {
  # Leading and trailing whitespace should be handled gracefully
  expect_error(as_glycan_structure(" Man "), "Could not parse")
  expect_error(as_glycan_structure("  "), "Cannot parse empty")
  expect_error(as_glycan_structure("\t\n"), "Cannot parse empty")
})

test_that("as_glycan_structure.character handles malformed brackets", {
  # Unmatched brackets
  expect_error(as_glycan_structure("Man[Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man]Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man[[Gal]]"), "Could not parse")
  expect_error(as_glycan_structure("Man(a1-3)[Gal"), "Could not parse")
})

test_that("as_glycan_structure.character handles malformed linkages", {
  # Empty parentheses
  expect_error(as_glycan_structure("Man()Gal"), "Could not parse")

  # Incomplete linkages
  expect_error(as_glycan_structure("Man(a1)Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man(-3)Gal"), "Could not parse")

  # Invalid characters in linkages
  expect_error(as_glycan_structure("Man(c1-3)Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man(a0-3)Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man(a3-3)Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man(a1-0)Gal"), "Could not parse")
  expect_error(as_glycan_structure("Man(a#-3)Gal"), "Could not parse")
})

test_that("as_glycan_structure.character handles invalid monosaccharide names", {
  # Too short
  expect_error(as_glycan_structure("M"), "Could not parse")
  expect_error(as_glycan_structure("G"), "Could not parse")

  # Numbers at start
  expect_error(as_glycan_structure("1Man"), "Could not parse")
  expect_error(as_glycan_structure("2Gal"), "Could not parse")

  # Special characters
  expect_error(as_glycan_structure("Man@"), "Could not parse")
  expect_error(as_glycan_structure("Gal#"), "Could not parse")
  expect_error(as_glycan_structure("Man-Gal"), "Could not parse")

  # Case sensitivity (monosaccharide names are case sensitive)
  expect_error(as_glycan_structure("man"), "Could not parse")
  expect_error(as_glycan_structure("MAN"), "Could not parse")
  expect_error(as_glycan_structure("gal"), "Could not parse")
})

test_that("as_glycan_structure.character handles complex Neu variants", {
  # Test special Neu variants from glyparse
  glycan1 <- as_glycan_structure("Neu4Ac5Ac(?2-")
  graph1 <- get_structure_graphs(glycan1, return_list = FALSE)
  expect_equal(igraph::V(graph1)$mono, "Neu5Ac")
  expect_equal(igraph::V(graph1)$sub, "4Ac")

  glycan2 <- as_glycan_structure("Neu4Ac5Gc(?2-")
  graph2 <- get_structure_graphs(glycan2, return_list = FALSE)
  expect_equal(igraph::V(graph2)$mono, "Neu5Gc")
  expect_equal(igraph::V(graph2)$sub, "4Ac")

  # Neu5Ac should not be split even though it matches substituent pattern
  glycan3 <- as_glycan_structure("Neu5Ac(?2-")
  graph3 <- get_structure_graphs(glycan3, return_list = FALSE)
  expect_equal(igraph::V(graph3)$mono, "Neu5Ac")
  expect_equal(igraph::V(graph3)$sub, "")
})

test_that("as_glycan_structure.character handles invalid substituents", {
  # Invalid substituent positions
  expect_error(as_glycan_structure("Man0Ac"), "Could not parse")
  expect_error(as_glycan_structure("ManAAc"), "Could not parse")
  expect_error(as_glycan_structure("Man1X"), "Could not parse") # X is not a valid substituent
})

test_that("as_glycan_structure.character handles deeply nested structures", {
  # Very deep nesting
  deep_structure <- "Man(a1-2)[Man(a1-4)[Man(a1-4)[Man(a1-5)Man(a1-2)]Man(a1-3)]Man(a1-4)]Man(a1-"
  glycan <- as_glycan_structure(deep_structure)
  expect_s3_class(glycan, "glyrepr_structure")

  # Verify the graph has correct number of nodes
  graph <- get_structure_graphs(glycan, return_list = FALSE)
  expect_equal(igraph::vcount(graph), 8)
})

test_that("as_glycan_structure.character handles mixed valid/invalid in vectors", {
  # Vector with some valid and some invalid strings
  mixed_vector <- c("Man", "invalid_mono", "Gal")
  expect_error(as_glycan_structure(mixed_vector), "Could not parse")

  # All invalid
  all_invalid <- c("invalid1", "invalid2")
  expect_error(as_glycan_structure(all_invalid), "Could not parse")
})

test_that("as_glycan_structure.character accepts mixed mono types with NA", {
  mixed_vector <- c("Glc(a1-", NA, "Hex(a1-")

  result <- as_glycan_structure(mixed_vector)
  expect_identical(
    get_mono_type(result),
    c("concrete", NA_character_, "generic")
  )
})

test_that("as_glycan_structure.character handles extreme linkage cases", {
  # Maximum valid positions
  glycan1 <- as_glycan_structure("Man(a2-9)Gal(?1-")
  expect_s3_class(glycan1, "glyrepr_structure")

  glycan2 <- as_glycan_structure("Man(b1-1)Gal(?1-")
  expect_s3_class(glycan2, "glyrepr_structure")

  # Complex multi-position linkages
  glycan3 <- as_glycan_structure("Man(a1-2/3/4/5/6)Gal(?1-")
  expect_s3_class(glycan3, "glyrepr_structure")
  expect_equal(structure_to_iupac(glycan3), "Man(a1-2/3/4/5/6)Gal(?1-")
})

test_that("as_glycan_structure.character handles single character edge cases", {
  # Very short but invalid inputs
  expect_error(as_glycan_structure("("), "Could not parse")
  expect_error(as_glycan_structure(")"), "Could not parse")
  expect_error(as_glycan_structure("["), "Could not parse")
  expect_error(as_glycan_structure("]"), "Could not parse")
  expect_error(as_glycan_structure("-"), "Could not parse")
})

test_that("as_glycan_structure.character handles unusual anomer cases", {
  # Various anomer edge cases
  glycan1 <- as_glycan_structure("Neu5Ac(?2-")
  expect_equal(structure_to_iupac(glycan1), "Neu5Ac(?2-")

  glycan2 <- as_glycan_structure("Man(??-")
  expect_equal(structure_to_iupac(glycan2), "Man(??-")

  glycan3 <- as_glycan_structure("Fuc(?1-")
  expect_equal(structure_to_iupac(glycan3), "Fuc(?1-")
})

test_that("as_glycan_structure.character preserves complex substituent patterns", {
  # Test various substituent combinations
  glycan1 <- as_glycan_structure("Gal6S(b1-3)GlcNAc4S(?1-")
  graph1 <- get_structure_graphs(glycan1, return_list = FALSE)
  expect_equal(sort(igraph::V(graph1)$sub), sort(c("4S", "6S")))

  # Unknown position substituents
  glycan2 <- as_glycan_structure("Man?S(?1-")
  graph2 <- get_structure_graphs(glycan2, return_list = FALSE)
  expect_equal(igraph::V(graph2)$sub, "?S")
})

test_that("as_glycan_structure.character handles multiple substituents", {
  glycan <- as_glycan_structure("Glc3Me6S(a1-")
  graph <- get_structure_graphs(glycan, return_list = FALSE)
  expect_equal(igraph::V(graph)$sub, "3Me,6S")
})

test_that("as_glycan_structure.character handles multiple unknown substituents", {
  iupacs <- c("Gal?Me?S(a1-", "Gal?S?S(a1-")

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_equal(
    purrr::map_chr(graphs, ~ igraph::V(.x)$sub),
    c("?Me,?S", "?S,?S")
  )
  expect_equal(structure_to_iupac(glycans), iupacs)
})

test_that("as_glycan_structure.character handles ambiguous substituent positions", {
  iupacs <- c(
    "Gal4/6S(a1-",
    "Gal3/4/6S(a1-",
    "Gal4/6Ac(a1-",
    "Neu4/5Ac(a2-"
  )

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)

  expect_identical(
    purrr::map_chr(graphs, \(graph) igraph::V(graph)$mono),
    c("Gal", "Gal", "Gal", "Neu")
  )
  expect_identical(
    purrr::map_chr(graphs, \(graph) igraph::V(graph)$sub),
    c("4/6S", "3/4/6S", "4/6Ac", "4/5Ac")
  )
  expect_identical(unname(structure_to_iupac(glycans)), iupacs)
  expect_identical(unname(count_mono(glycans, "S")), c(1L, 1L, 0L, 0L))
})

test_that("ambiguous substituent alternatives are canonicalized", {
  iupacs <- c("Gal6/4S(a1-", "Gal4/6S(a1-", "Gal4/4S(a1-")

  glycans <- as_glycan_structure(iupacs)

  expect_identical(
    unname(structure_to_iupac(glycans)),
    c("Gal4/6S(a1-", "Gal4/6S(a1-", "Gal4S(a1-")
  )
  expect_identical(glycans[[1]], glycans[[2]])
})

test_that("as_glycan_structure.character rejects malformed ambiguous positions", {
  iupacs <- c("Gal/6S(a1-", "Gal4/S(a1-", "Gal4//6S(a1-")
  parsed <- purrr::map_lgl(iupacs, function(iupac) {
    tryCatch(
      {
        as_glycan_structure(iupac)
        TRUE
      },
      error = \(error) FALSE
    )
  })

  expect_identical(parsed, rep(FALSE, length(iupacs)))
})

test_that("as_glycan_structure.character prefers longer substituent tokens", {
  iupacs <- c("Glc3Pyr(a1-", "Glc3PC(a1-", "Glc3PPEtn(a1-", "Glc3PEtn(a1-")
  expected_subs <- c("3Pyr", "3PC", "3PPEtn", "3PEtn")

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)
  subs <- purrr::map_chr(graphs, ~ igraph::V(.x)$sub)

  expect_equal(subs, expected_subs)
  expect_equal(structure_to_iupac(glycans), iupacs)
})

test_that("as_glycan_structure.character handles glycolyl substituents", {
  iupacs <- c("Glc3NGc(a1-", "Glc3Gc(a1-")
  expected_subs <- c("3NGc", "3Gc")

  glycans <- as_glycan_structure(iupacs)
  graphs <- get_structure_graphs(glycans)
  subs <- purrr::map_chr(graphs, ~ igraph::V(.x)$sub)

  expect_equal(subs, expected_subs)
  expect_equal(structure_to_iupac(glycans), iupacs)
})

test_that("Neu monosaccharides with 5Ac are correctly parsed as Neu5Ac", {
  # Test cases where 5Ac should result in Neu5Ac base monosaccharide
  expect_equal(
    .extract_substituent("Neu3Me5Ac"),
    c(mono = "Neu5Ac", sub = "3Me")
  )
  expect_equal(
    .extract_substituent("Neu4Ac5Ac"),
    c(mono = "Neu5Ac", sub = "4Ac")
  )
  expect_equal(
    .extract_substituent("Neu4Ac5Ac9Ac"),
    c(mono = "Neu5Ac", sub = "4Ac,9Ac")
  )
  expect_equal(.extract_substituent("Neu7S5Ac"), c(mono = "Neu5Ac", sub = "7S"))
})

test_that("Neu monosaccharides with 5Gc are correctly parsed as Neu5Gc", {
  # Test cases where 5Gc should result in Neu5Gc base monosaccharide
  expect_equal(
    .extract_substituent("Neu3Me5Gc"),
    c(mono = "Neu5Gc", sub = "3Me")
  )
  expect_equal(
    .extract_substituent("Neu4Ac5Gc"),
    c(mono = "Neu5Gc", sub = "4Ac")
  )
  expect_equal(
    .extract_substituent("Neu3Gc5Gc"),
    c(mono = "Neu5Gc", sub = "3Gc")
  )
  expect_equal(.extract_substituent("Neu7S5Gc"), c(mono = "Neu5Gc", sub = "7S"))
})

test_that("Neu monosaccharides without 5Ac or 5Gc remain as Neu", {
  # Test cases where no 5Ac or 5Gc should result in Neu base monosaccharide
  expect_equal(.extract_substituent("Neu"), c(mono = "Neu", sub = ""))
  expect_equal(.extract_substituent("Neu7Ac"), c(mono = "Neu", sub = "7Ac"))
  expect_equal(
    .extract_substituent("Neu3Me7Ac"),
    c(mono = "Neu", sub = "3Me,7Ac")
  )
  expect_equal(.extract_substituent("Neu3Gc"), c(mono = "Neu", sub = "3Gc"))
})

test_that("Neu5Ac and Neu5Gc exact matches work correctly", {
  # Test exact matches
  expect_equal(.extract_substituent("Neu5Ac"), c(mono = "Neu5Ac", sub = ""))
  expect_equal(.extract_substituent("Neu5Gc"), c(mono = "Neu5Gc", sub = ""))
})

test_that("Neu5Ac and Neu5Gc with additional substituents work correctly", {
  # Test Neu5Ac/Neu5Gc with additional substituents
  expect_equal(
    .extract_substituent("Neu5Ac9Ac"),
    c(mono = "Neu5Ac", sub = "9Ac")
  )
  expect_equal(
    .extract_substituent("Neu5Gc9Ac"),
    c(mono = "Neu5Gc", sub = "9Ac")
  )
  expect_equal(
    .extract_substituent("Neu5Ac7S9Ac"),
    c(mono = "Neu5Ac", sub = "7S,9Ac")
  )
})

test_that("Error is thrown for monosaccharides with both 5Ac and 5Gc", {
  # This should be an error case
  expect_error(
    .extract_substituent("Neu5Ac5Gc"),
    "cannot have both 5Ac and 5Gc"
  )
  expect_error(
    .extract_substituent("Neu5Gc5Ac"),
    "cannot have both 5Ac and 5Gc"
  )
})

test_that("Full IUPAC parsing works with corrected Neu substituents", {
  # Test full IUPAC parsing
  result1 <- suppressMessages(as_glycan_structure(
    "Neu3Me5Ac(a2-3)Gal(b1-4)Glc(a1-"
  ))
  expect_true(is_glycan_structure(result1))

  result2 <- suppressMessages(as_glycan_structure(
    "Neu4Ac5Gc(a2-3)Gal(b1-4)Glc(a1-"
  ))
  expect_true(is_glycan_structure(result2))

  # Check that the output contains the correct monosaccharide names
  expect_match(as.character(result1), "Neu5Ac3Me")
  expect_match(as.character(result2), "Neu5Gc4Ac")
})

test_that("as_glycan_structure.character makes order of vertices and edges consistent with IUPAC-condensed", {
  glycan <- as_glycan_structure(
    "GlcNAc(b1-2)Man(a1-3)[GlcNAc(b1-2)Man(a1-6)]Man(b1-4)GlcNAc(b1-4)[Fuc(a1-3)]GlcNAc(b1-"
  )
  graph <- get_structure_graphs(glycan, return_list = FALSE)

  expect_equal(
    igraph::V(graph)$mono,
    c("GlcNAc", "Man", "GlcNAc", "Man", "Man", "GlcNAc", "Fuc", "GlcNAc")
  )
  expect_equal(
    igraph::E(graph)$linkage,
    c("b1-2", "a1-3", "b1-2", "a1-6", "b1-4", "b1-4", "a1-3")
  )
})

test_that("tree parsing preserves nested branch attachments and attributes", {
  graph <- .parse_iupac_tree_single(paste0(
    "Gal6S(b1-4)[Fuc(a1-3)]GlcNAc(b1-2)",
    "[Man(a1-6)]Man(b1-4)GlcNAc-ol"
  ))

  expect_identical(igraph::vertex_attr(graph, "name"), as.character(1:6))
  expect_identical(
    igraph::vertex_attr(graph, "mono"),
    c("GlcNAc", "Man", "Man", "GlcNAc", "Fuc", "Gal")
  )
  expect_identical(igraph::vertex_attr(graph, "sub"), c(rep("", 5), "6S"))
  expect_equal(
    igraph::as_edgelist(graph, names = FALSE),
    matrix(c(1, 2, 2, 3, 2, 4, 4, 5, 4, 6), ncol = 2, byrow = TRUE)
  )
  expect_identical(
    igraph::edge_attr(graph, "linkage"),
    c("b1-4", "a1-6", "b1-2", "a1-3", "b1-4")
  )
  expect_identical(
    igraph::graph_attr(graph),
    list(anomer = "?1", alditol = TRUE)
  )
})

test_that("single-residue trees retain empty linkage metadata", {
  graph <- .parse_iupac_tree_single("GlcNAc-ol(b1-")

  expect_identical(
    igraph::vertex_attr(graph),
    list(name = "1", mono = "GlcNAc", sub = "")
  )
  expect_identical(igraph::edge_attr(graph), list(linkage = character()))
  expect_identical(
    igraph::graph_attr(graph),
    list(anomer = "b1", alditol = TRUE)
  )
})

Try the glyrepr package in your browser

Any scripts or data that you put into this service are public.

glyrepr documentation built on Sept. 22, 2026, 5:09 p.m.