R/cidian.R

Defines functions validate_cidian_dictionary print.summary.cidian_dictionary summary.cidian_dictionary as.data.frame.cidian_dictionary print.cidian_dictionary cidian_metadata cidian_entries write_words read_cidian cidian_formats

Documented in as.data.frame.cidian_dictionary cidian_entries cidian_formats cidian_metadata print.cidian_dictionary read_cidian summary.cidian_dictionary write_words

#' List supported dictionary formats
#'
#' @return A character vector of format identifiers accepted by
#'   `read_cidian()`.
#' @export
#' @examples
#' cidian_formats()
cidian_formats <- function() {
  c("scel", "qcel", "qpyd", "bdict", "bcd")
}

#' Read a Chinese input-method dictionary
#'
#' `read_cidian()` reads a supported binary dictionary and returns a common
#' `cidian_dictionary` object. The parser preserves source order and does
#' not normalize, sort, or deduplicate entries.
#'
#' The format is inferred from the file extension when `format` is omitted or
#' `NULL`. Supply `format` when the file has no extension or its extension is
#' wrong.
#'
#' @param path A path to a dictionary file.
#' @param ... Must be empty.
#' @param format A format name, with or without a leading dot. Supported
#'   values are `"scel"`, `"qcel"`, `"qpyd"`, `"bdict"`, and `"bcd"`.
#'
#' @return A `cidian_dictionary` object containing `metadata`, `entries`, and
#'   `format` fields. The `entries$code` column is a list-column of character
#'   vectors, and `entries$weight` is a numeric vector.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' x
read_cidian <- function(
  path,
  ...,
  format = c("scel", "qcel", "qpyd", "bdict", "bcd")
) {
  rlang::check_dots_empty()

  # Check the path
  rlang::check_string(path, allow_empty = FALSE, arg = "path")
  path <- path.expand(path)
  if (!file.exists(path)) {
    cli::cli_abort(c(
      "Dictionary file does not exist.",
      "x" = "Cannot read {.path {path}}."
    ))
  }

  # Check the format
  if (missing(format) || is.null(format)) {
    format <- tolower(tools::file_ext(path))
    rlang::arg_match(format, values = cidian_formats(), error_arg = "format")
  } else {
    rlang::check_string(format, allow_empty = FALSE, arg = "format")
    format <- tolower(sub("^\\.", "", format))
    rlang::arg_match(format, values = cidian_formats(), error_arg = "format")
  }

  result <- tryCatch(
    parse_cidian(path, format),
    error = function(error) {
      cli::cli_abort(
        c(
          "Failed to parse dictionary file.",
          "x" = "{conditionMessage(error)}"
        ),
        parent = error
      )
    }
  )
  class(result) <- c("cidian_dictionary", "list")
  result
}

#' Write dictionary entries to a text file
#'
#' `write_words()` writes the word of every entry in `x` to `path`,
#' one word per line, preserving the source order.
#'
#' When `path` already exists, the function asks for confirmation unless
#' `overwrite` is given explicitly.
#'
#' @param x A `cidian_dictionary` object.
#' @param path A path to the output file.
#' @param ... Must be empty.
#' @param overwrite Whether to replace `path` when it already exists. `NULL`
#'   (the default) asks interactively; `TRUE` overwrites without asking;
#'   `FALSE` errors.
#'
#' @return The `path`, invisibly.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' write_words(x, tempfile(fileext = ".txt"))
write_words <- function(x, path, ..., overwrite = NULL) {
  rlang::check_dots_empty()

  validate_cidian_dictionary(x)
  rlang::check_string(path, allow_empty = FALSE, arg = "path")
  rlang::check_bool(overwrite, allow_null = TRUE)

  path <- path.expand(path)

  if (file.exists(path)) {
    if (is.null(overwrite)) {
      proceed <- utils::askYesNo(
        paste0("Overwrite ", path, "?"),
        default = FALSE
      )
      if (!isTRUE(proceed)) {
        cli::cli_abort("Not overwriting {.path {path}}.")
      }
    } else if (!isTRUE(overwrite)) {
      cli::cli_abort(c(
        "Output file already exists.",
        "i" = "Supply {.code overwrite = TRUE} to replace {.path {path}}."
      ))
    }
  }

  writeLines(cidian_entries(x)$word, path, useBytes = TRUE)
  invisible(path)
}

#' Extract dictionary entries
#'
#' @param x A `cidian_dictionary` object.
#'
#' @return A base `data.frame` with `word`, `code`, and `weight` columns.
#'   `code` is a list-column of character vectors and `weight` is numeric.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' head(cidian_entries(x))
cidian_entries <- function(x) {
  validate_cidian_dictionary(x)
  x$entries
}

#' Extract dictionary metadata
#'
#' @param x A `cidian_dictionary` object.
#'
#' @return A list with `name`, `category`, `description`, and `extra` fields.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' cidian_metadata(x)
cidian_metadata <- function(x) {
  validate_cidian_dictionary(x)
  x$metadata
}

#' Print a dictionary summary
#'
#' @param x A `cidian_dictionary` object.
#' @param ... Must be empty.
#'
#' @return Invisibly returns a `summary.cidian_dictionary` object summarizing `x`.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' print(x)
print.cidian_dictionary <- function(x, ...) {
  rlang::check_dots_empty()
  validate_cidian_dictionary(x)
  print(summary(x))
}

#' Convert a dictionary to a data frame
#'
#' @param x A `cidian_dictionary` object.
#' @param ... Must be empty.
#'
#' @return A base `data.frame` with `word`, `code`, and `weight` columns.
#'   `code` is a list-column of character vectors and `weight` is numeric.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' head(as.data.frame(x))
as.data.frame.cidian_dictionary <- function(x, ...) {
  rlang::check_dots_empty()
  validate_cidian_dictionary(x)
  cidian_entries(x)
}

#' Summarize a dictionary
#'
#' @param object A `cidian_dictionary` object.
#' @param ... Must be empty.
#'
#' @return An object of class `summary.cidian_dictionary` containing the
#'   format, metadata, entry count, and number of weighted entries.
#' @export
#' @examples
#' x <- read_cidian(system.file("extdata", "computer.qcel", package = "cidian"))
#' summary(x)
summary.cidian_dictionary <- function(object, ...) {
  rlang::check_dots_empty()
  validate_cidian_dictionary(object)

  metadata <- cidian_metadata(object)
  entries <- cidian_entries(object)
  result <- list(
    format = object$format,
    name = metadata$name,
    category = metadata$category,
    description = metadata$description,
    entries = nrow(entries),
    weighted_entries = sum(!is.na(entries$weight))
  )
  class(result) <- "summary.cidian_dictionary"
  result
}

#' @export
#' @noRd
print.summary.cidian_dictionary <- function(x, ...) {
  cat("<summary.cidian_dictionary>\n")
  cat("Format:", toupper(x$format), "\n")
  if (!is.null(x$name)) {
    cat("Name: ", x$name, "\n", sep = "")
  }
  if (!is.null(x$category)) {
    cat("Category: ", x$category, "\n", sep = "")
  }
  cat(
    "Entries:",
    format(x$entries, big.mark = ",", trim = TRUE),
    "\n"
  )
  cat(
    "Weighted entries:",
    format(x$weighted_entries, big.mark = ",", trim = TRUE),
    "\n"
  )

  invisible(x)
}

validate_cidian_dictionary <- function(x) {
  if (!inherits(x, "cidian_dictionary") || !is.list(x)) {
    cli::cli_abort(
      "{.arg x} must be a {.cls cidian_dictionary} object.",
      class = "cidian_object_error"
    )
  }
  required <- c("metadata", "entries", "format")
  if (!all(required %in% names(x))) {
    cli::cli_abort(
      "{.arg x} is not a valid {.cls cidian_dictionary} object.",
      class = "cidian_object_error"
    )
  }
  invisible(x)
}

Try the cidian package in your browser

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

cidian documentation built on Sept. 28, 2026, 5:08 p.m.