From d5eed16176089b5f4ff5f6f433b68a76a150d748 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Thu, 11 Jul 2024 11:04:58 +0200 Subject: [PATCH 01/12] add test feed --- inst/extdata/locations_feed.zip | Bin 0 -> 7650 bytes inst/tinytest/test_import_gtfs.R | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 inst/extdata/locations_feed.zip diff --git a/inst/extdata/locations_feed.zip b/inst/extdata/locations_feed.zip new file mode 100644 index 0000000000000000000000000000000000000000..60ff15023dd79ba4ce92ac5fa2e3b7374317f5b7 GIT binary patch literal 7650 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 literal 0 HcmV?d00001 diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index 728538f..18251b6 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -431,3 +431,10 @@ expect_error(tester(not_gtfs_file), class = "path_must_be_zip") not_gtfs_url <- "https://www.google.com" expect_error(tester(not_gtfs_url), class = "path_must_be_zip") + +# issue #36 --------------------------------------------------------------- +# locations.geojson files should be read without warning + +expect_silent( + import_gtfs(system.file("extdata/locations_feed.zip", package = "gtfsio")) +) From 89c164685da1bce30709380bf21b4ccdd88c46ba Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Thu, 11 Jul 2024 11:44:20 +0200 Subject: [PATCH 02/12] read geojson as json with jsonlite read_files now needs file extension --- DESCRIPTION | 5 ++-- NAMESPACE | 1 + R/import_gtfs.R | 50 +++++++++++++++++++++++--------- inst/tinytest/test_import_gtfs.R | 5 ++++ man/read_files.Rd | 2 +- man/read_geojson.Rd | 15 ++++++++++ 6 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 man/read_geojson.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 9fd6077..26a87f7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -41,7 +41,8 @@ BugReports: https://github.com/r-transit/gtfsio/issues Imports: data.table, utils, - zip + zip, + jsonlite Suggests: knitr, rmarkdown, @@ -50,7 +51,7 @@ VignetteBuilder: knitr Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 Collate: 'gtfsio_error.R' 'assert_gtfs.R' diff --git a/NAMESPACE b/NAMESPACE index 2020898..5d88933 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -16,3 +16,4 @@ export(import_gtfs) export(new_gtfs) importFrom(data.table,"%chin%") importFrom(data.table,":=") +importFrom(jsonlite,read_json) diff --git a/R/import_gtfs.R b/R/import_gtfs.R index 36cdff0..41d1363 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -117,18 +117,17 @@ import_gtfs <- function(path, ) if (inherits(files_in_gtfs, "error")) error_path_must_be_zip() - non_text_files <- files_in_gtfs[!grepl("\\.txt$", files_in_gtfs)] + non_standard_file_ext <- files_in_gtfs[!(grepl("\\.txt$", files_in_gtfs) | grepl("\\.geojson$", files_in_gtfs))] - if (!identical(non_text_files, character(0))) { + if (!identical(non_standard_file_ext, character(0))) { warning( - "Found non .txt files when attempting to read the GTFS feed: ", - paste(non_text_files, collapse = ", "), "\n", + "Found non .txt/.geojson files when attempting to read the GTFS feed: ", + paste(non_standard_file_ext, collapse = ", "), "\n", "These files have been ignored and were not imported to the GTFS object.", call. = FALSE ) } - files_in_gtfs <- setdiff(files_in_gtfs, non_text_files) - files_in_gtfs <- gsub("\\.txt", "", files_in_gtfs) + files_in_gtfs <- setdiff(files_in_gtfs, non_standard_file_ext) # read only the text files specified either in 'files' or in 'skip'. # if both are NULL, read all text files @@ -165,7 +164,7 @@ import_gtfs <- function(path, zip::unzip( path, - files = paste0(files_to_read, ".txt"), + files = files_to_read, exdir = tmpdir, overwrite = TRUE ) @@ -173,7 +172,7 @@ import_gtfs <- function(path, if (!quiet) message( "Unzipped the following files to ", tmpdir, ":\n", - paste0(" * ", files_to_read, ".txt", collapse = "\n") + paste0(" * ", files_to_read, collapse = "\n") ) # get GTFS standards to assign correct classes to each field @@ -203,7 +202,7 @@ import_gtfs <- function(path, USE.NAMES = FALSE ) - names(gtfs) <- file_names + names(gtfs) <- remove_file_ext(file_names) # create gtfs object from 'gtfs' @@ -219,7 +218,7 @@ import_gtfs <- function(path, #' #' Reads a GTFS text file from the main \code{.zip} file. #' -#' @param file A string. The name of the file (without \code{.txt} extension) to +#' @param file A string. The name of the file (with \code{.txt} or \code{.geojson} extension) to #' be read. #' @param gtfs_standards A named list. Created by #' \code{\link{get_gtfs_standards}}. @@ -251,9 +250,19 @@ read_files <- function(file, # create object to hold the file with '.txt' extension - file_txt <- paste0(file, ".txt") + file_ext <- file + file_type <- "txt" + if(grepl("\\.geojson$", file)) { + file_type <- "geojson" + } + file <- remove_file_ext(file) + + if (!quiet) message("Reading ", file) - if (!quiet) message("Reading ", file_txt) + # read geojson via separate function and return + if (file_type == "geojson") { + return(read_geojson(file.path(tmpdir, file_ext))) + } # get standards for reading and fields to be read from the given 'file' @@ -282,7 +291,7 @@ read_files <- function(file, withCallingHandlers( { sample_dt <- data.table::fread( - file.path(tmpdir, file_txt), + file.path(tmpdir, file_ext), nrows = 1, colClasses = "character" ) @@ -356,7 +365,7 @@ read_files <- function(file, withCallingHandlers( { full_dt <- data.table::fread( - file.path(tmpdir, file_txt), + file.path(tmpdir, file_ext), select = fields_classes, encoding = encoding ) @@ -368,6 +377,19 @@ read_files <- function(file, } +#' Read geojson file +#' +#' @param file.geojson geojson file +#' +#' @keywords internal +#' @importFrom jsonlite read_json +read_geojson <- function(file.geojson) { + read_json(file.geojson) +} + +remove_file_ext = function(file) { + gsub("\\.txt$", "", gsub("\\.geojson$", "", file)) +} # errors ------------------------------------------------------------------ diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index 18251b6..a5f96e7 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -438,3 +438,8 @@ expect_error(tester(not_gtfs_url), class = "path_must_be_zip") expect_silent( import_gtfs(system.file("extdata/locations_feed.zip", package = "gtfsio")) ) + +locations_feed <- import_gtfs(system.file("extdata/locations_feed.zip", package = "gtfsio")) + +expect_inherits(locations_feed[["locations"]], "list") +expect_equal(names(locations_feed[["locations"]]), c("type", "name", "crs", "features")) diff --git a/man/read_files.Rd b/man/read_files.Rd index 8179cb9..791cec1 100644 --- a/man/read_files.Rd +++ b/man/read_files.Rd @@ -7,7 +7,7 @@ read_files(file, gtfs_standards, fields, extra_spec, tmpdir, quiet, encoding) } \arguments{ -\item{file}{A string. The name of the file (without \code{.txt} extension) to +\item{file}{A string. The name of the file (with \code{.txt} or \code{.geojson} extension) to be read.} \item{gtfs_standards}{A named list. Created by diff --git a/man/read_geojson.Rd b/man/read_geojson.Rd new file mode 100644 index 0000000..87a5425 --- /dev/null +++ b/man/read_geojson.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/import_gtfs.R +\name{read_geojson} +\alias{read_geojson} +\title{Read geojson file} +\usage{ +read_geojson(file.geojson) +} +\arguments{ +\item{file.geojson}{geojson file} +} +\description{ +Read geojson file +} +\keyword{internal} From d9f29e72e709d72402327fd6f96e93a74399aafa Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Thu, 11 Jul 2024 13:25:32 +0200 Subject: [PATCH 03/12] separate handling of file variables with/without extension add file_ext to specs --- R/get_gtfs_standards.R | 30 +++++++++++++++- R/import_gtfs.R | 59 ++++++++++++++++++++------------ inst/tinytest/test_import_gtfs.R | 2 +- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/R/get_gtfs_standards.R b/R/get_gtfs_standards.R index 1484625..3712c84 100644 --- a/R/get_gtfs_standards.R +++ b/R/get_gtfs_standards.R @@ -46,6 +46,7 @@ #' @export get_gtfs_standards <- function() { agency <- list( + file_ext = "txt", file_spec = "req", agency_id = list("id", "cond"), agency_name = list("text", "req"), @@ -58,6 +59,7 @@ get_gtfs_standards <- function() { ) stops <- list( + file_ext = "txt", file_spec = "req", stop_id = list("id", "req"), stop_code = list("text", "opt"), @@ -77,6 +79,7 @@ get_gtfs_standards <- function() { ) routes <- list( + file_ext = "txt", file_spec = "req", route_id = list("id", "req"), agency_id = list("id", "cond"), @@ -95,6 +98,7 @@ get_gtfs_standards <- function() { ) trips <- list( + file_ext = "txt", file_spec = "req", route_id = list("id", "req"), service_id = list("id", "req"), @@ -109,6 +113,7 @@ get_gtfs_standards <- function() { ) stop_times <- list( + file_ext = "txt", file_spec = "req", trip_id = list("id", "req"), arrival_time = list("time", "cond"), @@ -125,6 +130,7 @@ get_gtfs_standards <- function() { ) calendar <- list( + file_ext = "txt", file_spec = "cond", service_id = list("id", "req"), monday = list("enum", "req", c(0, 1)), @@ -139,6 +145,7 @@ get_gtfs_standards <- function() { ) calendar_dates <- list( + file_ext = "txt", file_spec = "cond", service_id = list("id", "req"), date = list("date", "req"), @@ -146,6 +153,7 @@ get_gtfs_standards <- function() { ) fare_attributes <- list( + file_ext = "txt", file_spec = "opt", fare_id = list("id", "req"), price = list("float", "req"), @@ -157,6 +165,7 @@ get_gtfs_standards <- function() { ) fare_rules <- list( + file_ext = "txt", file_spec = "opt", fare_id = list("id", "req"), route_id = list("id", "opt"), @@ -166,6 +175,7 @@ get_gtfs_standards <- function() { ) fare_products <- list( + file_ext = "txt", file_spec = "opt", fare_product_id = list("id", "req"), fare_product_name = list("text", "opt"), @@ -174,6 +184,7 @@ get_gtfs_standards <- function() { ) fare_leg_rules <- list( + file_ext = "txt", file_spec = "opt", leg_group_id = list("id", "opt"), network_id = list("id", "opt"), @@ -183,6 +194,7 @@ get_gtfs_standards <- function() { ) fare_transfer_rules <- list( + file_ext = "txt", file_spec = "opt", from_leg_group_id = list("id", "opt"), to_leg_group_id = list("id", "opt"), @@ -194,18 +206,21 @@ get_gtfs_standards <- function() { ) areas <- list( + file_ext = "txt", file_spec = "opt", area_id = list("id", "req"), area_name = list("text", "opt") ) stop_areas <- list( + file_ext = "txt", file_spec = "opt", area_id = list("id", "req"), stop_id = list("id", "opt") ) shapes <- list( + file_ext = "txt", file_spec = "opt", shape_id = list("id", "req"), shape_pt_lat = list("latitude", "req"), @@ -215,6 +230,7 @@ get_gtfs_standards <- function() { ) frequencies <- list( + file_ext = "txt", file_spec = "opt", trip_id = list("id", "req"), start_time = list("time", "req"), @@ -224,6 +240,7 @@ get_gtfs_standards <- function() { ) transfers <- list( + file_ext = "txt", file_spec = "opt", from_stop_id = list("id", "cond"), to_stop_id = list("id", "cond"), @@ -236,6 +253,7 @@ get_gtfs_standards <- function() { ) pathways <- list( + file_ext = "txt", file_spec = "opt", pathway_id = list("id", "req"), from_stop_id = list("id", "req"), @@ -252,6 +270,7 @@ get_gtfs_standards <- function() { ) levels <- list( + file_ext = "txt", file_spec = "cond", level_id = list("id", "req"), level_index = list("float", "req"), @@ -259,6 +278,7 @@ get_gtfs_standards <- function() { ) translations <- list( + file_ext = "txt", file_spec = "opt", table_name = list("enum", "req", c("agency", "stops", "routes", "trips", "stop_times", @@ -273,6 +293,7 @@ get_gtfs_standards <- function() { ) feed_info <- list( + file_ext = "txt", file_spec = "cond", feed_publisher_name = list("text", "req"), feed_publisher_url = list("url", "req"), @@ -286,6 +307,7 @@ get_gtfs_standards <- function() { ) attributions <- list( + file_ext = "txt", file_spec = "opt", attribution_id = list("id", "opt"), agency_id = list("id", "opt"), @@ -300,6 +322,11 @@ get_gtfs_standards <- function() { attribution_phone = list("phone_number", "opt") ) + locations <- list( + file_ext = "geojson", + file_spec = "opt" + ) + # create gtfs_standards object gtfs_standards <- list( @@ -324,7 +351,8 @@ get_gtfs_standards <- function() { levels = levels, translations = translations, feed_info = feed_info, - attributions = attributions + attributions = attributions, + locations = locations ) # define R types most similar to GTFS reference types diff --git a/R/import_gtfs.R b/R/import_gtfs.R index 41d1363..72d9fc8 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -110,47 +110,48 @@ import_gtfs <- function(path, # check which files are inside the GTFS. if any non text file is found, raise # a warning and do not try to read it as a csv. remove the '.txt' extension # from the text files to reference them without it in messages and errors + # filenames: file with extension (.txt/.geojson), file: withouth extension - files_in_gtfs <- tryCatch( + filenames_in_gtfs <- tryCatch( zip::zip_list(path)$filename, error = function(cnd) cnd ) - if (inherits(files_in_gtfs, "error")) error_path_must_be_zip() + if (inherits(filenames_in_gtfs, "error")) error_path_must_be_zip() - non_standard_file_ext <- files_in_gtfs[!(grepl("\\.txt$", files_in_gtfs) | grepl("\\.geojson$", files_in_gtfs))] + non_standard_file_ext <- filenames_in_gtfs[!(grepl("\\.txt$", filenames_in_gtfs) | grepl("\\.geojson$", filenames_in_gtfs))] if (!identical(non_standard_file_ext, character(0))) { warning( - "Found non .txt/.geojson files when attempting to read the GTFS feed: ", + "Found non .txt or .geojson files when attempting to read the GTFS feed: ", paste(non_standard_file_ext, collapse = ", "), "\n", "These files have been ignored and were not imported to the GTFS object.", call. = FALSE ) } - files_in_gtfs <- setdiff(files_in_gtfs, non_standard_file_ext) + filenames_in_gtfs <- setdiff(filenames_in_gtfs, non_standard_file_ext) # read only the text files specified either in 'files' or in 'skip'. # if both are NULL, read all text files - if (!is.null(files)) { - files_to_read <- files + filenames_to_read <- append_file_ext(files) } else if (!is.null(skip)) { - files_to_read <- setdiff(files_in_gtfs, skip) + filenames_to_read <- setdiff(filenames_in_gtfs, append_file_ext(skip)) } else { - files_to_read <- files_in_gtfs + filenames_to_read <- filenames_in_gtfs } # check if all specified files exist and raise an error if any does not - missing_files <- files_to_read[! files_to_read %chin% files_in_gtfs] + missing_files <- filenames_to_read[! filenames_to_read %chin% filenames_in_gtfs] + if (!identical(missing_files, character(0))) { error_gtfs_missing_files(missing_files) } # raise an error if a file is specified in 'fields' but does not appear in - # 'files_to_read' + # 'filenames_to_read' - files_misspec <- names(fields)[! names(fields) %chin% files_to_read] + files_misspec <- names(fields)[! names(fields) %chin% remove_file_ext(filenames_to_read)] if (!is.null(files_misspec) & !identical(files_misspec, character(0))) { error_files_misspecified(files_misspec) @@ -164,7 +165,7 @@ import_gtfs <- function(path, zip::unzip( path, - files = files_to_read, + files = filenames_to_read, exdir = tmpdir, overwrite = TRUE ) @@ -172,7 +173,7 @@ import_gtfs <- function(path, if (!quiet) message( "Unzipped the following files to ", tmpdir, ":\n", - paste0(" * ", files_to_read, collapse = "\n") + paste0(" * ", filenames_to_read, collapse = "\n") ) # get GTFS standards to assign correct classes to each field @@ -182,7 +183,7 @@ import_gtfs <- function(path, # read files into list gtfs <- lapply( - X = files_to_read, + X = filenames_to_read, FUN = read_files, gtfs_standards, fields, @@ -196,7 +197,7 @@ import_gtfs <- function(path, # need to be stripped here file_names <- vapply( - files_to_read, + filenames_to_read, function(i) utils::tail(strsplit(i, .Platform$file.sep)[[1]], 1), character(1), USE.NAMES = FALSE @@ -250,7 +251,7 @@ read_files <- function(file, # create object to hold the file with '.txt' extension - file_ext <- file + filename <- file file_type <- "txt" if(grepl("\\.geojson$", file)) { file_type <- "geojson" @@ -259,9 +260,9 @@ read_files <- function(file, if (!quiet) message("Reading ", file) - # read geojson via separate function and return + # read geojson and return if (file_type == "geojson") { - return(read_geojson(file.path(tmpdir, file_ext))) + return(read_geojson(file.path(tmpdir, filename))) } # get standards for reading and fields to be read from the given 'file' @@ -291,7 +292,7 @@ read_files <- function(file, withCallingHandlers( { sample_dt <- data.table::fread( - file.path(tmpdir, file_ext), + file.path(tmpdir, filename), nrows = 1, colClasses = "character" ) @@ -365,7 +366,7 @@ read_files <- function(file, withCallingHandlers( { full_dt <- data.table::fread( - file.path(tmpdir, file_ext), + file.path(tmpdir, filename), select = fields_classes, encoding = encoding ) @@ -391,6 +392,22 @@ remove_file_ext = function(file) { gsub("\\.txt$", "", gsub("\\.geojson$", "", file)) } +append_file_ext = function(file) { + gtfs_standards <- get_gtfs_standards() + vapply(file, function(f) { + file_ext <- gtfs_standards[[f]][["file_ext"]] + if (is.null(file_ext)) { + # use default for argument-specified non-standard files, behaviour defined in test_import_gtfs.R#292 + file_ext <- "txt" + } + if(grepl(paste0("\\.", file_ext, "$"), f)) { + return(f) # file extension already present + } else { + return(paste0(f, ".", file_ext)) + } + }, ".txt", USE.NAMES = FALSE) +} + # errors ------------------------------------------------------------------ diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index a5f96e7..a314bc4 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -55,7 +55,7 @@ expect_error( tester(files = "ola"), pattern = paste0( "The provided GTFS feed doesn't contain the following ", - "text file\\(s\\): 'ola'" + "text file\\(s\\): 'ola.txt'" ), class = "gtfs_missing_files" ) From 963bf9bcff58b15e8a237229b37fa0dd489356c8 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Mon, 26 Aug 2024 09:50:23 +0200 Subject: [PATCH 04/12] write json in export_gtfs() --- R/export_gtfs.R | 40 ++++++++++++++++++-------------- inst/tinytest/test_export_gtfs.R | 10 ++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/R/export_gtfs.R b/R/export_gtfs.R index c92f937..ca434a4 100644 --- a/R/export_gtfs.R +++ b/R/export_gtfs.R @@ -119,35 +119,41 @@ export_gtfs <- function(gtfs, if (!quiet) message("Writing text files to ", tmpd) - for (file in files) { + filenames = append_file_ext(files) - filename <- paste0(file, ".txt") + for (filename in filenames) { + + file <- remove_file_ext(filename) filepath <- file.path(tmpd, filename) if (!quiet) message(" - Writing ", filename) - dt <- gtfs[[file]] + dt <- gtfs[[remove_file_ext(filename)]] - # if 'standard_only' is set to TRUE, remove non-standard fields from 'dt' - # before writing it to disk + if(endsWith(filename, ".geojson")) { + jsonlite::write_json(dt, filepath, pretty = FALSE, auto_unbox = TRUE, digits = 8) + } else { - if (standard_only) { + # if 'standard_only' is set to TRUE, remove non-standard fields from 'dt' + # before writing it to disk - file_cols <- names(dt) - extra_cols <- setdiff(file_cols, names(gtfs_standards[[file]])) + if (standard_only) { - if (!identical(extra_cols, character(0))) dt <- dt[, !..extra_cols] + file_cols <- names(dt) + extra_cols <- setdiff(file_cols, names(gtfs_standards[[file]])) - } + if (!identical(extra_cols, character(0))) dt <- dt[, !..extra_cols] - # print warning message if warning is raised and 'quiet' is FALSE - withCallingHandlers( - data.table::fwrite(dt, filepath, scipen = 999), - warning = function(cnd) { - if (!quiet) message(" - ", conditionMessage(cnd)) } - ) + # print warning message if warning is raised and 'quiet' is FALSE + withCallingHandlers( + data.table::fwrite(dt, filepath, scipen = 999), + warning = function(cnd) { + if (!quiet) message(" - ", conditionMessage(cnd)) + } + ) + } } # zip the contents of 'tmpd' to 'path', if as_dir = FALSE @@ -161,7 +167,7 @@ export_gtfs <- function(gtfs, unlink(path, recursive = TRUE) - filepaths <- file.path(tmpd, paste0(files, ".txt")) + filepaths <- file.path(tmpd, filenames) zip::zip( path, diff --git a/inst/tinytest/test_export_gtfs.R b/inst/tinytest/test_export_gtfs.R index ad11850..100b0ae 100644 --- a/inst/tinytest/test_export_gtfs.R +++ b/inst/tinytest/test_export_gtfs.R @@ -294,3 +294,13 @@ resulting_shapes_content <- readLines(file.path(target_dir, "shapes.txt")) expect_false(identical(resulting_shapes_content[3], "b,2,41,41,1e+07")) expect_identical(resulting_shapes_content[3], "b,2,41,41,10000000") + +# issue #36 --------------------------------------------------------------- +# re-reading written json files are the same + +locations_feed <- import_gtfs(system.file("extdata/locations_feed.zip", package = "gtfsio")) +tmpfile <- tempfile(fileext = ".zip") +export_gtfs(locations_feed, tmpfile) +reimported <- import_gtfs(tmpfile) + +expect_equal(reimported, locations_feed) From 554bf657cd6ec7a36fadb4a4c19a5785f4f2766f Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Tue, 27 Aug 2024 11:57:37 +0200 Subject: [PATCH 05/12] parse reference and set conversion types automatically --- DESCRIPTION | 3 + R/export_gtfs.R | 10 +- R/get_gtfs_standards.R | 434 ++---------------- R/import_gtfs.R | 5 +- R/sysdata.rda | Bin 0 -> 2824 bytes inst/reference/create_gtfs_standards.R | 69 +++ .../gtfsio_field_conversion_types.csv | 1 + inst/reference/parse_markdown.R | 100 ++++ inst/tinytest/test_export_gtfs.R | 15 +- inst/tinytest/test_import_gtfs.R | 1 + man/get_gtfs_standards.Rd | 55 ++- man/translate_types.Rd | 23 - 12 files changed, 249 insertions(+), 467 deletions(-) create mode 100644 R/sysdata.rda create mode 100644 inst/reference/create_gtfs_standards.R create mode 100644 inst/reference/gtfsio_field_conversion_types.csv create mode 100644 inst/reference/parse_markdown.R delete mode 100644 man/translate_types.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 26a87f7..10d4355 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -64,3 +64,6 @@ Collate: 'gtfsio.R' 'import_gtfs.R' 'new_gtfs.R' +LazyData: true +Depends: + R (>= 3.1.0) diff --git a/R/export_gtfs.R b/R/export_gtfs.R index ca434a4..f51e2e1 100644 --- a/R/export_gtfs.R +++ b/R/export_gtfs.R @@ -120,11 +120,13 @@ export_gtfs <- function(gtfs, if (!quiet) message("Writing text files to ", tmpd) filenames = append_file_ext(files) + filepaths <- file.path(tmpd, filename) - for (filename in filenames) { + for (i in seq_along(files)) { - file <- remove_file_ext(filename) - filepath <- file.path(tmpd, filename) + filename <- filenames[i] + file <- files[i] + filepath <- filepaths[i] if (!quiet) message(" - Writing ", filename) @@ -167,8 +169,6 @@ export_gtfs <- function(gtfs, unlink(path, recursive = TRUE) - filepaths <- file.path(tmpd, filenames) - zip::zip( path, filepaths, diff --git a/R/get_gtfs_standards.R b/R/get_gtfs_standards.R index 3712c84..5ab67d1 100644 --- a/R/get_gtfs_standards.R +++ b/R/get_gtfs_standards.R @@ -5,14 +5,11 @@ #' GTFS feeds with R. Each list element (also a list) represents a distinct GTFS #' table, and describes: #' -#' - whether the table is required, optional or conditionally required; #' - the fields that compose the table, including which R data type is best #' suited to represent it, whether the field is required, optional or -#' conditionally required, and which values it can assume (most relevant to GTFS -#' `ENUM`s. -#' -#' Note: the standards list is based on the specification as revised in May 9th, -#' 2022. +#' conditionally required. +#' - whether the table is required, optional or conditionally required (as an +#' attribute) #' #' @return A named list, in which each element represents the R equivalent of #' each GTFS table standard. @@ -21,414 +18,39 @@ #' GTFS standards were derived from [GTFS Schedule #' Reference](https://gtfs.org/schedule/reference/). The R data types chosen to #' represent each GTFS data type are described below: -#' -#' - Color = `character` -#' - Currency amount = `numeric` -#' - Currency code = `character` -#' - Date = `integer` -#' - Email = `character` -#' - ENUM = `integer` -#' - ID = `character` -#' - Integer = `integer` -#' - Language code = `character` -#' - Latitude = `numeric` -#' - Longitude = `numeric` -#' - Float = `numeric` -#' - Phone number = `character` -#' - Text = `character` -#' - Time = `character` -#' - Timezone = `character` -#' - URL = `character` +#' `r .doc_field_types()` #' #' @examples #' gtfs_standards <- get_gtfs_standards() #' #' @export get_gtfs_standards <- function() { - agency <- list( - file_ext = "txt", - file_spec = "req", - agency_id = list("id", "cond"), - agency_name = list("text", "req"), - agency_url = list("url", "req"), - agency_timezone = list("timezone", "req"), - agency_lang = list("language_code", "opt"), - agency_phone = list("phone_number", "opt"), - agency_fare_url = list("url", "opt"), - agency_email = list("email", "opt") - ) - - stops <- list( - file_ext = "txt", - file_spec = "req", - stop_id = list("id", "req"), - stop_code = list("text", "opt"), - stop_name = list("text", "cond"), - tts_stop_name = list("text", "opt"), - stop_desc = list("text", "opt"), - stop_lat = list("latitude", "cond"), - stop_lon = list("longitude", "cond"), - zone_id = list("id", "cond"), - stop_url = list("url", "opt"), - location_type = list("enum", "opt", c(0, 1, 2, 3, 4)), - parent_station = list("id", "cond"), - stop_timezone = list("timezone", "opt"), - wheelchair_boarding = list("enum", "opt", c(0, 1, 2)), - level_id = list("id", "opt"), - platform_code = list("text", "opt") - ) - - routes <- list( - file_ext = "txt", - file_spec = "req", - route_id = list("id", "req"), - agency_id = list("id", "cond"), - route_short_name = list("text", "cond"), - route_long_name = list("text", "cond"), - route_desc = list("text", "opt"), - route_type = list("enum", "req", c(0, 1, 2, 3, 4, 5, 6, 7, 11, - 12)), - route_url = list("url", "opt"), - route_color = list("color", "opt"), - route_text_color = list("color", "opt"), - route_sort_order = list("integer", "opt"), - continuous_pickup = list("enum", "opt", c(0, 1, 2, 3)), - continuous_dropoff = list("enum", "opt", c(0, 1, 2, 3)), - network_id = list("id", "opt") - ) - - trips <- list( - file_ext = "txt", - file_spec = "req", - route_id = list("id", "req"), - service_id = list("id", "req"), - trip_id = list("id", "req"), - trip_headsign = list("text", "opt"), - trip_short_name = list("text", "opt"), - direction_id = list("enum", "opt", c(0, 1)), - block_id = list("id", "opt"), - shape_id = list("id", "cond"), - wheelchair_accessible = list("enum", "opt", c(0, 1, 2)), - bikes_allowed = list("enum", "opt", c(0, 1, 2)) - ) - - stop_times <- list( - file_ext = "txt", - file_spec = "req", - trip_id = list("id", "req"), - arrival_time = list("time", "cond"), - departure_time = list("time", "cond"), - stop_id = list("id", "req"), - stop_sequence = list("integer", "req"), - stop_headsign = list("text", "opt"), - pickup_type = list("enum", "opt", c(0, 1, 2, 3)), - drop_off_type = list("enum", "opt", c(0, 1, 2, 3)), - continuous_pickup = list("enum", "opt", c(0, 1, 2, 3)), - continuous_drop_off = list("enum", "opt", c(0, 1, 2, 3)), - shape_dist_traveled = list("float", "opt"), - timepoint = list("enum", "opt", c(0, 1)) - ) - - calendar <- list( - file_ext = "txt", - file_spec = "cond", - service_id = list("id", "req"), - monday = list("enum", "req", c(0, 1)), - tuesday = list("enum", "req", c(0, 1)), - wednesday = list("enum", "req", c(0, 1)), - thursday = list("enum", "req", c(0, 1)), - friday = list("enum", "req", c(0, 1)), - saturday = list("enum", "req", c(0, 1)), - sunday = list("enum", "req", c(0, 1)), - start_date = list("date", "req", c(0, 1)), - end_date = list("date", "req", c(0, 1)) - ) - - calendar_dates <- list( - file_ext = "txt", - file_spec = "cond", - service_id = list("id", "req"), - date = list("date", "req"), - exception_type = list("enum", "req", c(1, 2)) - ) - - fare_attributes <- list( - file_ext = "txt", - file_spec = "opt", - fare_id = list("id", "req"), - price = list("float", "req"), - currency_type = list("currency_code", "req"), - payment_method = list("enum", "req", c(0, 1)), - transfers = list("enum", "req", c(0, 1, 2)), - agency_id = list("id", "cond"), - transfer_duration = list("integer", "opt") - ) - - fare_rules <- list( - file_ext = "txt", - file_spec = "opt", - fare_id = list("id", "req"), - route_id = list("id", "opt"), - origin_id = list("id", "opt"), - destination_id = list("id", "opt"), - contains_id = list("id", "opt") - ) - - fare_products <- list( - file_ext = "txt", - file_spec = "opt", - fare_product_id = list("id", "req"), - fare_product_name = list("text", "opt"), - amount = list("currency_amount", "req"), - currency = list("currency_code", "req") - ) - - fare_leg_rules <- list( - file_ext = "txt", - file_spec = "opt", - leg_group_id = list("id", "opt"), - network_id = list("id", "opt"), - from_area_id = list("id", "opt"), - to_area_id = list("id", "opt"), - fare_product_id = list("id", "req") - ) - - fare_transfer_rules <- list( - file_ext = "txt", - file_spec = "opt", - from_leg_group_id = list("id", "opt"), - to_leg_group_id = list("id", "opt"), - transfer_count = list("integer", "cond"), - duration_limit = list("integer", "opt"), - duration_limit_type = list("enum", "cond", c(0, 1, 2, 3)), - fare_transfer_type = list("enum", "req", c(0, 1, 2)), - fare_product_id = list("id", "opt") - ) - - areas <- list( - file_ext = "txt", - file_spec = "opt", - area_id = list("id", "req"), - area_name = list("text", "opt") - ) - - stop_areas <- list( - file_ext = "txt", - file_spec = "opt", - area_id = list("id", "req"), - stop_id = list("id", "opt") - ) - - shapes <- list( - file_ext = "txt", - file_spec = "opt", - shape_id = list("id", "req"), - shape_pt_lat = list("latitude", "req"), - shape_pt_lon = list("longitude", "req"), - shape_pt_sequence = list("integer", "req"), - shape_dist_traveled = list("float", "opt") - ) - - frequencies <- list( - file_ext = "txt", - file_spec = "opt", - trip_id = list("id", "req"), - start_time = list("time", "req"), - end_time = list("time", "req"), - headway_secs = list("integer", "req"), - exact_times = list("enum", "opt", c(0, 1)) - ) - - transfers <- list( - file_ext = "txt", - file_spec = "opt", - from_stop_id = list("id", "cond"), - to_stop_id = list("id", "cond"), - from_route_id = list("id", "opt"), - to_route_id = list("id", "opt"), - from_trip_id = list("id", "cond"), - to_trip_id = list("id", "cond"), - transfer_type = list("enum", "req", c(0, 1, 2, 3, 4, 5)), - min_transfer_time = list("integer", "opt") - ) - - pathways <- list( - file_ext = "txt", - file_spec = "opt", - pathway_id = list("id", "req"), - from_stop_id = list("id", "req"), - to_stop_id = list("id", "req"), - pathway_mode = list("enum", "req", c(1, 2, 3, 4, 5, 6, 7)), - is_bidirectional = list("enum", "req", c(0, 1)), - length = list("float", "opt"), - traversal_time = list("integer", "opt"), - stair_count = list("integer", "opt"), - max_slope = list("float", "opt"), - min_width = list("float", "opt"), - signposted_as = list("text", "opt"), - reversed_signposted_as = list("text", "opt") - ) - - levels <- list( - file_ext = "txt", - file_spec = "cond", - level_id = list("id", "req"), - level_index = list("float", "req"), - level_name = list("text", "opt") - ) - - translations <- list( - file_ext = "txt", - file_spec = "opt", - table_name = list("enum", "req", c("agency", "stops", "routes", - "trips", "stop_times", - "feed_info", "pathways", - "levels", "attribution")), - field_name = list("text", "req"), - language = list("language_code", "req"), - translation = list(c("text", "url", "email", "phone_number"), "req"), - record_id = list("id", "cond"), - record_sub_id = list("id", "cond"), - field_value = list(c("text", "url", "email", "phone_number"), "cond") - ) - - feed_info <- list( - file_ext = "txt", - file_spec = "cond", - feed_publisher_name = list("text", "req"), - feed_publisher_url = list("url", "req"), - feed_lang = list("language_code", "req"), - default_lang = list("language_code", "opt"), - feed_start_date = list("date", "opt"), - feed_end_date = list("date", "opt"), - feed_version = list("text", "opt"), - feed_contact_email = list("email", "opt"), - feed_contact_url = list("url", "opt") - ) - - attributions <- list( - file_ext = "txt", - file_spec = "opt", - attribution_id = list("id", "opt"), - agency_id = list("id", "opt"), - route_id = list("id", "opt"), - trip_id = list("id", "opt"), - organization_name = list("text", "req"), - is_producer = list("enum", "opt", c(0, 1)), - is_operator = list("enum", "opt", c(0, 1)), - is_authority = list("enum", "opt", c(0, 1)), - attribution_url = list("url", "opt"), - attribution_email = list("email", "opt"), - attribution_phone = list("phone_number", "opt") - ) - - locations <- list( - file_ext = "geojson", - file_spec = "opt" - ) - - # create gtfs_standards object - - gtfs_standards <- list( - agency = agency, - stops = stops, - routes = routes, - trips = trips, - stop_times = stop_times, - calendar = calendar, - calendar_dates = calendar_dates, - fare_attributes = fare_attributes, - fare_rules = fare_rules, - fare_products = fare_products, - fare_leg_rules = fare_leg_rules, - fare_transfer_rules = fare_transfer_rules, - areas = areas, - stop_areas = stop_areas, - shapes = shapes, - frequencies = frequencies, - transfers = transfers, - pathways = pathways, - levels = levels, - translations = translations, - feed_info = feed_info, - attributions = attributions, - locations = locations - ) - - # define R types most similar to GTFS reference types - - r_equivalents <- c( - color = "character", - currency_amount = "numeric", - currency_code = "character", - date = "integer", - email = "character", - enum = "integer", - id = "character", - integer = "integer", - language_code = "character", - latitude = "numeric", - longitude = "numeric", - float = "numeric", - phone_number = "character", - text = "character", - time = "character", - timezone = "character", - url = "character" - ) - - # translate GTFS reference types to R types - - gtfs_standards <- lapply(gtfs_standards, translate_types, r_equivalents) - - # correct a small special case: - # 'translations' 'table_name' field is an ENUM, but its allowed values are - # strings, not integers. this results in a warning when importing the GTFS - # using data.table::fread(). so change R equivalent to character, instead of - # integer - - gtfs_standards$translations$table_name[[1]] <- "character" - - return(gtfs_standards) + files_list <- split(gtfsio_field_types, gtfsio_field_types$file) + files_list <- lapply(files_list, function(feed_file) { + type = feed_file$gtfsio_type + names(type) <- feed_file$field_name + type + }) + return(files_list) } -#' Translate GTFS specification types to R equivalent types -#' -#' @param text_file A named `list` containing a GTFS text file specification, as -#' described in the body of [get_gtfs_standards]. -#' @param r_equivalents A named `character vector`, in which each name is the -#' GTFS specification type and the content its R equivalent. -#' -#' @return A named `list` holding a GTFS text file specification, but with -#' R data types instead of GTFS spec data types. -#' -#' @keywords internal -translate_types <- function(text_file, r_equivalents) { - - # for each text_file element: - # - check if it's a list. - # - if it's not, then it's the 'file_spec' element. return 'file_spec' value - # - if it is, replace first entry (GTFS spec type) for an equivalent R type - - text_file <- lapply( - text_file, - function(i) { - if (!is.list(i)) return(i) - - new_spec <- i - gtfs_type <- new_spec[[1]] - r_type <- r_equivalents[gtfs_type] - - # some 'translations' fields ('translation' and 'field_value') might have - # more than one GTFS data type. - # their R equivalent is always 'character', no matter the GTFS type, so we - # can safely get only the first value ('character') +get_field_types = function(file) { + if(endsWith(file, ".geojson") || endsWith(file, ".txt")) stop() + types = gtfsio_field_types$gtfsio_type[gtfsio_field_types$file == file] + names(types) <- gtfsio_field_types$field_name[gtfsio_field_types$file == file] + stopifnot(!is.null(names(types))) + return(types) +} - if (length(r_type) > 1) r_type <- r_type[1] +.doc_field_types = function() { # nocov start + type_assignment <- unique(gtfsio_field_types[,c("Type", "gtfsio_type")]) + type_assignment <- type_assignment[!startsWith(type_assignment$Type, "Foreign ID"),] + type_assignment <- type_assignment[order(type_assignment$gtfsio_type),] - new_spec[[1]] <- r_type + doc <- c("\\itemize{", + paste0("\\item{", type_assignment$Type, " = \`", + type_assignment$gtfsio_type, "\`}"), + "}\n") - return(new_spec) - } - ) -} + return(paste(doc, collapse = "\n")) +} # nocov end diff --git a/R/import_gtfs.R b/R/import_gtfs.R index 72d9fc8..936fb26 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -393,10 +393,9 @@ remove_file_ext = function(file) { } append_file_ext = function(file) { - gtfs_standards <- get_gtfs_standards() vapply(file, function(f) { - file_ext <- gtfs_standards[[f]][["file_ext"]] - if (is.null(file_ext)) { + file_ext <- gtfs_file_data$file_ext[gtfs_file_data$file == f] + if (length(file_ext) == 0) { # use default for argument-specified non-standard files, behaviour defined in test_import_gtfs.R#292 file_ext <- "txt" } diff --git a/R/sysdata.rda b/R/sysdata.rda new file mode 100644 index 0000000000000000000000000000000000000000..3f2e059bdccaa22d86b14955ac1a094aa5f944d0 GIT binary patch literal 2824 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¬)Y!5bwvYuUL_0*PN`<%v6g9-iZ#BRt zaAUO8!)=NnVZcIST+B>gH4PSaSg68~UO~w5a~39zZhcxBhmxtIc_Gw1NIvr--x1-` zJvufVM!p4favOZ1v`hT!IyVF}IBU7~QAU$1JUd;ViO44~9Odwr$wMl?fCeup8C5fz z=ma?8NJtzAZ?y%BQWmL9sv3*B7LB0tof8r5?J~m%+SFcQMvBn!;@t}g+Ji-r0_vbx z0|rQv0SHY~)b-AL?zjg+>(D2r9R|eSbaP!(kBl>Qzz#a4o2sa^R2|3-j1+=FBnp8D zNayP7qr!0lCH48Wsn_HZY2XKa%)JUf0n3Hthca#8r#frv=R-a<9Qi{#bj*Y`e#BS{ ziL>aENGED@^&Vgify3=osDir@9^WKHbZ`m}T2itkgQYY&kl*q%@ZzF-(11+h2PRNn zRKSBJwkeB|oEEw~{uw(2H1QCkF-9vOhMG`-;l^n!C838h1F=kpNSX+SB;tf-7QPz6 aYRklY)AbH@*+?XR#oUoj6eK5LO02-Yy7oN) literal 0 HcmV?d00001 diff --git a/inst/reference/create_gtfs_standards.R b/inst/reference/create_gtfs_standards.R new file mode 100644 index 0000000..980e1c5 --- /dev/null +++ b/inst/reference/create_gtfs_standards.R @@ -0,0 +1,69 @@ +#' Parse the official GTFS reference markdown file and create a table +#' that assigns every GTFS field with an R-equivalent gtfsio datatype + +library(dplyr) +source("parse_markdown.R") +reference.md = curl::curl_download("https://raw.githubusercontent.com/google/transit/master/gtfs/spec/en/reference.md", tempfile()) + +# Parse current reference markdown to list of tables and bind it #### +f <- bind_fields_reference_list(parse_fields(reference.md)) + +# Link gtfs types to R types #### +f$gtfsio_type <- NA + +# Enum +f$gtfsio_type[f$Type == "Enum"] <- "integer" +# Correct non-integer enums (manual fix) +f[f$File_Name == "translations.txt" & f$Field_Name == "table_name","gtfsio_type"] <- "character" + +# ID: character +f$gtfsio_type[startsWith(f$Type, "Foreign ID")] <- "character" +f$gtfsio_type[startsWith(f$Type, "ID referencing")] <- "character" +f$gtfsio_type[f$Type %in% c("ID", "Foreign ID", "Unique ID")] <- "character" + +# Text/Strings +f$gtfsio_type[f$Type %in% c("Text", "String")] <- "character" +f$gtfsio_type[f$Type %in% c("URL", "Language code", "Currency code", "Email", + "Phone number", "Timezone", "Color", + "Text or URL or Email or Phone number")] <- "character" + +# Date and Time +f$gtfsio_type[f$Type == "Date"] <- "integer" +f$gtfsio_type[f$Type == "Time"] <- "character" + +# Numerics +f$gtfsio_type[f$Type %in% c("Latitude", "Longitude", "Non-negative float", + "Positive float", "Float", "Currency amount")] <- "numeric" +f$gtfsio_type[f$Type %in% c("Non-negative integer", "Non-zero integer", + "Positive integer", "Non-null integer", "Integer")] <- "integer" + +# Geojson +f$gtfsio_type[f$Type == "Array"] <- "geojson_array" +f$gtfsio_type[f$Type == "Object"] <- "geojson_object" + +f$Field_Name[f$File_Name == "locations.geojson"] <- gsub(" ", "", f$Field_Name[f$File_Name == "locations.geojson"]) +f$Field_Name[f$File_Name == "locations.geojson"] <- gsub("\\\\", "", f$Field_Name[f$File_Name == "locations.geojson"]) +f$Field_Name[f$File_Name == "locations.geojson"] <- gsub("-", "", f$Field_Name[f$File_Name == "locations.geojson"]) + +if(any(is.na(f$gtfsio_type))) { + stop("GTFS types without R equivalent found:\n", paste0(unique(f$Type[is.na(f$gtfsio_type)]), collapse = ", ")) +} + +# Rename columns, add file column without location #### +gtfsio_field_types = f |> + rename(file = File_Name, field_name = Field_Name) |> + mutate(file = gsub("\\.txt$", "", gsub("\\.geojson$", "", file))) |> + as.data.frame() + +gtfs_file_data = cleanup_files_reference(parse_files(reference.md)) +gtfs_file_data <- gtfs_file_data |> + tidyr::separate(File_Name, c("file", "file_ext"), sep = "\\.", remove = F) |> + select(File_Name, File_Presence, file, file_ext) |> + as.data.frame() + +# save for possible external use +write.csv(gtfsio_field_types, "gtfsio_field_conversion_types.csv", row.names = FALSE, eol = "\r", fileEncoding = "UTF-8") + +# save as internal data +usethis::use_data(gtfsio_field_types, gtfs_file_data, internal = T, overwrite = T) + diff --git a/inst/reference/gtfsio_field_conversion_types.csv b/inst/reference/gtfsio_field_conversion_types.csv new file mode 100644 index 0000000..9f0bd14 --- /dev/null +++ b/inst/reference/gtfsio_field_conversion_types.csv @@ -0,0 +1 @@ +"file","field_name","Type","Presence","gtfsio_type" "agency","agency_id","Unique ID","Conditionally Required","character" "agency","agency_name","Text","Required","character" "agency","agency_url","URL","Required","character" "agency","agency_timezone","Timezone","Required","character" "agency","agency_lang","Language code","Optional","character" "agency","agency_phone","Phone number","Optional","character" "agency","agency_fare_url","URL","Optional","character" "agency","agency_email","Email","Optional","character" "stops","stop_id","Unique ID","Required","character" "stops","stop_code","Text","Optional","character" "stops","stop_name","Text","Conditionally Required","character" "stops","tts_stop_name","Text","Optional","character" "stops","stop_desc","Text","Optional","character" "stops","stop_lat","Latitude","Conditionally Required","numeric" "stops","stop_lon","Longitude","Conditionally Required","numeric" "stops","zone_id","ID","Optional","character" "stops","stop_url","URL","Optional","character" "stops","location_type","Enum","Optional","integer" "stops","parent_station","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "stops","stop_timezone","Timezone","Optional","character" "stops","wheelchair_boarding","Enum","Optional","integer" "stops","level_id","Foreign ID referencing `levels.level_id`","Optional","character" "stops","platform_code","Text","Optional","character" "routes","route_id","Unique ID","Required","character" "routes","agency_id","Foreign ID referencing `agency.agency_id`","Conditionally Required","character" "routes","route_short_name","Text","Conditionally Required","character" "routes","route_long_name","Text","Conditionally Required","character" "routes","route_desc","Text","Optional","character" "routes","route_type","Enum","Required","integer" "routes","route_url","URL","Optional","character" "routes","route_color","Color","Optional","character" "routes","route_text_color","Color","Optional","character" "routes","route_sort_order","Non-negative integer","Optional","integer" "routes","continuous_pickup","Enum","Conditionally Forbidden","integer" "routes","continuous_drop_off","Enum","Conditionally Forbidden","integer" "routes","network_id","ID","Conditionally Forbidden","character" "trips","route_id","Foreign ID referencing `routes.route_id`","Required","character" "trips","service_id","Foreign ID referencing `calendar.service_id` or `calendar_dates.service_id`","Required","character" "trips","trip_id","Unique ID","Required","character" "trips","trip_headsign","Text","Optional","character" "trips","trip_short_name","Text","Optional","character" "trips","direction_id","Enum","Optional","integer" "trips","block_id","ID","Optional","character" "trips","shape_id","Foreign ID referencing `shapes.shape_id`","Conditionally Required","character" "trips","wheelchair_accessible","Enum","Optional","integer" "trips","bikes_allowed","Enum","Optional","integer" "stop_times","trip_id","Foreign ID referencing `trips.trip_id`","Required","character" "stop_times","arrival_time","Time","Conditionally Required","character" "stop_times","departure_time","Time","Conditionally Required","character" "stop_times","stop_id","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "stop_times","location_group_id","Foreign ID referencing `location_groups.location_group_id`","Conditionally Forbidden","character" "stop_times","location_id","Foreign ID referencing `id` from `locations.geojson`","Conditionally Forbidden","character" "stop_times","stop_sequence","Non-negative integer","Required","integer" "stop_times","stop_headsign","Text","Optional","character" "stop_times","start_pickup_drop_off_window","Time","Conditionally Required","character" "stop_times","end_pickup_drop_off_window","Time","Conditionally Required","character" "stop_times","pickup_type","Enum","Conditionally Forbidden","integer" "stop_times","drop_off_type","Enum","Conditionally Forbidden","integer" "stop_times","continuous_pickup","Enum","Conditionally Forbidden","integer" "stop_times","continuous_drop_off","Enum","Conditionally Forbidden","integer" "stop_times","shape_dist_traveled","Non-negative float","Optional","numeric" "stop_times","timepoint","Enum","Optional","integer" "stop_times","pickup_booking_rule_id","Foreign ID referencing `booking_rules.booking_rule_id`","Optional","character" "stop_times","drop_off_booking_rule_id","Foreign ID referencing `booking_rules.booking_rule_id`","Optional","character" "calendar","service_id","Unique ID","Required","character" "calendar","monday","Enum","Required","integer" "calendar","tuesday","Enum","Required","integer" "calendar","wednesday","Enum","Required","integer" "calendar","thursday","Enum","Required","integer" "calendar","friday","Enum","Required","integer" "calendar","saturday","Enum","Required","integer" "calendar","sunday","Enum","Required","integer" "calendar","start_date","Date","Required","integer" "calendar","end_date","Date","Required","integer" "calendar_dates","service_id","Foreign ID referencing `calendar.service_id` or ID","Required","character" "calendar_dates","date","Date","Required","integer" "calendar_dates","exception_type","Enum","Required","integer" "fare_attributes","fare_id","Unique ID","Required","character" "fare_attributes","price","Non-negative float","Required","numeric" "fare_attributes","currency_type","Currency code","Required","character" "fare_attributes","payment_method","Enum","Required","integer" "fare_attributes","transfers","Enum","Required","integer" "fare_attributes","agency_id","Foreign ID referencing `agency.agency_id`","Conditionally Required","character" "fare_attributes","transfer_duration","Non-negative integer","Optional","integer" "fare_rules","fare_id","Foreign ID referencing `fare_attributes.fare_id`","Required","character" "fare_rules","route_id","Foreign ID referencing `routes.route_id`","Optional","character" "fare_rules","origin_id","Foreign ID referencing `stops.zone_id`","Optional","character" "fare_rules","destination_id","Foreign ID referencing `stops.zone_id`","Optional","character" "fare_rules","contains_id","Foreign ID referencing `stops.zone_id`","Optional","character" "timeframes","timeframe_group_id","ID","Required","character" "timeframes","start_time","Time","Conditionally Required","character" "timeframes","end_time","Time","Conditionally Required","character" "timeframes","service_id","Foreign ID referencing `calendar.service_id` or `calendar_dates.service_id`","Required","character" "fare_media","fare_media_id","Unique ID","Required","character" "fare_media","fare_media_name","Text","Optional","character" "fare_media","fare_media_type","Enum","Required","integer" "fare_products","fare_product_id","ID","Required","character" "fare_products","fare_product_name","Text","Optional","character" "fare_products","fare_media_id","Foreign ID referencing `fare_media.fare_media_id`","Optional","character" "fare_products","amount","Currency amount","Required","numeric" "fare_products","currency","Currency code","Required","character" "fare_leg_rules","leg_group_id","ID","Optional","character" "fare_leg_rules","network_id","Foreign ID referencing `routes.network_id` or `networks.network_id`","Optional","character" "fare_leg_rules","from_area_id","Foreign ID referencing `areas.area_id`","Optional","character" "fare_leg_rules","to_area_id","Foreign ID referencing `areas.area_id`","Optional","character" "fare_leg_rules","from_timeframe_group_id","Foreign ID referencing `timeframes.timeframe_group_id`","Optional","character" "fare_leg_rules","to_timeframe_group_id","Foreign ID referencing `timeframes.timeframe_group_id`","Optional","character" "fare_leg_rules","fare_product_id","Foreign ID referencing `fare_products.fare_product_id`","Required","character" "fare_leg_rules","rule_priority","Non-negative integer","Optional","integer" "fare_transfer_rules","from_leg_group_id","Foreign ID referencing `fare_leg_rules.leg_group_id`","Optional","character" "fare_transfer_rules","to_leg_group_id","Foreign ID referencing `fare_leg_rules.leg_group_id`","Optional","character" "fare_transfer_rules","transfer_count","Non-zero integer","Conditionally Forbidden","integer" "fare_transfer_rules","duration_limit","Positive integer","Optional","integer" "fare_transfer_rules","duration_limit_type","Enum","Conditionally Required","integer" "fare_transfer_rules","fare_transfer_type","Enum","Required","integer" "fare_transfer_rules","fare_product_id","Foreign ID referencing `fare_products.fare_product_id`","Optional","character" "areas","area_id","Unique ID","Required","character" "areas","area_name","Text","Optional","character" "stop_areas","area_id","Foreign ID referencing `areas.area_id`","Required","character" "stop_areas","stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "networks","network_id","Unique ID","Required","character" "networks","network_name","Text","Optional","character" "route_networks","network_id","Foreign ID referencing `networks.network_id`","Required","character" "route_networks","route_id","Foreign ID referencing `routes.route_id`","Required","character" "shapes","shape_id","ID","Required","character" "shapes","shape_pt_lat","Latitude","Required","numeric" "shapes","shape_pt_lon","Longitude","Required","numeric" "shapes","shape_pt_sequence","Non-negative integer","Required","integer" "shapes","shape_dist_traveled","Non-negative float","Optional","numeric" "frequencies","trip_id","Foreign ID referencing `trips.trip_id`","Required","character" "frequencies","start_time","Time","Required","character" "frequencies","end_time","Time","Required","character" "frequencies","headway_secs","Positive integer","Required","integer" "frequencies","exact_times","Enum","Optional","integer" "transfers","from_stop_id","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "transfers","to_stop_id","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "transfers","from_route_id","Foreign ID referencing `routes.route_id`","Optional","character" "transfers","to_route_id","Foreign ID referencing `routes.route_id`","Optional","character" "transfers","from_trip_id","Foreign ID referencing `trips.trip_id`","Conditionally Required","character" "transfers","to_trip_id","Foreign ID referencing `trips.trip_id`","Conditionally Required","character" "transfers","transfer_type","Enum","Required","integer" "transfers","min_transfer_time","Non-negative integer","Optional","integer" "pathways","pathway_id","Unique ID","Required","character" "pathways","from_stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "pathways","to_stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "pathways","pathway_mode","Enum","Required","integer" "pathways","is_bidirectional","Enum","Required","integer" "pathways","length","Non-negative float","Optional","numeric" "pathways","traversal_time","Positive integer","Optional","integer" "pathways","stair_count","Non-null integer","Optional","integer" "pathways","max_slope","Float","Optional","numeric" "pathways","min_width","Positive float","Optional","numeric" "pathways","signposted_as","Text","Optional","character" "pathways","reversed_signposted_as","Text","Optional","character" "levels","level_id","Unique ID","Required","character" "levels","level_index","Float","Required","numeric" "levels","level_name","Text","Optional","character" "location_groups","location_group_id","Unique ID","Required","character" "location_groups","location_group_name","Text","Optional","character" "location_group_stops","location_group_id","Foreign ID referencing `location_groups.location_group_id`","Required","character" "location_group_stops","stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "locations","type","String","Required","character" "locations","features","Array","Required","geojson_array" "locations","type","String","Required","character" "locations","id","String","Required","character" "locations","properties","Object","Required","geojson_object" "locations","stop_name","String","Optional","character" "locations","stop_desc","String","Optional","character" "locations","geometry","Object","Required","geojson_object" "locations","type","String","Required","character" "locations","coordinates","Array","Required","geojson_array" "booking_rules","booking_rule_id","Unique ID","Required","character" "booking_rules","booking_type","Enum","Required","integer" "booking_rules","prior_notice_duration_min","Integer","Conditionally Required","integer" "booking_rules","prior_notice_duration_max","Integer","Conditionally Forbidden","integer" "booking_rules","prior_notice_last_day","Integer","Conditionally Required","integer" "booking_rules","prior_notice_last_time","Time","Conditionally Required","character" "booking_rules","prior_notice_start_day","Integer","Conditionally Forbidden","integer" "booking_rules","prior_notice_start_time","Time","Conditionally Required","character" "booking_rules","prior_notice_service_id","Foreign ID referencing `calendar.service_id`","Conditionally Forbidden","character" "booking_rules","message","Text","Optional","character" "booking_rules","pickup_message","Text","Optional","character" "booking_rules","drop_off_message","Text","Optional","character" "booking_rules","phone_number","Phone number","Optional","character" "booking_rules","info_url","URL","Optional","character" "booking_rules","booking_url","URL","Optional","character" "translations","table_name","Enum","Required","character" "translations","field_name","Text","Required","character" "translations","language","Language code","Required","character" "translations","translation","Text or URL or Email or Phone number","Required","character" "translations","record_id","Foreign ID","Conditionally Required","character" "translations","record_sub_id","Foreign ID","Conditionally Required","character" "translations","field_value","Text or URL or Email or Phone number","Conditionally Required","character" "feed_info","feed_publisher_name","Text","Required","character" "feed_info","feed_publisher_url","URL","Required","character" "feed_info","feed_lang","Language code","Required","character" "feed_info","default_lang","Language code","Optional","character" "feed_info","feed_start_date","Date","Recommended","integer" "feed_info","feed_end_date","Date","Recommended","integer" "feed_info","feed_version","Text","Recommended","character" "feed_info","feed_contact_email","Email","Optional","character" "feed_info","feed_contact_url","URL","Optional","character" "attributions","attribution_id","Unique ID","Optional","character" "attributions","agency_id","Foreign ID referencing `agency.agency_id`","Optional","character" "attributions","route_id","Foreign ID referencing `routes.route_id`","Optional","character" "attributions","trip_id","Foreign ID referencing `trips.trip_id`","Optional","character" "attributions","organization_name","Text","Required","character" "attributions","is_producer","Enum","Optional","integer" "attributions","is_operator","Enum","Optional","integer" "attributions","is_authority","Enum","Optional","integer" "attributions","attribution_url","URL","Optional","character" "attributions","attribution_email","Email","Optional","character" "attributions","attribution_phone","Phone number","Optional","character" \ No newline at end of file diff --git a/inst/reference/parse_markdown.R b/inst/reference/parse_markdown.R new file mode 100644 index 0000000..cab2096 --- /dev/null +++ b/inst/reference/parse_markdown.R @@ -0,0 +1,100 @@ +# https://stackoverflow.com/questions/48087762/markdown-table-to-data-frame-in-r +read_markdown_table = function(lines) { + lines <- lines[!grepl('^[[:blank:]+-=:_|]*$', lines)] + lines <- gsub('(^\\s*?\\|)|(\\|\\s*?$)', '', lines) + readr::read_delim(paste(lines, collapse = '\n'), delim = '|', + trim_ws = TRUE, show_col_types = FALSE) +} + +# parse all field definitions and return a list of tables +parse_fields = function(reference.md) { + field_reference_list = list() + + ref_lines = readr::read_lines(reference.md) + ref_lines[length(ref_lines)+1] <- "" # ensure empty last row + + table_index = stringr::str_starts(ref_lines, "\\| ") # lines with table markdown + + i = which(ref_lines == "## Field Definitions") + while(i <= length(ref_lines)) { + .line = ref_lines[i] + if(stringr::str_starts(.line, "### ")) { + .current_file <- stringr::str_replace_all(.line, "### ", "") + } + if(stringr::str_starts(.line, "File: ")) { + .file_presence <- stringr::str_replace_all(.line, "File: ", "") + } + if(stringr::str_starts(.line, "Primary key ")) { + .primary_key <- stringr::str_replace_all(.line, "Primary key \\(", "") + .primary_key <- stringr::str_replace_all(.primary_key, "\\)", "") + } + + # parse fields table + if(stringr::str_starts(.line, "\\|[ ]+Field Name \\| Type")) { + j = min(which(!table_index & seq_along(ref_lines) > i))-1 + ref_table = read_markdown_table(ref_lines[i:j]) + + stopifnot(!is.null(.current_file), !is.null(.file_presence)) + if(is.null(.primary_key)) stopifnot(stringr::str_ends(.current_file, "geojson")) + + # print problems if available + if(nrow(readr::problems(ref_table)) > 0) { + cat(.current_file, "\n") + print(readr::problems(ref_table)[,1:4]) + } + + # cleanup attributes + attributes(ref_table)$presence <- .file_presence + attributes(ref_table)$primary_key <- .primary_key + attributes(ref_table)$spec <- NULL # remove col_type info + attributes(ref_table)$problems <- NULL # remove col_type info + + # assign to return list + field_reference_list[[.current_file]] <- ref_table + + # clear values + .current_file <- .file_presence <- .primary_key <- NULL + } + i <- i+1 + } + return(field_reference_list) +} + +bind_fields_reference_list = function(field_reference_list) { + field_reference_list |> + bind_rows(.id = "File_Name") |> + rename(Field_Name = `Field Name`) |> + mutate(Field_Name = gsub("`", "", Field_Name)) |> + mutate(Presence = gsub("**", "", Presence, fixed = TRUE)) |> + select(-Description) +} + +parse_files = function(reference.md) { + ref_lines = readr::read_lines(reference.md) + + i = which(ref_lines == "## Dataset Files") + j = which(ref_lines == "## File Requirements") + stopifnot(i < j) + ref_lines <- ref_lines[i:j] + + index = stringr::str_starts(ref_lines, "\\| ") + stopifnot(sum(diff(index)) == 0) + + files_table = read_markdown_table(ref_lines[index]) + + files_table$`File Name` <- files_table$`File Name` |> + strsplit(, split = "](", fixed = T) |> + lapply(\(x) { + gsub("[", "", x[1], fixed = T) + }) |> unlist() + + return(files_table) +} + +# cleanup +cleanup_files_reference = function(files_table) { + files_table |> + rename(File_Name = `File Name`) |> + mutate(Presence = gsub("**", "", Presence, fixed = TRUE)) |> + select(File_Name, File_Presence = Presence) +} diff --git a/inst/tinytest/test_export_gtfs.R b/inst/tinytest/test_export_gtfs.R index 100b0ae..73a76b3 100644 --- a/inst/tinytest/test_export_gtfs.R +++ b/inst/tinytest/test_export_gtfs.R @@ -186,25 +186,25 @@ expect_true( ) ) -for (file in list.files(tmpd)) { +for (filenames in list.files(tmpd)) { # all existing fields should be standard - no_txt_file <- sub(".txt", "", file) - std_fields <- setdiff(names(gtfs_standards[[no_txt_file]]), "file_spec") - existing_fields <- readLines(file.path(tmpd, file), n = 1L) + file <- gtfsio:::remove_file_ext(filenames) + std_fields <- setdiff(names(gtfs_standards[[file]]), "file_spec") + existing_fields <- readLines(file.path(tmpd, filenames), n = 1L) existing_fields <- strsplit(existing_fields, ",")[[1]] - expect_true(all(existing_fields %in% std_fields), info = no_txt_file) + expect_true(all(existing_fields %in% std_fields), info = file) # all standard fields in the object should be written - std_fields_in_obj <- names(gtfs[[no_txt_file]]) + std_fields_in_obj <- names(gtfs[[file]]) std_fields_in_obj <- std_fields_in_obj[std_fields_in_obj %in% std_fields] expect_true( all(std_fields_in_obj %in% existing_fields), - info = no_txt_file + info = file ) } @@ -301,6 +301,7 @@ expect_identical(resulting_shapes_content[3], "b,2,41,41,10000000") locations_feed <- import_gtfs(system.file("extdata/locations_feed.zip", package = "gtfsio")) tmpfile <- tempfile(fileext = ".zip") export_gtfs(locations_feed, tmpfile) + reimported <- import_gtfs(tmpfile) expect_equal(reimported, locations_feed) diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index a314bc4..3cc9a5f 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -154,6 +154,7 @@ standard_types <- lapply( types <- types[order(names(types))] } ) +names(standard_types) <- gtfsio:::remove_file_ext(names(standard_types)) standard_types <- standard_types[order(names(standard_types))] # get the type actually used to read each field diff --git a/man/get_gtfs_standards.Rd b/man/get_gtfs_standards.Rd index 7dcd6c1..98156b9 100644 --- a/man/get_gtfs_standards.Rd +++ b/man/get_gtfs_standards.Rd @@ -15,38 +15,47 @@ Generates a list specifying the standards to be used when reading and writing GTFS feeds with R. Each list element (also a list) represents a distinct GTFS table, and describes: \itemize{ -\item whether the table is required, optional or conditionally required; \item the fields that compose the table, including which R data type is best suited to represent it, whether the field is required, optional or -conditionally required, and which values it can assume (most relevant to GTFS -\code{ENUM}s. +conditionally required. +\item whether the table is required, optional or conditionally required (as an +attribute) } - -Note: the standards list is based on the specification as revised in May 9th, -2022. } \section{Details}{ GTFS standards were derived from \href{https://gtfs.org/schedule/reference/}{GTFS Schedule Reference}. The R data types chosen to represent each GTFS data type are described below: \itemize{ -\item Color = \code{character} -\item Currency amount = \code{numeric} -\item Currency code = \code{character} -\item Date = \code{integer} -\item Email = \code{character} -\item ENUM = \code{integer} -\item ID = \code{character} -\item Integer = \code{integer} -\item Language code = \code{character} -\item Latitude = \code{numeric} -\item Longitude = \code{numeric} -\item Float = \code{numeric} -\item Phone number = \code{character} -\item Text = \code{character} -\item Time = \code{character} -\item Timezone = \code{character} -\item URL = \code{character} +\item{Unique ID = \code{character}} +\item{Text = \code{character}} +\item{URL = \code{character}} +\item{Timezone = \code{character}} +\item{Language code = \code{character}} +\item{Phone number = \code{character}} +\item{Email = \code{character}} +\item{ID = \code{character}} +\item{Color = \code{character}} +\item{Time = \code{character}} +\item{Currency code = \code{character}} +\item{String = \code{character}} +\item{Enum = \code{character}} +\item{Text or URL or Email or Phone number = \code{character}} +\item{Array = \code{geojson_array}} +\item{Object = \code{geojson_object}} +\item{Enum = \code{integer}} +\item{Non-negative integer = \code{integer}} +\item{Date = \code{integer}} +\item{Non-zero integer = \code{integer}} +\item{Positive integer = \code{integer}} +\item{Non-null integer = \code{integer}} +\item{Integer = \code{integer}} +\item{Latitude = \code{numeric}} +\item{Longitude = \code{numeric}} +\item{Non-negative float = \code{numeric}} +\item{Currency amount = \code{numeric}} +\item{Float = \code{numeric}} +\item{Positive float = \code{numeric}} } } diff --git a/man/translate_types.Rd b/man/translate_types.Rd deleted file mode 100644 index ca8d06c..0000000 --- a/man/translate_types.Rd +++ /dev/null @@ -1,23 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/get_gtfs_standards.R -\name{translate_types} -\alias{translate_types} -\title{Translate GTFS specification types to R equivalent types} -\usage{ -translate_types(text_file, r_equivalents) -} -\arguments{ -\item{text_file}{A named \code{list} containing a GTFS text file specification, as -described in the body of \link{get_gtfs_standards}.} - -\item{r_equivalents}{A named \verb{character vector}, in which each name is the -GTFS specification type and the content its R equivalent.} -} -\value{ -A named \code{list} holding a GTFS text file specification, but with -R data types instead of GTFS spec data types. -} -\description{ -Translate GTFS specification types to R equivalent types -} -\keyword{internal} From 2466cf0b8190a9b08c20011991c47214976e82a6 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Tue, 27 Aug 2024 14:40:49 +0200 Subject: [PATCH 06/12] use internal gtfs_reference object instead of gtfs_standards --- R/export_gtfs.R | 14 ++++------ R/get_gtfs_standards.R | 21 ++++---------- R/import_gtfs.R | 24 ++++++---------- R/sysdata.rda | Bin 2824 -> 4078 bytes inst/reference/create_gtfs_standards.R | 26 ++++++++++++------ .../gtfsio_field_conversion_types.csv | 1 - inst/tinytest/test_export_gtfs.R | 2 +- inst/tinytest/test_import_gtfs.R | 11 +------- man/get_gtfs_standards.Rd | 12 ++++---- man/read_files.Rd | 5 +--- 10 files changed, 45 insertions(+), 71 deletions(-) delete mode 100644 inst/reference/gtfsio_field_conversion_types.csv diff --git a/R/export_gtfs.R b/R/export_gtfs.R index f51e2e1..7083e3f 100644 --- a/R/export_gtfs.R +++ b/R/export_gtfs.R @@ -52,8 +52,6 @@ export_gtfs <- function(gtfs, overwrite = TRUE, quiet = TRUE) { - gtfs_standards <- get_gtfs_standards() - # basic input checking assert_class(gtfs, "gtfs") @@ -73,7 +71,7 @@ export_gtfs <- function(gtfs, if (!as_dir & !grepl("\\.zip$", path)) error_ext_must_be_zip() if (as_dir & grepl("\\.zip$", path)) error_path_must_be_dir() - extra_files <- setdiff(files, names(gtfs_standards)) + extra_files <- setdiff(files, names(gtfs_reference)) if (standard_only & !is.null(files) & !identical(extra_files, character(0))) { error_non_standard_files(extra_files) } @@ -91,7 +89,7 @@ export_gtfs <- function(gtfs, # 'extra_files' is re-evaluated because 'files' might have changed in the # lines above - extra_files <- setdiff(files, names(gtfs_standards)) + extra_files <- setdiff(files, names(gtfs_reference)) if (standard_only) files <- setdiff(files, extra_files) @@ -119,8 +117,8 @@ export_gtfs <- function(gtfs, if (!quiet) message("Writing text files to ", tmpd) - filenames = append_file_ext(files) - filepaths <- file.path(tmpd, filename) + filenames <- append_file_ext(files) + filepaths <- file.path(tmpd, filenames) for (i in seq_along(files)) { @@ -130,7 +128,7 @@ export_gtfs <- function(gtfs, if (!quiet) message(" - Writing ", filename) - dt <- gtfs[[remove_file_ext(filename)]] + dt <- gtfs[[file]] if(endsWith(filename, ".geojson")) { jsonlite::write_json(dt, filepath, pretty = FALSE, auto_unbox = TRUE, digits = 8) @@ -142,7 +140,7 @@ export_gtfs <- function(gtfs, if (standard_only) { file_cols <- names(dt) - extra_cols <- setdiff(file_cols, names(gtfs_standards[[file]])) + extra_cols <- setdiff(file_cols, names(gtfs_reference[[file]][["field_types"]])) if (!identical(extra_cols, character(0))) dt <- dt[, !..extra_cols] diff --git a/R/get_gtfs_standards.R b/R/get_gtfs_standards.R index 5ab67d1..b2682f1 100644 --- a/R/get_gtfs_standards.R +++ b/R/get_gtfs_standards.R @@ -25,25 +25,14 @@ #' #' @export get_gtfs_standards <- function() { - files_list <- split(gtfsio_field_types, gtfsio_field_types$file) - files_list <- lapply(files_list, function(feed_file) { - type = feed_file$gtfsio_type - names(type) <- feed_file$field_name - type - }) - return(files_list) -} - -get_field_types = function(file) { - if(endsWith(file, ".geojson") || endsWith(file, ".txt")) stop() - types = gtfsio_field_types$gtfsio_type[gtfsio_field_types$file == file] - names(types) <- gtfsio_field_types$field_name[gtfsio_field_types$file == file] - stopifnot(!is.null(names(types))) - return(types) + return(NULL) } .doc_field_types = function() { # nocov start - type_assignment <- unique(gtfsio_field_types[,c("Type", "gtfsio_type")]) + fields <- lapply(gtfs_reference, `[[`, "fields") + fields <- do.call("rbind", fields) + + type_assignment <- unique(fields[,c("Type", "gtfsio_type")]) type_assignment <- type_assignment[!startsWith(type_assignment$Type, "Foreign ID"),] type_assignment <- type_assignment[order(type_assignment$gtfsio_type),] diff --git a/R/import_gtfs.R b/R/import_gtfs.R index 936fb26..4ec4109 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -176,16 +176,11 @@ import_gtfs <- function(path, paste0(" * ", filenames_to_read, collapse = "\n") ) - # get GTFS standards to assign correct classes to each field - - gtfs_standards <- get_gtfs_standards() - # read files into list gtfs <- lapply( X = filenames_to_read, FUN = read_files, - gtfs_standards, fields, extra_spec, tmpdir, @@ -221,8 +216,6 @@ import_gtfs <- function(path, #' #' @param file A string. The name of the file (with \code{.txt} or \code{.geojson} extension) to #' be read. -#' @param gtfs_standards A named list. Created by -#' \code{\link{get_gtfs_standards}}. #' @param fields A named list. Passed by the user to \code{\link{import_gtfs}}. #' @param extra_spec A named list. Passed by the user to #' \code{\link{import_gtfs}}. @@ -242,7 +235,6 @@ import_gtfs <- function(path, #' #' @keywords internal read_files <- function(file, - gtfs_standards, fields, extra_spec, tmpdir, @@ -252,7 +244,7 @@ read_files <- function(file, # create object to hold the file with '.txt' extension filename <- file - file_type <- "txt" + file_type <- "txt" # TODO get file ext as function if(grepl("\\.geojson$", file)) { file_type <- "geojson" } @@ -267,7 +259,7 @@ read_files <- function(file, # get standards for reading and fields to be read from the given 'file' - file_standards <- gtfs_standards[[file]] + ref_fields <- gtfs_reference[[file]][["field_types"]] fields <- fields[[file]] extra_spec <- extra_spec[[file]] @@ -275,9 +267,9 @@ read_files <- function(file, # documented or extra files are read. throw an error if it refers to # documented fields - spec_both <- names(extra_spec)[names(extra_spec) %chin% names(file_standards)] + spec_both <- names(extra_spec)[names(extra_spec) %chin% names(ref_fields)] - if (any(names(extra_spec) %chin% names(file_standards))) { + if (any(names(extra_spec) %chin% names(ref_fields))) { error_field_is_documented(file, spec_both) } @@ -285,7 +277,7 @@ read_files <- function(file, # - if 'file_standards' is NULL then file is undocumented # - print warning message if warning is raised and 'quiet' is FALSE - if (is.null(file_standards) & !quiet) { + if (is.null(ref_fields) & !quiet) { message(" - File undocumented. Trying to read it as a csv.") } @@ -336,11 +328,11 @@ read_files <- function(file, # get the standard data types of documented fields from 'file_standards' - doc_fields <- fields_to_read[fields_to_read %chin% names(file_standards)] + doc_fields <- fields_to_read[fields_to_read %chin% names(ref_fields)] doc_classes <- vapply( doc_fields, - function(field) file_standards[[field]][[1]], + function(field) ref_fields[[field]][[1]], character(1) ) @@ -394,7 +386,7 @@ remove_file_ext = function(file) { append_file_ext = function(file) { vapply(file, function(f) { - file_ext <- gtfs_file_data$file_ext[gtfs_file_data$file == f] + file_ext <- gtfs_reference[[f]]["file_ext"] if (length(file_ext) == 0) { # use default for argument-specified non-standard files, behaviour defined in test_import_gtfs.R#292 file_ext <- "txt" diff --git a/R/sysdata.rda b/R/sysdata.rda index 3f2e059bdccaa22d86b14955ac1a094aa5f944d0..255493c8a4e7de0e7308baa45cffea96cdcb01c4 100644 GIT binary patch literal 4078 zcmZ`)XIK;1(heYqk)VJeMS36-f*T;A8Qi54ssagJCA0vc_W&zWf`Al*^s;nJ=tv6) zUO=KDNEM_BO0UwCBCGr9-e32}_ntX3bKYm3Iq!MSkI}Znt1D{R%39h^C8=*-&7dz1^{9+1-Jl&*d%_oma>qIV`97SdQtfh~%@vI1h?Bx+z+4Aoo%TZKSWyU-FzY>2b_^+5>FPMRB5y_?k z>`hbJ(>xv!ZHsF&hdMgC@bq19E`wWGiz}CpR~VdwhJy!t1<5?5%RRiJ;R9}9vLc+s z1O8A@`A5*2+|%^4xHAm{*SyWV62Zoc^E6qlhQBk}bhwLx;Y6P(8I3BfEAka#!4_np z{)-Zs{7STtX1gFOy^2bBi;=(*T8h^=S%;eeC2}meGQdp?FgZ15uCDVU)@X~|QOlee z^){Z_eA}_pN9x$~p$&uJWS?=7Q-{vk)|ol~S;yd4bOpNMInLh8eL{Jc)#6wU1SMId zF2~)G?I#$v1>#+VI|G$#`~- zKOTdj&FWyahp!fKrAwS8=OhkXrI&$k7#FU~;g-3KjVp6=_2iH$hA)=4_bK041b|s3 zrB^Hw_bXVUt>iG6Nn!4e?%tKs&wHbtvs(uzYBpgx?Ng^h>IUM0%JzZd%`#ttn!h50 zTaGS$BykTbtKEXx%NA~B6RZvGY z3`ys-$zQMvw}JM0jRG?_%*?WlGc(HvXx|?Yy}qmDTQUd6+#b+u!)ductz&>y@J*xD zqXr%vOxKD}<$CG8O<{2XOqy*2QSe9C(;U4`g^s#5N;4g8ja;pq8OvcSivRglUzr@>1eN9KZ4*{5P({9f#!J4?BOu=`-KmRug4Y zvpT;z3#@-)hM%ojHomk#@5@0M3M~++9IK~2CkY(G2hD2Tss`>tAj`^3el*t6U(r8} zu&v`RChTdcyrZ7?Gj> zL~Vzoa)ORSI(fyeCMGz)`Oq=@V#V5+HpNWNB&D^X)$Kd}=Ibo0Hwir>-lXMEOxAUO zDGXMyml|2NTC>cw)I(3zVg|d;HP*&I*V9ijd6qZpvS#HJDPWfd>_ZX?xnUX(HQd?p zsrFt#*(V+)>{yA}9*S6D&P9(?+fcS7R3 ziK?3%YO_;xrPLSY`O~g8crL`5DX&>3{?89}t10|XGc>n9Z6Uq-xLhe2EjE6xSuH_M zU4>KlODgbj6Nt|5FPS0*+%U*)UcW|6MCHGE}fnAjrY_ymCuVPuFwrsopxp$CI@=?)quc+%FD}EX&LYTh48xDK+73 z${`D#Rm_)t_LJVByRVycO=08 zOD8KmHTG^5c$Mtk`cALau$*iNj%I*kr3{;3WLS)3n7o#qA+TK^*tDJ(`Q#A@7-yZH zdFSn8!(1=?N>kWxl~om$sjUzTwn!|erR3?Y|Mrw!ktE0o9M zrf(7w7j5ruJCv}AIA|tIa1z5PnAc#+AJr)NRCzyaZ@(A9cYRqH^-l-B`%urxe0b+Z z-%3}7YRFsVV0NH(+v}R#4Y~G)E3!Je^Hh;L= zZP5E|V?RHIPG5Vk;%hxbm?K~@Hc5DV{$pXKNN)#`JaC@U>2^aOu#XEj2UXQqSm^v_ zn=Lq6d8we{ZMzC#o(UXi!B$uT^O_%EzSfFB+fI6MtRPiaMN%Z>B*GmD=E`Y`HTu)dc20RLHY%@ zwF|d3tb0R^-c3Msu=dkx2*SWCm)FXLue=zEK5||nbEzO3>+>T{@d!V)YboPyL%=!L zL23`Js}s76-596^*{ip-U68HK^$F16?-^P_E5M zLs_dWwA_z#3vC21!gS^9>q`0*0b4DD9ov zQEP%e%H96I`-hJ

5b{^*q#A_-$}IRJ9!vS%rh9 zJu6%(s&{J6N2}gaJ#l>9dTc)&-@a{K(z6zF)b&~jVv3%zvKCnKy?7~iEOG1*oDq`= zJ}&fC9wcsnMR-)(z(<;O6PTxmMxlRwyJ> z&54YI92yZSegv;`6DH!AZH-(F3$wn6JkYheNs!}w;@0fM`@g)$D=kcK$ZIS)I1|7{ zpoaJSbt*%*31nL;I*Utv%tXbdCtgHp8qC=ABCWVunC#G=2Vz%k;0)SxeXJ|(DbuZ1 z2~UTd%NJ)gEqe&wQRhA{S&IITM?qsEwNjAqpHX{3XGi5qTMuN%2k@X1&NZ=&i-9yo zdRjEfz%o{35?W7Xxz~;|c(2KQUi>7Ah=nQgsEMh|uBb2i1Q`0Owv`}bgRgzePLj8a zjgJ551grq`h)@@%(GZuMm=H`;uTj0S`z$<=uLwh!fp9pkN!ILwb$nCn4SOprsI_;z zn`*84IQv^E-qiw3-eiRqqfG29m|~O(eb&oRG`1L_jUX0>% zHifOJ5SV;R9E9f$HT{$zH0V9 zsu!FOF~)*$s;?%>ro!)$$S64(Gwa`^!ZpSsFFhFkn$R~ex_4IN{?4n5e^+TDCEqV< zE0(gY8FF_i`IFuJ=+LP-%8b)k?7%Gak+?8=4_vST=@Sb(KwW?c=FiQE&P>l8|odOm~&F;o~eiirP zCmDl&44QO~q+B9ERnTt|Z^gWkeqTBKC%5$}+5EQX4ZZ1s&}A9b&T=2LD%pta-St@p z?cpjlQ6F?H&RUov${jN-663iM6EPClRaLLaf+On{7~s$r6dpC*G$~WYo5=D<;rHZL zZ_=xr?iZLHLk}u>1G;>9gAM6bxW18{Nv;&$V6vC%&_%cDfmPicJ6r_@ckLwXQwA|i zWKYvwmk{HFR>$z(Tmmu{d^F{+p*IQFD!c0atA|t8_#Rxiuo}L(;Gx#FHX6m84@9g- z)IaRI8>$`hB5`g{G?jio2bG0J7U{_@thE^JAE?ikFC&UuaOGJ>MpWuEXjaWr4?Z4E zdEm$HTcQH(NF0S zRZ=JCrXobombw>Bc}-Bmc4qA0)A|6h;7=SB&DRd8&u&%r&!Eec->=?<@!!+Ug{M~t zkadzH`7#gqu`uk~2qE=?hD6=7CxhfZcVk#jUgNy}a?#%z{hqh0rEDMu@#Vnx#i5qG$lsvLvpTLIU1Hk^T6vtB>(dVd&VHSH9Y`_EiZAjNx{3&iFW-^PeK+E;hvUx9oHWeyBk zkJ{xL=Ln7Q_}N>tKS!zenSW>fjnTxO=OAQ&(qmv_tEw;ED`2;_7PFADJ*Ww}Ln8qX zeS;uZGrQnbmR!0S=RGmumPz80?ZfR6yV8u8#mnl2m0}PJ^w?(&XX~*icg1?lAQgk} zzSRh-iR2O^H-=6r?bi4`;bv^@+bPfY3wKXC8{s=qYNA-QCKBE>x?v06ZDNoD`uls2 zbKGv4^R^n4tf}BfEny9wwSubfd2(>8>FvRcVKCfHH^YhyLSXEm^-5h7@IiI&#Jm?I z5uzL}R)I1LjauA8CRjf6H*?jYka(?~x!~yoZ$#5g5ls_~$pW*!SDOzSA~u)FActX{ zJahTt7h>6M6xuf9_p$tY-lHULT literal 2824 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¬)Y!5bwvYuUL_0*PN`<%v6g9-iZ#BRt zaAUO8!)=NnVZcIST+B>gH4PSaSg68~UO~w5a~39zZhcxBhmxtIc_Gw1NIvr--x1-` zJvufVM!p4favOZ1v`hT!IyVF}IBU7~QAU$1JUd;ViO44~9Odwr$wMl?fCeup8C5fz z=ma?8NJtzAZ?y%BQWmL9sv3*B7LB0tof8r5?J~m%+SFcQMvBn!;@t}g+Ji-r0_vbx z0|rQv0SHY~)b-AL?zjg+>(D2r9R|eSbaP!(kBl>Qzz#a4o2sa^R2|3-j1+=FBnp8D zNayP7qr!0lCH48Wsn_HZY2XKa%)JUf0n3Hthca#8r#frv=R-a<9Qi{#bj*Y`e#BS{ ziL>aENGED@^&Vgify3=osDir@9^WKHbZ`m}T2itkgQYY&kl*q%@ZzF-(11+h2PRNn zRKSBJwkeB|oEEw~{uw(2H1QCkF-9vOhMG`-;l^n!C838h1F=kpNSX+SB;tf-7QPz6 aYRklY)AbH@*+?XR#oUoj6eK5LO02-Yy7oN) diff --git a/inst/reference/create_gtfs_standards.R b/inst/reference/create_gtfs_standards.R index 980e1c5..35d2189 100644 --- a/inst/reference/create_gtfs_standards.R +++ b/inst/reference/create_gtfs_standards.R @@ -50,20 +50,28 @@ if(any(is.na(f$gtfsio_type))) { } # Rename columns, add file column without location #### -gtfsio_field_types = f |> - rename(file = File_Name, field_name = Field_Name) |> - mutate(file = gsub("\\.txt$", "", gsub("\\.geojson$", "", file))) |> +gtfs_reference_fields = f |> + mutate(file = gsub("\\.txt$", "", gsub("\\.geojson$", "", File_Name))) |> as.data.frame() -gtfs_file_data = cleanup_files_reference(parse_files(reference.md)) -gtfs_file_data <- gtfs_file_data |> +gtfs_reference_files = cleanup_files_reference(parse_files(reference.md)) +gtfs_reference_files <- gtfs_reference_files |> tidyr::separate(File_Name, c("file", "file_ext"), sep = "\\.", remove = F) |> select(File_Name, File_Presence, file, file_ext) |> as.data.frame() -# save for possible external use -write.csv(gtfsio_field_types, "gtfsio_field_conversion_types.csv", row.names = FALSE, eol = "\r", fileEncoding = "UTF-8") - # save as internal data -usethis::use_data(gtfsio_field_types, gtfs_file_data, internal = T, overwrite = T) +gtfs_reference = gtfs_reference_files |> + split(gtfs_reference_files$file) |> + lapply(as.list) + +for(file in names(gtfs_reference)) { + fields = gtfs_reference_fields[gtfs_reference_fields$file == file,] + fields <- fields |> select(-file, -File_Name) + gtfs_reference[[file]]$fields <- fields + field_types = fields$gtfsio_type + names(field_types) <- fields$Field_Name + gtfs_reference[[file]]$field_types <- field_types +} +usethis::use_data(gtfs_reference, internal = T, overwrite = T) diff --git a/inst/reference/gtfsio_field_conversion_types.csv b/inst/reference/gtfsio_field_conversion_types.csv deleted file mode 100644 index 9f0bd14..0000000 --- a/inst/reference/gtfsio_field_conversion_types.csv +++ /dev/null @@ -1 +0,0 @@ -"file","field_name","Type","Presence","gtfsio_type" "agency","agency_id","Unique ID","Conditionally Required","character" "agency","agency_name","Text","Required","character" "agency","agency_url","URL","Required","character" "agency","agency_timezone","Timezone","Required","character" "agency","agency_lang","Language code","Optional","character" "agency","agency_phone","Phone number","Optional","character" "agency","agency_fare_url","URL","Optional","character" "agency","agency_email","Email","Optional","character" "stops","stop_id","Unique ID","Required","character" "stops","stop_code","Text","Optional","character" "stops","stop_name","Text","Conditionally Required","character" "stops","tts_stop_name","Text","Optional","character" "stops","stop_desc","Text","Optional","character" "stops","stop_lat","Latitude","Conditionally Required","numeric" "stops","stop_lon","Longitude","Conditionally Required","numeric" "stops","zone_id","ID","Optional","character" "stops","stop_url","URL","Optional","character" "stops","location_type","Enum","Optional","integer" "stops","parent_station","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "stops","stop_timezone","Timezone","Optional","character" "stops","wheelchair_boarding","Enum","Optional","integer" "stops","level_id","Foreign ID referencing `levels.level_id`","Optional","character" "stops","platform_code","Text","Optional","character" "routes","route_id","Unique ID","Required","character" "routes","agency_id","Foreign ID referencing `agency.agency_id`","Conditionally Required","character" "routes","route_short_name","Text","Conditionally Required","character" "routes","route_long_name","Text","Conditionally Required","character" "routes","route_desc","Text","Optional","character" "routes","route_type","Enum","Required","integer" "routes","route_url","URL","Optional","character" "routes","route_color","Color","Optional","character" "routes","route_text_color","Color","Optional","character" "routes","route_sort_order","Non-negative integer","Optional","integer" "routes","continuous_pickup","Enum","Conditionally Forbidden","integer" "routes","continuous_drop_off","Enum","Conditionally Forbidden","integer" "routes","network_id","ID","Conditionally Forbidden","character" "trips","route_id","Foreign ID referencing `routes.route_id`","Required","character" "trips","service_id","Foreign ID referencing `calendar.service_id` or `calendar_dates.service_id`","Required","character" "trips","trip_id","Unique ID","Required","character" "trips","trip_headsign","Text","Optional","character" "trips","trip_short_name","Text","Optional","character" "trips","direction_id","Enum","Optional","integer" "trips","block_id","ID","Optional","character" "trips","shape_id","Foreign ID referencing `shapes.shape_id`","Conditionally Required","character" "trips","wheelchair_accessible","Enum","Optional","integer" "trips","bikes_allowed","Enum","Optional","integer" "stop_times","trip_id","Foreign ID referencing `trips.trip_id`","Required","character" "stop_times","arrival_time","Time","Conditionally Required","character" "stop_times","departure_time","Time","Conditionally Required","character" "stop_times","stop_id","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "stop_times","location_group_id","Foreign ID referencing `location_groups.location_group_id`","Conditionally Forbidden","character" "stop_times","location_id","Foreign ID referencing `id` from `locations.geojson`","Conditionally Forbidden","character" "stop_times","stop_sequence","Non-negative integer","Required","integer" "stop_times","stop_headsign","Text","Optional","character" "stop_times","start_pickup_drop_off_window","Time","Conditionally Required","character" "stop_times","end_pickup_drop_off_window","Time","Conditionally Required","character" "stop_times","pickup_type","Enum","Conditionally Forbidden","integer" "stop_times","drop_off_type","Enum","Conditionally Forbidden","integer" "stop_times","continuous_pickup","Enum","Conditionally Forbidden","integer" "stop_times","continuous_drop_off","Enum","Conditionally Forbidden","integer" "stop_times","shape_dist_traveled","Non-negative float","Optional","numeric" "stop_times","timepoint","Enum","Optional","integer" "stop_times","pickup_booking_rule_id","Foreign ID referencing `booking_rules.booking_rule_id`","Optional","character" "stop_times","drop_off_booking_rule_id","Foreign ID referencing `booking_rules.booking_rule_id`","Optional","character" "calendar","service_id","Unique ID","Required","character" "calendar","monday","Enum","Required","integer" "calendar","tuesday","Enum","Required","integer" "calendar","wednesday","Enum","Required","integer" "calendar","thursday","Enum","Required","integer" "calendar","friday","Enum","Required","integer" "calendar","saturday","Enum","Required","integer" "calendar","sunday","Enum","Required","integer" "calendar","start_date","Date","Required","integer" "calendar","end_date","Date","Required","integer" "calendar_dates","service_id","Foreign ID referencing `calendar.service_id` or ID","Required","character" "calendar_dates","date","Date","Required","integer" "calendar_dates","exception_type","Enum","Required","integer" "fare_attributes","fare_id","Unique ID","Required","character" "fare_attributes","price","Non-negative float","Required","numeric" "fare_attributes","currency_type","Currency code","Required","character" "fare_attributes","payment_method","Enum","Required","integer" "fare_attributes","transfers","Enum","Required","integer" "fare_attributes","agency_id","Foreign ID referencing `agency.agency_id`","Conditionally Required","character" "fare_attributes","transfer_duration","Non-negative integer","Optional","integer" "fare_rules","fare_id","Foreign ID referencing `fare_attributes.fare_id`","Required","character" "fare_rules","route_id","Foreign ID referencing `routes.route_id`","Optional","character" "fare_rules","origin_id","Foreign ID referencing `stops.zone_id`","Optional","character" "fare_rules","destination_id","Foreign ID referencing `stops.zone_id`","Optional","character" "fare_rules","contains_id","Foreign ID referencing `stops.zone_id`","Optional","character" "timeframes","timeframe_group_id","ID","Required","character" "timeframes","start_time","Time","Conditionally Required","character" "timeframes","end_time","Time","Conditionally Required","character" "timeframes","service_id","Foreign ID referencing `calendar.service_id` or `calendar_dates.service_id`","Required","character" "fare_media","fare_media_id","Unique ID","Required","character" "fare_media","fare_media_name","Text","Optional","character" "fare_media","fare_media_type","Enum","Required","integer" "fare_products","fare_product_id","ID","Required","character" "fare_products","fare_product_name","Text","Optional","character" "fare_products","fare_media_id","Foreign ID referencing `fare_media.fare_media_id`","Optional","character" "fare_products","amount","Currency amount","Required","numeric" "fare_products","currency","Currency code","Required","character" "fare_leg_rules","leg_group_id","ID","Optional","character" "fare_leg_rules","network_id","Foreign ID referencing `routes.network_id` or `networks.network_id`","Optional","character" "fare_leg_rules","from_area_id","Foreign ID referencing `areas.area_id`","Optional","character" "fare_leg_rules","to_area_id","Foreign ID referencing `areas.area_id`","Optional","character" "fare_leg_rules","from_timeframe_group_id","Foreign ID referencing `timeframes.timeframe_group_id`","Optional","character" "fare_leg_rules","to_timeframe_group_id","Foreign ID referencing `timeframes.timeframe_group_id`","Optional","character" "fare_leg_rules","fare_product_id","Foreign ID referencing `fare_products.fare_product_id`","Required","character" "fare_leg_rules","rule_priority","Non-negative integer","Optional","integer" "fare_transfer_rules","from_leg_group_id","Foreign ID referencing `fare_leg_rules.leg_group_id`","Optional","character" "fare_transfer_rules","to_leg_group_id","Foreign ID referencing `fare_leg_rules.leg_group_id`","Optional","character" "fare_transfer_rules","transfer_count","Non-zero integer","Conditionally Forbidden","integer" "fare_transfer_rules","duration_limit","Positive integer","Optional","integer" "fare_transfer_rules","duration_limit_type","Enum","Conditionally Required","integer" "fare_transfer_rules","fare_transfer_type","Enum","Required","integer" "fare_transfer_rules","fare_product_id","Foreign ID referencing `fare_products.fare_product_id`","Optional","character" "areas","area_id","Unique ID","Required","character" "areas","area_name","Text","Optional","character" "stop_areas","area_id","Foreign ID referencing `areas.area_id`","Required","character" "stop_areas","stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "networks","network_id","Unique ID","Required","character" "networks","network_name","Text","Optional","character" "route_networks","network_id","Foreign ID referencing `networks.network_id`","Required","character" "route_networks","route_id","Foreign ID referencing `routes.route_id`","Required","character" "shapes","shape_id","ID","Required","character" "shapes","shape_pt_lat","Latitude","Required","numeric" "shapes","shape_pt_lon","Longitude","Required","numeric" "shapes","shape_pt_sequence","Non-negative integer","Required","integer" "shapes","shape_dist_traveled","Non-negative float","Optional","numeric" "frequencies","trip_id","Foreign ID referencing `trips.trip_id`","Required","character" "frequencies","start_time","Time","Required","character" "frequencies","end_time","Time","Required","character" "frequencies","headway_secs","Positive integer","Required","integer" "frequencies","exact_times","Enum","Optional","integer" "transfers","from_stop_id","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "transfers","to_stop_id","Foreign ID referencing `stops.stop_id`","Conditionally Required","character" "transfers","from_route_id","Foreign ID referencing `routes.route_id`","Optional","character" "transfers","to_route_id","Foreign ID referencing `routes.route_id`","Optional","character" "transfers","from_trip_id","Foreign ID referencing `trips.trip_id`","Conditionally Required","character" "transfers","to_trip_id","Foreign ID referencing `trips.trip_id`","Conditionally Required","character" "transfers","transfer_type","Enum","Required","integer" "transfers","min_transfer_time","Non-negative integer","Optional","integer" "pathways","pathway_id","Unique ID","Required","character" "pathways","from_stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "pathways","to_stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "pathways","pathway_mode","Enum","Required","integer" "pathways","is_bidirectional","Enum","Required","integer" "pathways","length","Non-negative float","Optional","numeric" "pathways","traversal_time","Positive integer","Optional","integer" "pathways","stair_count","Non-null integer","Optional","integer" "pathways","max_slope","Float","Optional","numeric" "pathways","min_width","Positive float","Optional","numeric" "pathways","signposted_as","Text","Optional","character" "pathways","reversed_signposted_as","Text","Optional","character" "levels","level_id","Unique ID","Required","character" "levels","level_index","Float","Required","numeric" "levels","level_name","Text","Optional","character" "location_groups","location_group_id","Unique ID","Required","character" "location_groups","location_group_name","Text","Optional","character" "location_group_stops","location_group_id","Foreign ID referencing `location_groups.location_group_id`","Required","character" "location_group_stops","stop_id","Foreign ID referencing `stops.stop_id`","Required","character" "locations","type","String","Required","character" "locations","features","Array","Required","geojson_array" "locations","type","String","Required","character" "locations","id","String","Required","character" "locations","properties","Object","Required","geojson_object" "locations","stop_name","String","Optional","character" "locations","stop_desc","String","Optional","character" "locations","geometry","Object","Required","geojson_object" "locations","type","String","Required","character" "locations","coordinates","Array","Required","geojson_array" "booking_rules","booking_rule_id","Unique ID","Required","character" "booking_rules","booking_type","Enum","Required","integer" "booking_rules","prior_notice_duration_min","Integer","Conditionally Required","integer" "booking_rules","prior_notice_duration_max","Integer","Conditionally Forbidden","integer" "booking_rules","prior_notice_last_day","Integer","Conditionally Required","integer" "booking_rules","prior_notice_last_time","Time","Conditionally Required","character" "booking_rules","prior_notice_start_day","Integer","Conditionally Forbidden","integer" "booking_rules","prior_notice_start_time","Time","Conditionally Required","character" "booking_rules","prior_notice_service_id","Foreign ID referencing `calendar.service_id`","Conditionally Forbidden","character" "booking_rules","message","Text","Optional","character" "booking_rules","pickup_message","Text","Optional","character" "booking_rules","drop_off_message","Text","Optional","character" "booking_rules","phone_number","Phone number","Optional","character" "booking_rules","info_url","URL","Optional","character" "booking_rules","booking_url","URL","Optional","character" "translations","table_name","Enum","Required","character" "translations","field_name","Text","Required","character" "translations","language","Language code","Required","character" "translations","translation","Text or URL or Email or Phone number","Required","character" "translations","record_id","Foreign ID","Conditionally Required","character" "translations","record_sub_id","Foreign ID","Conditionally Required","character" "translations","field_value","Text or URL or Email or Phone number","Conditionally Required","character" "feed_info","feed_publisher_name","Text","Required","character" "feed_info","feed_publisher_url","URL","Required","character" "feed_info","feed_lang","Language code","Required","character" "feed_info","default_lang","Language code","Optional","character" "feed_info","feed_start_date","Date","Recommended","integer" "feed_info","feed_end_date","Date","Recommended","integer" "feed_info","feed_version","Text","Recommended","character" "feed_info","feed_contact_email","Email","Optional","character" "feed_info","feed_contact_url","URL","Optional","character" "attributions","attribution_id","Unique ID","Optional","character" "attributions","agency_id","Foreign ID referencing `agency.agency_id`","Optional","character" "attributions","route_id","Foreign ID referencing `routes.route_id`","Optional","character" "attributions","trip_id","Foreign ID referencing `trips.trip_id`","Optional","character" "attributions","organization_name","Text","Required","character" "attributions","is_producer","Enum","Optional","integer" "attributions","is_operator","Enum","Optional","integer" "attributions","is_authority","Enum","Optional","integer" "attributions","attribution_url","URL","Optional","character" "attributions","attribution_email","Email","Optional","character" "attributions","attribution_phone","Phone number","Optional","character" \ No newline at end of file diff --git a/inst/tinytest/test_export_gtfs.R b/inst/tinytest/test_export_gtfs.R index 73a76b3..4fcf0d0 100644 --- a/inst/tinytest/test_export_gtfs.R +++ b/inst/tinytest/test_export_gtfs.R @@ -191,7 +191,7 @@ for (filenames in list.files(tmpd)) { # all existing fields should be standard file <- gtfsio:::remove_file_ext(filenames) - std_fields <- setdiff(names(gtfs_standards[[file]]), "file_spec") + std_fields <- names(gtfsio:::gtfs_reference[[file]][["field_types"]]) existing_fields <- readLines(file.path(tmpd, filenames), n = 1L) existing_fields <- strsplit(existing_fields, ",")[[1]] diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index 3cc9a5f..6c745bb 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -146,16 +146,7 @@ expect_identical(gtfs_fields, list(shapes = "shape_id", trips = "trip_id")) gtfs_standards <- get_gtfs_standards() -standard_types <- lapply( - gtfs_standards, - function(file) { - fields <- setdiff(names(file), "file_spec") - types <- vapply(fields, function(f) file[[f]][[1]], character(1)) - types <- types[order(names(types))] - } -) -names(standard_types) <- gtfsio:::remove_file_ext(names(standard_types)) -standard_types <- standard_types[order(names(standard_types))] +standard_types <- lapply(gtfsio:::gtfs_reference, `[[`, "field_types") # get the type actually used to read each field diff --git a/man/get_gtfs_standards.Rd b/man/get_gtfs_standards.Rd index 98156b9..6ea0dde 100644 --- a/man/get_gtfs_standards.Rd +++ b/man/get_gtfs_standards.Rd @@ -34,28 +34,28 @@ represent each GTFS data type are described below: \item{Language code = \code{character}} \item{Phone number = \code{character}} \item{Email = \code{character}} -\item{ID = \code{character}} -\item{Color = \code{character}} \item{Time = \code{character}} \item{Currency code = \code{character}} +\item{ID = \code{character}} \item{String = \code{character}} +\item{Color = \code{character}} \item{Enum = \code{character}} \item{Text or URL or Email or Phone number = \code{character}} \item{Array = \code{geojson_array}} \item{Object = \code{geojson_object}} \item{Enum = \code{integer}} -\item{Non-negative integer = \code{integer}} +\item{Integer = \code{integer}} \item{Date = \code{integer}} +\item{Non-negative integer = \code{integer}} \item{Non-zero integer = \code{integer}} \item{Positive integer = \code{integer}} \item{Non-null integer = \code{integer}} -\item{Integer = \code{integer}} -\item{Latitude = \code{numeric}} -\item{Longitude = \code{numeric}} \item{Non-negative float = \code{numeric}} \item{Currency amount = \code{numeric}} \item{Float = \code{numeric}} \item{Positive float = \code{numeric}} +\item{Latitude = \code{numeric}} +\item{Longitude = \code{numeric}} } } diff --git a/man/read_files.Rd b/man/read_files.Rd index 791cec1..04e7f72 100644 --- a/man/read_files.Rd +++ b/man/read_files.Rd @@ -4,15 +4,12 @@ \alias{read_files} \title{Read a GTFS text file} \usage{ -read_files(file, gtfs_standards, fields, extra_spec, tmpdir, quiet, encoding) +read_files(file, fields, extra_spec, tmpdir, quiet, encoding) } \arguments{ \item{file}{A string. The name of the file (with \code{.txt} or \code{.geojson} extension) to be read.} -\item{gtfs_standards}{A named list. Created by -\code{\link{get_gtfs_standards}}.} - \item{fields}{A named list. Passed by the user to \code{\link{import_gtfs}}.} \item{extra_spec}{A named list. Passed by the user to From fb84dbeef5a4ec5862eb1f0f3b83dd946b678df3 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Tue, 27 Aug 2024 15:42:15 +0200 Subject: [PATCH 07/12] deprecate get_gtfs_standard() revert to original implementation --- R/export_gtfs.R | 4 +- R/get_gtfs_standards.R | 394 ++++++++++++++++++++++++++++++- R/import_gtfs.R | 9 +- inst/tinytest/test_export_gtfs.R | 1 - inst/tinytest/test_import_gtfs.R | 2 - man/export_gtfs.Rd | 4 +- man/get_gtfs_standards.Rd | 62 +++-- man/import_gtfs.Rd | 7 +- man/read_files.Rd | 2 +- man/translate_types.Rd | 23 ++ 10 files changed, 450 insertions(+), 58 deletions(-) create mode 100644 man/translate_types.Rd diff --git a/R/export_gtfs.R b/R/export_gtfs.R index 7083e3f..16d69a9 100644 --- a/R/export_gtfs.R +++ b/R/export_gtfs.R @@ -2,7 +2,7 @@ #' #' Writes GTFS objects to disk as GTFS transit feeds. The object must be #' formatted according to the standards for reading and writing GTFS transit -#' feeds, as specified in \code{\link{get_gtfs_standards}} (i.e. data types are +#' feeds, as specified in \code{\link{gtfs_reference}} (i.e. data types are #' not checked). If present, does not write auxiliary tables held in a sub-list #' named \code{"."}. #' @@ -25,7 +25,7 @@ #' #' @return Invisibly returns the same GTFS object passed to \code{gtfs}. #' -#' @seealso \code{\link{get_gtfs_standards}} +#' @seealso \code{\link{gtfs_reference}} #' #' @family io functions #' diff --git a/R/get_gtfs_standards.R b/R/get_gtfs_standards.R index b2682f1..2cb0dcb 100644 --- a/R/get_gtfs_standards.R +++ b/R/get_gtfs_standards.R @@ -1,15 +1,20 @@ #' Generate GTFS standards #' #' @description +#' *This function is deprecated and no longer used in [import_gtfs()] or [export_gtfs()].* +#' #' Generates a list specifying the standards to be used when reading and writing #' GTFS feeds with R. Each list element (also a list) represents a distinct GTFS #' table, and describes: #' +#' - whether the table is required, optional or conditionally required; #' - the fields that compose the table, including which R data type is best #' suited to represent it, whether the field is required, optional or -#' conditionally required. -#' - whether the table is required, optional or conditionally required (as an -#' attribute) +#' conditionally required, and which values it can assume (most relevant to GTFS +#' `ENUM`s. +#' +#' Note: the standards list is based on the specification as revised in May 9th, +#' 2022. #' #' @return A named list, in which each element represents the R equivalent of #' each GTFS table standard. @@ -18,14 +23,389 @@ #' GTFS standards were derived from [GTFS Schedule #' Reference](https://gtfs.org/schedule/reference/). The R data types chosen to #' represent each GTFS data type are described below: -#' `r .doc_field_types()` #' -#' @examples -#' gtfs_standards <- get_gtfs_standards() +#' - Color = `character` +#' - Currency amount = `numeric` +#' - Currency code = `character` +#' - Date = `integer` +#' - Email = `character` +#' - ENUM = `integer` +#' - ID = `character` +#' - Integer = `integer` +#' - Language code = `character` +#' - Latitude = `numeric` +#' - Longitude = `numeric` +#' - Float = `numeric` +#' - Phone number = `character` +#' - Text = `character` +#' - Time = `character` +#' - Timezone = `character` +#' - URL = `character` #' +#' @examples \dontrun{ +#' gtfs_standards <- get_gtfs_standards() +#' } #' @export get_gtfs_standards <- function() { - return(NULL) + .Deprecated("gtfs_reference") + agency <- list( + file_spec = "req", + agency_id = list("id", "cond"), + agency_name = list("text", "req"), + agency_url = list("url", "req"), + agency_timezone = list("timezone", "req"), + agency_lang = list("language_code", "opt"), + agency_phone = list("phone_number", "opt"), + agency_fare_url = list("url", "opt"), + agency_email = list("email", "opt") + ) + + stops <- list( + file_spec = "req", + stop_id = list("id", "req"), + stop_code = list("text", "opt"), + stop_name = list("text", "cond"), + tts_stop_name = list("text", "opt"), + stop_desc = list("text", "opt"), + stop_lat = list("latitude", "cond"), + stop_lon = list("longitude", "cond"), + zone_id = list("id", "cond"), + stop_url = list("url", "opt"), + location_type = list("enum", "opt", c(0, 1, 2, 3, 4)), + parent_station = list("id", "cond"), + stop_timezone = list("timezone", "opt"), + wheelchair_boarding = list("enum", "opt", c(0, 1, 2)), + level_id = list("id", "opt"), + platform_code = list("text", "opt") + ) + + routes <- list( + file_spec = "req", + route_id = list("id", "req"), + agency_id = list("id", "cond"), + route_short_name = list("text", "cond"), + route_long_name = list("text", "cond"), + route_desc = list("text", "opt"), + route_type = list("enum", "req", c(0, 1, 2, 3, 4, 5, 6, 7, 11, + 12)), + route_url = list("url", "opt"), + route_color = list("color", "opt"), + route_text_color = list("color", "opt"), + route_sort_order = list("integer", "opt"), + continuous_pickup = list("enum", "opt", c(0, 1, 2, 3)), + continuous_dropoff = list("enum", "opt", c(0, 1, 2, 3)), + network_id = list("id", "opt") + ) + + trips <- list( + file_spec = "req", + route_id = list("id", "req"), + service_id = list("id", "req"), + trip_id = list("id", "req"), + trip_headsign = list("text", "opt"), + trip_short_name = list("text", "opt"), + direction_id = list("enum", "opt", c(0, 1)), + block_id = list("id", "opt"), + shape_id = list("id", "cond"), + wheelchair_accessible = list("enum", "opt", c(0, 1, 2)), + bikes_allowed = list("enum", "opt", c(0, 1, 2)) + ) + + stop_times <- list( + file_spec = "req", + trip_id = list("id", "req"), + arrival_time = list("time", "cond"), + departure_time = list("time", "cond"), + stop_id = list("id", "req"), + stop_sequence = list("integer", "req"), + stop_headsign = list("text", "opt"), + pickup_type = list("enum", "opt", c(0, 1, 2, 3)), + drop_off_type = list("enum", "opt", c(0, 1, 2, 3)), + continuous_pickup = list("enum", "opt", c(0, 1, 2, 3)), + continuous_drop_off = list("enum", "opt", c(0, 1, 2, 3)), + shape_dist_traveled = list("float", "opt"), + timepoint = list("enum", "opt", c(0, 1)) + ) + + calendar <- list( + file_spec = "cond", + service_id = list("id", "req"), + monday = list("enum", "req", c(0, 1)), + tuesday = list("enum", "req", c(0, 1)), + wednesday = list("enum", "req", c(0, 1)), + thursday = list("enum", "req", c(0, 1)), + friday = list("enum", "req", c(0, 1)), + saturday = list("enum", "req", c(0, 1)), + sunday = list("enum", "req", c(0, 1)), + start_date = list("date", "req", c(0, 1)), + end_date = list("date", "req", c(0, 1)) + ) + + calendar_dates <- list( + file_spec = "cond", + service_id = list("id", "req"), + date = list("date", "req"), + exception_type = list("enum", "req", c(1, 2)) + ) + + fare_attributes <- list( + file_spec = "opt", + fare_id = list("id", "req"), + price = list("float", "req"), + currency_type = list("currency_code", "req"), + payment_method = list("enum", "req", c(0, 1)), + transfers = list("enum", "req", c(0, 1, 2)), + agency_id = list("id", "cond"), + transfer_duration = list("integer", "opt") + ) + + fare_rules <- list( + file_spec = "opt", + fare_id = list("id", "req"), + route_id = list("id", "opt"), + origin_id = list("id", "opt"), + destination_id = list("id", "opt"), + contains_id = list("id", "opt") + ) + + fare_products <- list( + file_spec = "opt", + fare_product_id = list("id", "req"), + fare_product_name = list("text", "opt"), + amount = list("currency_amount", "req"), + currency = list("currency_code", "req") + ) + + fare_leg_rules <- list( + file_spec = "opt", + leg_group_id = list("id", "opt"), + network_id = list("id", "opt"), + from_area_id = list("id", "opt"), + to_area_id = list("id", "opt"), + fare_product_id = list("id", "req") + ) + + fare_transfer_rules <- list( + file_spec = "opt", + from_leg_group_id = list("id", "opt"), + to_leg_group_id = list("id", "opt"), + transfer_count = list("integer", "cond"), + duration_limit = list("integer", "opt"), + duration_limit_type = list("enum", "cond", c(0, 1, 2, 3)), + fare_transfer_type = list("enum", "req", c(0, 1, 2)), + fare_product_id = list("id", "opt") + ) + + areas <- list( + file_spec = "opt", + area_id = list("id", "req"), + area_name = list("text", "opt") + ) + + stop_areas <- list( + file_spec = "opt", + area_id = list("id", "req"), + stop_id = list("id", "opt") + ) + + shapes <- list( + file_spec = "opt", + shape_id = list("id", "req"), + shape_pt_lat = list("latitude", "req"), + shape_pt_lon = list("longitude", "req"), + shape_pt_sequence = list("integer", "req"), + shape_dist_traveled = list("float", "opt") + ) + + frequencies <- list( + file_spec = "opt", + trip_id = list("id", "req"), + start_time = list("time", "req"), + end_time = list("time", "req"), + headway_secs = list("integer", "req"), + exact_times = list("enum", "opt", c(0, 1)) + ) + + transfers <- list( + file_spec = "opt", + from_stop_id = list("id", "cond"), + to_stop_id = list("id", "cond"), + from_route_id = list("id", "opt"), + to_route_id = list("id", "opt"), + from_trip_id = list("id", "cond"), + to_trip_id = list("id", "cond"), + transfer_type = list("enum", "req", c(0, 1, 2, 3, 4, 5)), + min_transfer_time = list("integer", "opt") + ) + + pathways <- list( + file_spec = "opt", + pathway_id = list("id", "req"), + from_stop_id = list("id", "req"), + to_stop_id = list("id", "req"), + pathway_mode = list("enum", "req", c(1, 2, 3, 4, 5, 6, 7)), + is_bidirectional = list("enum", "req", c(0, 1)), + length = list("float", "opt"), + traversal_time = list("integer", "opt"), + stair_count = list("integer", "opt"), + max_slope = list("float", "opt"), + min_width = list("float", "opt"), + signposted_as = list("text", "opt"), + reversed_signposted_as = list("text", "opt") + ) + + levels <- list( + file_spec = "cond", + level_id = list("id", "req"), + level_index = list("float", "req"), + level_name = list("text", "opt") + ) + + translations <- list( + file_spec = "opt", + table_name = list("enum", "req", c("agency", "stops", "routes", + "trips", "stop_times", + "feed_info", "pathways", + "levels", "attribution")), + field_name = list("text", "req"), + language = list("language_code", "req"), + translation = list(c("text", "url", "email", "phone_number"), "req"), + record_id = list("id", "cond"), + record_sub_id = list("id", "cond"), + field_value = list(c("text", "url", "email", "phone_number"), "cond") + ) + + feed_info <- list( + file_spec = "cond", + feed_publisher_name = list("text", "req"), + feed_publisher_url = list("url", "req"), + feed_lang = list("language_code", "req"), + default_lang = list("language_code", "opt"), + feed_start_date = list("date", "opt"), + feed_end_date = list("date", "opt"), + feed_version = list("text", "opt"), + feed_contact_email = list("email", "opt"), + feed_contact_url = list("url", "opt") + ) + + attributions <- list( + file_spec = "opt", + attribution_id = list("id", "opt"), + agency_id = list("id", "opt"), + route_id = list("id", "opt"), + trip_id = list("id", "opt"), + organization_name = list("text", "req"), + is_producer = list("enum", "opt", c(0, 1)), + is_operator = list("enum", "opt", c(0, 1)), + is_authority = list("enum", "opt", c(0, 1)), + attribution_url = list("url", "opt"), + attribution_email = list("email", "opt"), + attribution_phone = list("phone_number", "opt") + ) + + # create gtfs_standards object + + gtfs_standards <- list( + agency = agency, + stops = stops, + routes = routes, + trips = trips, + stop_times = stop_times, + calendar = calendar, + calendar_dates = calendar_dates, + fare_attributes = fare_attributes, + fare_rules = fare_rules, + fare_products = fare_products, + fare_leg_rules = fare_leg_rules, + fare_transfer_rules = fare_transfer_rules, + areas = areas, + stop_areas = stop_areas, + shapes = shapes, + frequencies = frequencies, + transfers = transfers, + pathways = pathways, + levels = levels, + translations = translations, + feed_info = feed_info, + attributions = attributions + ) + + # define R types most similar to GTFS reference types + + r_equivalents <- c( + color = "character", + currency_amount = "numeric", + currency_code = "character", + date = "integer", + email = "character", + enum = "integer", + id = "character", + integer = "integer", + language_code = "character", + latitude = "numeric", + longitude = "numeric", + float = "numeric", + phone_number = "character", + text = "character", + time = "character", + timezone = "character", + url = "character" + ) + + # translate GTFS reference types to R types + + gtfs_standards <- lapply(gtfs_standards, translate_types, r_equivalents) + + # correct a small special case: + # 'translations' 'table_name' field is an ENUM, but its allowed values are + # strings, not integers. this results in a warning when importing the GTFS + # using data.table::fread(). so change R equivalent to character, instead of + # integer + + gtfs_standards$translations$table_name[[1]] <- "character" + + return(gtfs_standards) +} + +#' Translate GTFS specification types to R equivalent types +#' +#' @param text_file A named `list` containing a GTFS text file specification, as +#' described in the body of [get_gtfs_standards]. +#' @param r_equivalents A named `character vector`, in which each name is the +#' GTFS specification type and the content its R equivalent. +#' +#' @return A named `list` holding a GTFS text file specification, but with +#' R data types instead of GTFS spec data types. +#' +#' @keywords internal +translate_types <- function(text_file, r_equivalents) { + + # for each text_file element: + # - check if it's a list. + # - if it's not, then it's the 'file_spec' element. return 'file_spec' value + # - if it is, replace first entry (GTFS spec type) for an equivalent R type + + text_file <- lapply( + text_file, + function(i) { + if (!is.list(i)) return(i) + + new_spec <- i + gtfs_type <- new_spec[[1]] + r_type <- r_equivalents[gtfs_type] + + # some 'translations' fields ('translation' and 'field_value') might have + # more than one GTFS data type. + # their R equivalent is always 'character', no matter the GTFS type, so we + # can safely get only the first value ('character') + + if (length(r_type) > 1) r_type <- r_type[1] + + new_spec[[1]] <- r_type + + return(new_spec) + } + ) } .doc_field_types = function() { # nocov start diff --git a/R/import_gtfs.R b/R/import_gtfs.R index 4ec4109..4f7a309 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -2,7 +2,7 @@ #' #' Imports GTFS transit feeds from either a local \code{.zip} file or an URL. #' Columns are parsed according to the standards for reading and writing GTFS -#' feeds specified in \code{\link{get_gtfs_standards}}. +#' feeds specified in \code{\link{gtfs_reference}}. #' #' @param path A string. The path to a GTFS \code{.zip} file. #' @param files A character vector. The text files to be read from the GTFS, @@ -21,8 +21,7 @@ #' if an undocumented field is not specified in \code{extra_spec}, it is read #' as character (i.e. you may specify in \code{extra_spec} only the fields #' that you want to read as a different type). Only supports the -#' \code{character}, \code{integer} and \code{numeric} types, also used in -#' \code{\link{get_gtfs_standards}}. +#' \code{character}, \code{integer} and \code{numeric} types. #' @param skip A character vector. Text files that should not be read from the #' GTFS, without the \code{.txt} extension. If \code{NULL} (the default), no #' files are skipped. Cannot be used if \code{files} is set. @@ -36,7 +35,7 @@ #' @return A GTFS object: a named list of data frames, each one corresponding to #' a distinct text file from the given GTFS feed. #' -#' @seealso \code{\link{get_gtfs_standards}} +#' @seealso \code{\link{gtfs_reference}} #' #' @family io functions #' @@ -231,7 +230,7 @@ import_gtfs <- function(path, #' @return A \code{data.table} representing the desired text file according to #' the standards for reading and writing GTFS feeds with R. #' -#' @seealso \code{\link{get_gtfs_standards}} +#' @seealso \code{\link{gtfs_reference}} #' #' @keywords internal read_files <- function(file, diff --git a/inst/tinytest/test_export_gtfs.R b/inst/tinytest/test_export_gtfs.R index 4fcf0d0..2de613b 100644 --- a/inst/tinytest/test_export_gtfs.R +++ b/inst/tinytest/test_export_gtfs.R @@ -2,7 +2,6 @@ path <- system.file("extdata/ggl_gtfs.zip", package = "gtfsio") gtfs <- import_gtfs(path) tmpf <- tempfile(fileext = ".zip") tmpd <- tempfile() -gtfs_standards <- get_gtfs_standards() tester <- function(gtfs_obj = gtfs, path = tmpf, diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index 6c745bb..11cd6b3 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -144,8 +144,6 @@ expect_identical(gtfs_fields, list(shapes = "shape_id", trips = "trip_id")) # get the standard type in R used to read each field -gtfs_standards <- get_gtfs_standards() - standard_types <- lapply(gtfsio:::gtfs_reference, `[[`, "field_types") # get the type actually used to read each field diff --git a/man/export_gtfs.Rd b/man/export_gtfs.Rd index 47b1b5c..705c7f8 100644 --- a/man/export_gtfs.Rd +++ b/man/export_gtfs.Rd @@ -46,7 +46,7 @@ Invisibly returns the same GTFS object passed to \code{gtfs}. \description{ Writes GTFS objects to disk as GTFS transit feeds. The object must be formatted according to the standards for reading and writing GTFS transit -feeds, as specified in \code{\link{get_gtfs_standards}} (i.e. data types are +feeds, as specified in \code{\link{gtfs_reference}} (i.e. data types are not checked). If present, does not write auxiliary tables held in a sub-list named \code{"."}. } @@ -65,7 +65,7 @@ zip::zip_list(tmpf)$filename } \seealso{ -\code{\link{get_gtfs_standards}} +\code{\link{gtfs_reference}} Other io functions: \code{\link{import_gtfs}()} diff --git a/man/get_gtfs_standards.Rd b/man/get_gtfs_standards.Rd index 6ea0dde..abf3a96 100644 --- a/man/get_gtfs_standards.Rd +++ b/man/get_gtfs_standards.Rd @@ -11,55 +11,49 @@ A named list, in which each element represents the R equivalent of each GTFS table standard. } \description{ +\emph{This function is deprecated and no longer used in \code{\link[=import_gtfs]{import_gtfs()}} or \code{\link[=export_gtfs]{export_gtfs()}}.} + Generates a list specifying the standards to be used when reading and writing GTFS feeds with R. Each list element (also a list) represents a distinct GTFS table, and describes: \itemize{ +\item whether the table is required, optional or conditionally required; \item the fields that compose the table, including which R data type is best suited to represent it, whether the field is required, optional or -conditionally required. -\item whether the table is required, optional or conditionally required (as an -attribute) +conditionally required, and which values it can assume (most relevant to GTFS +\code{ENUM}s. } + +Note: the standards list is based on the specification as revised in May 9th, +2022. } \section{Details}{ GTFS standards were derived from \href{https://gtfs.org/schedule/reference/}{GTFS Schedule Reference}. The R data types chosen to represent each GTFS data type are described below: \itemize{ -\item{Unique ID = \code{character}} -\item{Text = \code{character}} -\item{URL = \code{character}} -\item{Timezone = \code{character}} -\item{Language code = \code{character}} -\item{Phone number = \code{character}} -\item{Email = \code{character}} -\item{Time = \code{character}} -\item{Currency code = \code{character}} -\item{ID = \code{character}} -\item{String = \code{character}} -\item{Color = \code{character}} -\item{Enum = \code{character}} -\item{Text or URL or Email or Phone number = \code{character}} -\item{Array = \code{geojson_array}} -\item{Object = \code{geojson_object}} -\item{Enum = \code{integer}} -\item{Integer = \code{integer}} -\item{Date = \code{integer}} -\item{Non-negative integer = \code{integer}} -\item{Non-zero integer = \code{integer}} -\item{Positive integer = \code{integer}} -\item{Non-null integer = \code{integer}} -\item{Non-negative float = \code{numeric}} -\item{Currency amount = \code{numeric}} -\item{Float = \code{numeric}} -\item{Positive float = \code{numeric}} -\item{Latitude = \code{numeric}} -\item{Longitude = \code{numeric}} +\item Color = \code{character} +\item Currency amount = \code{numeric} +\item Currency code = \code{character} +\item Date = \code{integer} +\item Email = \code{character} +\item ENUM = \code{integer} +\item ID = \code{character} +\item Integer = \code{integer} +\item Language code = \code{character} +\item Latitude = \code{numeric} +\item Longitude = \code{numeric} +\item Float = \code{numeric} +\item Phone number = \code{character} +\item Text = \code{character} +\item Time = \code{character} +\item Timezone = \code{character} +\item URL = \code{character} } } \examples{ -gtfs_standards <- get_gtfs_standards() - +\dontrun{ + gtfs_standards <- get_gtfs_standards() +} } diff --git a/man/import_gtfs.Rd b/man/import_gtfs.Rd index 49f88b8..758f2eb 100644 --- a/man/import_gtfs.Rd +++ b/man/import_gtfs.Rd @@ -35,8 +35,7 @@ undocumented fields, in the format if an undocumented field is not specified in \code{extra_spec}, it is read as character (i.e. you may specify in \code{extra_spec} only the fields that you want to read as a different type). Only supports the -\code{character}, \code{integer} and \code{numeric} types, also used in -\code{\link{get_gtfs_standards}}.} +\code{character}, \code{integer} and \code{numeric} types.} \item{skip}{A character vector. Text files that should not be read from the GTFS, without the \code{.txt} extension. If \code{NULL} (the default), no @@ -57,7 +56,7 @@ a distinct text file from the given GTFS feed. \description{ Imports GTFS transit feeds from either a local \code{.zip} file or an URL. Columns are parsed according to the standards for reading and writing GTFS -feeds specified in \code{\link{get_gtfs_standards}}. +feeds specified in \code{\link{gtfs_reference}}. } \examples{ gtfs_path <- system.file("extdata/ggl_gtfs.zip", package = "gtfsio") @@ -84,7 +83,7 @@ gtfs <- import_gtfs( } \seealso{ -\code{\link{get_gtfs_standards}} +\code{\link{gtfs_reference}} Other io functions: \code{\link{export_gtfs}()} diff --git a/man/read_files.Rd b/man/read_files.Rd index 04e7f72..10fddd8 100644 --- a/man/read_files.Rd +++ b/man/read_files.Rd @@ -34,6 +34,6 @@ the standards for reading and writing GTFS feeds with R. Reads a GTFS text file from the main \code{.zip} file. } \seealso{ -\code{\link{get_gtfs_standards}} +\code{\link{gtfs_reference}} } \keyword{internal} diff --git a/man/translate_types.Rd b/man/translate_types.Rd new file mode 100644 index 0000000..ca8d06c --- /dev/null +++ b/man/translate_types.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/get_gtfs_standards.R +\name{translate_types} +\alias{translate_types} +\title{Translate GTFS specification types to R equivalent types} +\usage{ +translate_types(text_file, r_equivalents) +} +\arguments{ +\item{text_file}{A named \code{list} containing a GTFS text file specification, as +described in the body of \link{get_gtfs_standards}.} + +\item{r_equivalents}{A named \verb{character vector}, in which each name is the +GTFS specification type and the content its R equivalent.} +} +\value{ +A named \code{list} holding a GTFS text file specification, but with +R data types instead of GTFS spec data types. +} +\description{ +Translate GTFS specification types to R equivalent types +} +\keyword{internal} From ced29c4c807e385dc3b316281d360971038cb335 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Tue, 27 Aug 2024 16:30:01 +0200 Subject: [PATCH 08/12] export gtfs_reference data set --- DESCRIPTION | 1 + R/data.R | 26 ++++++++++ R/export_gtfs.R | 6 +-- R/get_gtfs_standards.R | 2 +- R/import_gtfs.R | 4 +- R/sysdata.rda | Bin 4078 -> 0 bytes data/gtfs_reference.rda | Bin 0 -> 19276 bytes inst/reference/create_gtfs_standards.R | 39 ++++++++++++--- inst/reference/parse_markdown.R | 3 +- inst/tinytest/test_export_gtfs.R | 2 +- inst/tinytest/test_import_gtfs.R | 2 +- man/gtfs_reference.Rd | 65 +++++++++++++++++++++++++ 12 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 R/data.R delete mode 100644 R/sysdata.rda create mode 100644 data/gtfs_reference.rda create mode 100644 man/gtfs_reference.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 10d4355..1cae2d4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -57,6 +57,7 @@ Collate: 'assert_gtfs.R' 'assert_inputs.R' 'checks.R' + 'data.R' 'export_gtfs.R' 'get_gtfs_standards.R' 'gtfs_methods.R' diff --git a/R/data.R b/R/data.R new file mode 100644 index 0000000..3b1a44f --- /dev/null +++ b/R/data.R @@ -0,0 +1,26 @@ +#' GTFS reference +#' +#' The data from the official GTFS specification document parsed to a list. +#' +#' @format +#' A list with data for every GTFS file. Each named list element (also a list) has +#' specifications for one GTFS file in the following structure: +#' \itemize{ +#' \item{`File_Name`: file name including file extension (txt or geojson)} +#' \item{`File_Presence`: Presence condition applied to the file} +#' \item{`file`: file name without file extension} +#' \item{`file_ext`: file extension} +#' \item{`fields`: data.frame with parsed field specification (columns: +#' `Field_Name`, `Type`, `Presence`, `Description`, `gtfsio_type`)} +#' \item{`primary_key`: primary key as vector} +#' \item{`field_types`: named vector on how GTFS types (values) should be read in gtfsio +#' (names). Values are the same as in `fields`.} +#' } +#' +#' @details +#' GTFS Types are converted to R types in gtfsio according to the following list: +#' `r .doc_field_types()` +#' +#' @source [https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md) +#' @keywords data +"gtfs_reference" diff --git a/R/export_gtfs.R b/R/export_gtfs.R index 16d69a9..c366fa8 100644 --- a/R/export_gtfs.R +++ b/R/export_gtfs.R @@ -71,7 +71,7 @@ export_gtfs <- function(gtfs, if (!as_dir & !grepl("\\.zip$", path)) error_ext_must_be_zip() if (as_dir & grepl("\\.zip$", path)) error_path_must_be_dir() - extra_files <- setdiff(files, names(gtfs_reference)) + extra_files <- setdiff(files, names(gtfsio::gtfs_reference)) if (standard_only & !is.null(files) & !identical(extra_files, character(0))) { error_non_standard_files(extra_files) } @@ -89,7 +89,7 @@ export_gtfs <- function(gtfs, # 'extra_files' is re-evaluated because 'files' might have changed in the # lines above - extra_files <- setdiff(files, names(gtfs_reference)) + extra_files <- setdiff(files, names(gtfsio::gtfs_reference)) if (standard_only) files <- setdiff(files, extra_files) @@ -140,7 +140,7 @@ export_gtfs <- function(gtfs, if (standard_only) { file_cols <- names(dt) - extra_cols <- setdiff(file_cols, names(gtfs_reference[[file]][["field_types"]])) + extra_cols <- setdiff(file_cols, names(gtfsio::gtfs_reference[[file]][["field_types"]])) if (!identical(extra_cols, character(0))) dt <- dt[, !..extra_cols] diff --git a/R/get_gtfs_standards.R b/R/get_gtfs_standards.R index 2cb0dcb..eca76c8 100644 --- a/R/get_gtfs_standards.R +++ b/R/get_gtfs_standards.R @@ -409,7 +409,7 @@ translate_types <- function(text_file, r_equivalents) { } .doc_field_types = function() { # nocov start - fields <- lapply(gtfs_reference, `[[`, "fields") + fields <- lapply(gtfsio::gtfs_reference, `[[`, "fields") fields <- do.call("rbind", fields) type_assignment <- unique(fields[,c("Type", "gtfsio_type")]) diff --git a/R/import_gtfs.R b/R/import_gtfs.R index 4f7a309..f561a34 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -258,7 +258,7 @@ read_files <- function(file, # get standards for reading and fields to be read from the given 'file' - ref_fields <- gtfs_reference[[file]][["field_types"]] + ref_fields <- gtfsio::gtfs_reference[[file]][["field_types"]] fields <- fields[[file]] extra_spec <- extra_spec[[file]] @@ -385,7 +385,7 @@ remove_file_ext = function(file) { append_file_ext = function(file) { vapply(file, function(f) { - file_ext <- gtfs_reference[[f]]["file_ext"] + file_ext <- gtfsio::gtfs_reference[[f]]["file_ext"] if (length(file_ext) == 0) { # use default for argument-specified non-standard files, behaviour defined in test_import_gtfs.R#292 file_ext <- "txt" diff --git a/R/sysdata.rda b/R/sysdata.rda deleted file mode 100644 index 255493c8a4e7de0e7308baa45cffea96cdcb01c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4078 zcmZ`)XIK;1(heYqk)VJeMS36-f*T;A8Qi54ssagJCA0vc_W&zWf`Al*^s;nJ=tv6) zUO=KDNEM_BO0UwCBCGr9-e32}_ntX3bKYm3Iq!MSkI}Znt1D{R%39h^C8=*-&7dz1^{9+1-Jl&*d%_oma>qIV`97SdQtfh~%@vI1h?Bx+z+4Aoo%TZKSWyU-FzY>2b_^+5>FPMRB5y_?k z>`hbJ(>xv!ZHsF&hdMgC@bq19E`wWGiz}CpR~VdwhJy!t1<5?5%RRiJ;R9}9vLc+s z1O8A@`A5*2+|%^4xHAm{*SyWV62Zoc^E6qlhQBk}bhwLx;Y6P(8I3BfEAka#!4_np z{)-Zs{7STtX1gFOy^2bBi;=(*T8h^=S%;eeC2}meGQdp?FgZ15uCDVU)@X~|QOlee z^){Z_eA}_pN9x$~p$&uJWS?=7Q-{vk)|ol~S;yd4bOpNMInLh8eL{Jc)#6wU1SMId zF2~)G?I#$v1>#+VI|G$#`~- zKOTdj&FWyahp!fKrAwS8=OhkXrI&$k7#FU~;g-3KjVp6=_2iH$hA)=4_bK041b|s3 zrB^Hw_bXVUt>iG6Nn!4e?%tKs&wHbtvs(uzYBpgx?Ng^h>IUM0%JzZd%`#ttn!h50 zTaGS$BykTbtKEXx%NA~B6RZvGY z3`ys-$zQMvw}JM0jRG?_%*?WlGc(HvXx|?Yy}qmDTQUd6+#b+u!)ductz&>y@J*xD zqXr%vOxKD}<$CG8O<{2XOqy*2QSe9C(;U4`g^s#5N;4g8ja;pq8OvcSivRglUzr@>1eN9KZ4*{5P({9f#!J4?BOu=`-KmRug4Y zvpT;z3#@-)hM%ojHomk#@5@0M3M~++9IK~2CkY(G2hD2Tss`>tAj`^3el*t6U(r8} zu&v`RChTdcyrZ7?Gj> zL~Vzoa)ORSI(fyeCMGz)`Oq=@V#V5+HpNWNB&D^X)$Kd}=Ibo0Hwir>-lXMEOxAUO zDGXMyml|2NTC>cw)I(3zVg|d;HP*&I*V9ijd6qZpvS#HJDPWfd>_ZX?xnUX(HQd?p zsrFt#*(V+)>{yA}9*S6D&P9(?+fcS7R3 ziK?3%YO_;xrPLSY`O~g8crL`5DX&>3{?89}t10|XGc>n9Z6Uq-xLhe2EjE6xSuH_M zU4>KlODgbj6Nt|5FPS0*+%U*)UcW|6MCHGE}fnAjrY_ymCuVPuFwrsopxp$CI@=?)quc+%FD}EX&LYTh48xDK+73 z${`D#Rm_)t_LJVByRVycO=08 zOD8KmHTG^5c$Mtk`cALau$*iNj%I*kr3{;3WLS)3n7o#qA+TK^*tDJ(`Q#A@7-yZH zdFSn8!(1=?N>kWxl~om$sjUzTwn!|erR3?Y|Mrw!ktE0o9M zrf(7w7j5ruJCv}AIA|tIa1z5PnAc#+AJr)NRCzyaZ@(A9cYRqH^-l-B`%urxe0b+Z z-%3}7YRFsVV0NH(+v}R#4Y~G)E3!Je^Hh;L= zZP5E|V?RHIPG5Vk;%hxbm?K~@Hc5DV{$pXKNN)#`JaC@U>2^aOu#XEj2UXQqSm^v_ zn=Lq6d8we{ZMzC#o(UXi!B$uT^O_%EzSfFB+fI6MtRPiaMN%Z>B*GmD=E`Y`HTu)dc20RLHY%@ zwF|d3tb0R^-c3Msu=dkx2*SWCm)FXLue=zEK5||nbEzO3>+>T{@d!V)YboPyL%=!L zL23`Js}s76-596^*{ip-U68HK^$F16?-^P_E5M zLs_dWwA_z#3vC21!gS^9>q`0*0b4DD9ov zQEP%e%H96I`-hJ

5b{^*q#A_-$}IRJ9!vS%rh9 zJu6%(s&{J6N2}gaJ#l>9dTc)&-@a{K(z6zF)b&~jVv3%zvKCnKy?7~iEOG1*oDq`= zJ}&fC9wcsnMR-)(z(<;O6PTxmMxlRwyJ> z&54YI92yZSegv;`6DH!AZH-(F3$wn6JkYheNs!}w;@0fM`@g)$D=kcK$ZIS)I1|7{ zpoaJSbt*%*31nL;I*Utv%tXbdCtgHp8qC=ABCWVunC#G=2Vz%k;0)SxeXJ|(DbuZ1 z2~UTd%NJ)gEqe&wQRhA{S&IITM?qsEwNjAqpHX{3XGi5qTMuN%2k@X1&NZ=&i-9yo zdRjEfz%o{35?W7Xxz~;|c(2KQUi>7Ah=nQgsEMh|uBb2i1Q`0Owv`}bgRgzePLj8a zjgJ551grq`h)@@%(GZuMm=H`;uTj0S`z$<=uLwh!fp9pkN!ILwb$nCn4SOprsI_;z zn`*84IQv^E-qiw3-eiRqqfG29m|~O(eb&oRG`1L_jUX0>% zHifOJ5SV;R9E9f$HT{$zH0V9 zsu!FOF~)*$s;?%>ro!)$$S64(Gwa`^!ZpSsFFhFkn$R~ex_4IN{?4n5e^+TDCEqV< zE0(gY8FF_i`IFuJ=+LP-%8b)k?7%Gak+?8=4_vST=@Sb(KwW?c=FiQE&P>l8|odOm~&F;o~eiirP zCmDl&44QO~q+B9ERnTt|Z^gWkeqTBKC%5$}+5EQX4ZZ1s&}A9b&T=2LD%pta-St@p z?cpjlQ6F?H&RUov${jN-663iM6EPClRaLLaf+On{7~s$r6dpC*G$~WYo5=D<;rHZL zZ_=xr?iZLHLk}u>1G;>9gAM6bxW18{Nv;&$V6vC%&_%cDfmPicJ6r_@ckLwXQwA|i zWKYvwmk{HFR>$z(Tmmu{d^F{+p*IQFD!c0atA|t8_#Rxiuo}L(;Gx#FHX6m84@9g- z)IaRI8>$`hB5`g{G?jio2bG0J7U{_@thE^JAE?ikFC&UuaOGJ>MpWuEXjaWr4?Z4E zdEm$HTcQH(NF0S zRZ=JCrXobombw>Bc}-Bmc4qA0)A|6h;7=SB&DRd8&u&%r&!Eec->=?<@!!+Ug{M~t zkadzH`7#gqu`uk~2qE=?hD6=7CxhfZcVk#jUgNy}a?#%z{hqh0rEDMu@#Vnx#i5qG$lsvLvpTLIU1Hk^T6vtB>(dVd&VHSH9Y`_EiZAjNx{3&iFW-^PeK+E;hvUx9oHWeyBk zkJ{xL=Ln7Q_}N>tKS!zenSW>fjnTxO=OAQ&(qmv_tEw;ED`2;_7PFADJ*Ww}Ln8qX zeS;uZGrQnbmR!0S=RGmumPz80?ZfR6yV8u8#mnl2m0}PJ^w?(&XX~*icg1?lAQgk} zzSRh-iR2O^H-=6r?bi4`;bv^@+bPfY3wKXC8{s=qYNA-QCKBE>x?v06ZDNoD`uls2 zbKGv4^R^n4tf}BfEny9wwSubfd2(>8>FvRcVKCfHH^YhyLSXEm^-5h7@IiI&#Jm?I z5uzL}R)I1LjauA8CRjf6H*?jYka(?~x!~yoZ$#5g5ls_~$pW*!SDOzSA~u)FActX{ zJahTt7h>6M6xuf9_p$tY-lHULT diff --git a/data/gtfs_reference.rda b/data/gtfs_reference.rda new file mode 100644 index 0000000000000000000000000000000000000000..8e32c4a096a52f64fc070115b15a6ceec54400d5 GIT binary patch literal 19276 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 qu5HsW4pR +# Rename columns, add file column without file extension #### +f <- f |> mutate(file = gsub("\\.txt$", "", gsub("\\.geojson$", "", File_Name))) |> as.data.frame() +# Parse reference file data #### gtfs_reference_files = cleanup_files_reference(parse_files(reference.md)) gtfs_reference_files <- gtfs_reference_files |> tidyr::separate(File_Name, c("file", "file_ext"), sep = "\\.", remove = F) |> select(File_Name, File_Presence, file, file_ext) |> as.data.frame() -# save as internal data +# Check file presence #### +file_presence1 = lapply(reference_fields, \(file) { + trimws(gsub("\\*", "", attributes(file)$presence)) +}) +file_presence2 = as.list(gtfs_reference_files$File_Presence) +names(file_presence2) <- gtfs_reference_files$File_Name +stopifnot(identical(file_presence2, file_presence1)) +rm(file_presence1); rm(file_presence2) + +# Extract primary keys #### +primary_keys = lapply(reference_fields, \(file) { + pk = attributes(file)$primary_key + if(is.null(pk)) return(NULL) + pk <- gsub("`", "", pk) + pk <- gsub('\\"', "", pk) + pk <- stringr::str_split_1(pk, ",") + trimws(pk) +}) + +# Create gtfs_reference data object #### gtfs_reference = gtfs_reference_files |> split(gtfs_reference_files$file) |> lapply(as.list) for(file in names(gtfs_reference)) { - fields = gtfs_reference_fields[gtfs_reference_fields$file == file,] - fields <- fields |> select(-file, -File_Name) + fields = f[f$file == file,] + fields <- select(fields, -file, -File_Name) gtfs_reference[[file]]$fields <- fields + gtfs_reference[[file]][["primary_key"]] <- primary_keys[[file]] + field_types = fields$gtfsio_type names(field_types) <- fields$Field_Name - gtfs_reference[[file]]$field_types <- field_types + gtfs_reference[[file]][["field_types"]] <- field_types } -usethis::use_data(gtfs_reference, internal = T, overwrite = T) +usethis::use_data(gtfs_reference, internal = F, overwrite = T) diff --git a/inst/reference/parse_markdown.R b/inst/reference/parse_markdown.R index cab2096..d969f8f 100644 --- a/inst/reference/parse_markdown.R +++ b/inst/reference/parse_markdown.R @@ -65,8 +65,7 @@ bind_fields_reference_list = function(field_reference_list) { bind_rows(.id = "File_Name") |> rename(Field_Name = `Field Name`) |> mutate(Field_Name = gsub("`", "", Field_Name)) |> - mutate(Presence = gsub("**", "", Presence, fixed = TRUE)) |> - select(-Description) + mutate(Presence = gsub("**", "", Presence, fixed = TRUE)) } parse_files = function(reference.md) { diff --git a/inst/tinytest/test_export_gtfs.R b/inst/tinytest/test_export_gtfs.R index 2de613b..44c55c3 100644 --- a/inst/tinytest/test_export_gtfs.R +++ b/inst/tinytest/test_export_gtfs.R @@ -190,7 +190,7 @@ for (filenames in list.files(tmpd)) { # all existing fields should be standard file <- gtfsio:::remove_file_ext(filenames) - std_fields <- names(gtfsio:::gtfs_reference[[file]][["field_types"]]) + std_fields <- names(gtfsio::gtfs_reference[[file]][["field_types"]]) existing_fields <- readLines(file.path(tmpd, filenames), n = 1L) existing_fields <- strsplit(existing_fields, ",")[[1]] diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index 11cd6b3..74c2e49 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -144,7 +144,7 @@ expect_identical(gtfs_fields, list(shapes = "shape_id", trips = "trip_id")) # get the standard type in R used to read each field -standard_types <- lapply(gtfsio:::gtfs_reference, `[[`, "field_types") +standard_types <- lapply(gtfs_reference, `[[`, "field_types") # get the type actually used to read each field diff --git a/man/gtfs_reference.Rd b/man/gtfs_reference.Rd new file mode 100644 index 0000000..6b12974 --- /dev/null +++ b/man/gtfs_reference.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/data.R +\docType{data} +\name{gtfs_reference} +\alias{gtfs_reference} +\title{GTFS reference} +\format{ +A list with data for every GTFS file. Each named list element (also a list) has +specifications for one GTFS file in the following structure: +\itemize{ +\item{\code{File_Name}: file name including file extension (txt or geojson)} +\item{\code{File_Presence}: Presence condition applied to the file} +\item{\code{file}: file name without file extension} +\item{\code{file_ext}: file extension} +\item{\code{fields}: data.frame with parsed field specification (columns: +\code{Field_Name}, \code{Type}, \code{Presence}, \code{Description}, \code{gtfsio_type})} +\item{\code{primary_key}: primary key as vector} +\item{\code{field_types}: named vector on how GTFS types (values) should be read in gtfsio +(names). Values are the same as in \code{fields}.} +} +} +\source{ +\url{https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md} +} +\usage{ +gtfs_reference +} +\description{ +The data from the official GTFS specification document parsed to a list. +} +\details{ +GTFS Types are converted to R types in gtfsio according to the following list: +\itemize{ +\item{Unique ID = \code{character}} +\item{Text = \code{character}} +\item{URL = \code{character}} +\item{Timezone = \code{character}} +\item{Language code = \code{character}} +\item{Phone number = \code{character}} +\item{Email = \code{character}} +\item{Time = \code{character}} +\item{Currency code = \code{character}} +\item{ID = \code{character}} +\item{String = \code{character}} +\item{Color = \code{character}} +\item{Enum = \code{character}} +\item{Text or URL or Email or Phone number = \code{character}} +\item{Array = \code{geojson_array}} +\item{Object = \code{geojson_object}} +\item{Enum = \code{integer}} +\item{Integer = \code{integer}} +\item{Date = \code{integer}} +\item{Non-negative integer = \code{integer}} +\item{Non-zero integer = \code{integer}} +\item{Positive integer = \code{integer}} +\item{Non-null integer = \code{integer}} +\item{Non-negative float = \code{numeric}} +\item{Currency amount = \code{numeric}} +\item{Float = \code{numeric}} +\item{Positive float = \code{numeric}} +\item{Latitude = \code{numeric}} +\item{Longitude = \code{numeric}} +} +} +\keyword{data} From 91cbc3d2a38c62e8a20d2ef27f3da07545a6aec1 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Thu, 29 Aug 2024 13:18:40 +0200 Subject: [PATCH 09/12] save reference revision date --- data/gtfs_reference.rda | Bin 19276 -> 19386 bytes inst/reference/create_gtfs_standards.R | 4 ++++ inst/reference/parse_markdown.R | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/data/gtfs_reference.rda b/data/gtfs_reference.rda index 8e32c4a096a52f64fc070115b15a6ceec54400d5..3cfab2d0452ab6c683e000114cdd8a20e1b93051 100644 GIT binary patch delta 18953 zcmV)GK)%1smI1n#0TM!5XgM)KSte6iY#AC+08T&ukrE?+{qXg8-+T{z?w@-1UvAv& zJ>%&2z5w>Mk4JBy01rT99)$twfam}U007Vdpa21&9zOE&`{VCDxC2Ap=5X8IeGZxL zC0#VltF!O8)IPb6w|(vP>FM{7SKIEo#H8jk*7IL|#V4^~@p~JPz4LO*Q2DI#M#uYG<#=CC-5!Wupu5Uq(UAXH?wg&X;sot&L z&fh{vl4Q{|PfaE?8itrt)Y@oFqcVD#F*bxF^d?h((R&iY?_{<(ma#Y^vz9ADYBoULF#!xl+Yl8Ac6?VrmA~WQzIv&+C$X! z0iXZ?00000000000000000003Kmd?Q5=Ns_^iUJ^8hU{nLqkm=f?^DYXiS)yFeaFo zg9y=oh-6?9gwf~(zyO&rnJ}0W5X3N;38t7KNQi)hG$un+(w~()XqY5=o=|L4L8C^V zra_^jAOk=E00ThK00001kOM#f00008l7c1)s(wuXnK1%j3ZI2B5cH2SQ)Hf|k4&ad zP#%y0wGF8C4FDQ68UP2VXwU!vGyu>513&8a>Z>60cVCXGgb z$n^~aKmY&(KmY&$00000Gynhq0ipfx(s}@g1Sn9@C{m$ALcQVU^9`z^OJxE?iu{Ot0t%Q14c{x9D{hBM2rYsR+7mD3w5qaxck$b*OIpL6 zUEKIl303jMRb=DLu+2-ZN#WGXadEGILD**W;i?kWh29>j+1)6eIDYwea_`T2%~Ro` zF}B^^-T2*ASMISIuBRJw+lHo^VD#Pf&UjZ0TN^)|jdJa#;`+cUA~JM16Q zr|IrDJ4n4xu{mzNSG8}n-nZL-ecOG;H=Ex_?|Sn+k7w`uTkpZY_V^vo2k~2eM*P}O z(e76MmN;BL)ea+Ze_r?Y`@g&1<8l4|NAKj&{vXGc@MnLYFPS&t{*T3P>(2PO{R}UU zqcr1mx_{r~>POZ87;x)#-7ZLy-KD+B<41p6(qN(4|n{80pft&izG-dWMK>iVCT{w2EgHp4a)JtM~jK~&{Ee$!M|ZZ~>R)Z}!oIV5Kdp|-8jb%SXo)21Z3U!y;QKSqCAb(O;0X3?=} zESb}fb^2TqGc=w`%^8)mv%c};)CaHj1i);S&DpPg zH`C|a6|al0o428VxenqN-o4NX2@sG2gpoQBJIHoI`Xq2ifq4cYlZk2B39<)71Hl2} zLScH91L~3*Bs_<4hI1i6XEw^oBrIf_jgZ1v9tNGls^etTZj{|+21s3!7h9czyZP~{ z6?BrHieK!B(&)TFLbaPKbgr>($$R*w6~t_&C1}r-hE<| z9epopjNpZ=Yw3p9yPU21Cl>9Gj{xcr%aqMIakvr>UUx@iu|#Mhg{q`!8mdBufS@XnrhzblT?*(|C{%SJ5afoWuI}gT zf4PI6-}iky%UE1;9!Z%QE@l?etF|s5s)w5XdzTQqwhfX ze%D{(T5uh44j9I{hJC9!@$AS&@m5QS$&$i<%(EFJs4@VSdW2kX0U zK3PH{NGRAANivZmLUv}KX=zqat|g}Agwi(|Nf`5gWsD-5YbjEjrO;VVRUTDw5wc-N#UFZ~vxhnSINTnJs89F50!5Pp5Rzy%rN`|ate z+(DDHriPeQ86@o3pdsDpZ%9^pbd`cZAUtZ7`7ufesge|F57k{kj5?^W$$IhdX90qO zf*)!|rv2H^ldoLpQAtC=>6jQ$Ks@+=p61=jX2B?%3I(e9a?NJEd^ei%C{a}W9M-$sh}KL(2lV~;14M+C9u zpTH*B=;B06a#`TBfb+Z^F={=eAoy{u=8U(pu6l}QxtZTa2Ws1_ryN)&VN5)KC{YX6 znhhK;2DOq))XYWo&H82C_#o6_w_KuDUAw$VHJtq|WTAapF_Z9k!*9>~MCtY$x(DS$S^(&q= z-pOUi&~x}R6nKom+0g81RU5W{pCTej`r2z|jU?Zk;N&BwI9I`Zb_bBnklBo;={F+D zW{*;Haq4t^C`3;CGbg`r+%~d=Kf2|hIrdrYitt*{lG~8hrS&8f9Zs(d*Hq_;cXI?1 zIPH6#OeP#mCS5;pRXglM!e^S!hVr8CZz|Ilo+QZ>;tZ)24nXc^%2I`YH7)V6;Y;U; zlUDF%H+9c8Wu>*EAm1?O9A?ZXcInPNZV_i9L*E9}C$bZ`*3&Ud4a~99F8O1kV=XH# z&M<_I9*E)&iHPO4&2Un#@w?`;7%OmZf*eYPAm>NJZ{v6g%6(LIm668sKx>}Z<{Zf= z&7$`zDu|xR4u%lQlp05qPzxS^M11QjhAGBWt|i!ja(&1152+$dS<$H?b= zFeXA7mtF`QA5_IaoLeE<#9Mp2p_#g7#<_mGQx1u8fyv_Mpl2kLyk%*^gxl;I0t6y^ z^ehqe=WpV2k?D+i?&qEO?|rIgdQ9jgK?Ad?A`ac2T`+K5Ku4prejyfrm?6(kJbF&K z9DY2)L?IDmL1|@-&@~%a7-BM+GN?KgqT#J(Jv^%wMJ14Mb?BnUgwbgV6m(9i^|nH~ zbjk&V3ueX3HJf7Xtc2}3PMIV21?Sph*O1fZwp`bO=fYKS9tr#t>8?0UIN6qj!by;Z zl+DPv^?CJSb;h@>IGG=RZt)&{PiM#b?m&{*G0I^#mmA7pg|y#BpAJe9j$?yFydDn} z_sjFftVL6v`Uq#|<5c`-%kb6tnARxP^*ZXWNg!q?DYLVwTrWZJO|>vaPbnD>)y?{J zQN)IdVb>my0u!O!kk$^w2t|SLb})NoiX@aeZq$@-f_X=?aB2!v!?0_ckDE^5bji2s!-46SW8uq_ndKv}h$t$mnk(Wy5yP{b zGwTP^x8-MYl0?6>XR>}6f+#mE@XZd= zKsy}}I5$dAn(+)?a$KCKhiKeQ@Ob(&uN<8B?)}ez5E&8ap2XFtx7+8GW6@kXWeEIClSvRNe={5AycAYl z7-4f#LNGbUM-DcpH-WiKU9@_Wx*gq!M&sI~EP_v2KIXDx13nvu_NqOHpK{~W#H6uZ z-mdB7o_Kh)9LncHf;hVA>t-_QGR!yOlPM8Kf4H+5gp)w-kUO}3ET0qR=E(Md<;_?5t&1Ug zHr=H(-%z9;M}H1GdM|DTrXW6O`%pmdgl`_cIf>C%)!jQd)*lS;&)EoU8ArcE zi-h79FXYr6l-olqE;kJ1{s-&RR9jb?#)vMMwY3OJ2wp4F&$_I`G15IW5Gt1Cf0Jzn zyrw>zE*ec4QgfT}$1xt1HEUcjOK{&b&O|#nn5cdblNOunU>hVINt90-xp8&zzg#kE zO&&C*V^GJinY<)+hZY<$>w~E^P~6!XouD1>m?Xl-qsv)kct2yjndVa(byZ0)4z+gxbt!?NL!vT8j^XA&p6Aw%;|7aW%~ zkv@Diw+-ajk6G7k^E2Gzgd@|-e71Su=!R&-KzgG{f1SZ@aJusw=kf1df1uhI7$43` zl!p_JC-v!_iODSfk$mCevkjKjo|+#mY*?(AXwFw+2JyBBr+_w!C z7t)#$*1r@+Q+l@K9VfQ(^V(}rAjb;<9~&KuR8ThJ^QVl@fb2*&ZyS)&L3-*Y$y?DA4J8@BsiJMO77rvso1A{;Fviee`_H{ z7NTEDd1{WMZNQKcw6SheW4F9XTSVp^t~Ok7 z%>wy+AeOgr$7Wg&5;*Yiy-!K;N6_gQ`aT)+mD`ezUD)M9Dg`kRhxO(&EoL!Ee2Al9 z{|FlhG9mMQQAdj0(Fu=6RJvr+CTxA3cF|fqh(bvxY$Sr5e{~!|DpOf7L`r`tpSEf- z!_cz!`?vgc9`EUc8J6Uk>Zn<%cyB#BbPqyD)I-m2slhlUr&e%r5yg<8h=)mg`(3Ey zqlj{GkjLXO9Q+PEls{3P42XgYlKkPrP*T;`D8`4 zsfp_hN~$EXn)VM;X15`*f@iovt;f2AwcE$Q+jcDdX#nX*v```A>3zpGt2 zS+2uW7NE(WL)<3dW);*w406B#_rkuHo3p#m<=e^;5k$~QQ3WtmzA>FTbl*u%2h@HQ z3kv%rJfT=9WJHk?(I6n_u}Ey^ou3bbU%)#%XJ?5!yaj7M7S)J(>%+FLE^h1cy*sbD zfAaYBHpw=JC9z(($Lam>-Q>=rENt*fu}&fa)?}MD?4XQgLKF&F39c|qi|)(pr)_Rp zzQNZW<+FtkxJJGw!>r#AX2Y3zC4O?;`b0{>qS%Nu{k$~j%DVWZn{;Li117TRtZ7IW zwe2^WJ!LgSR0ocOIw~0!@h+S1?86UAe>@GF=gsfEl4wFEsZu10P^AJAlB9|#mI(qF zp%OwxkOK0kH~cZXMkB!IoE@B-n)$Pr#@#WMEn@?>RSN|+DTX@d5mhi0@|vx91Iup+ zl(ESMkgj}`_V@5x`qb_B_h*AxIDdHi_b^h`O6Fx!ZX7K-)6A&FK=p}lba0Epe;=2| zyM0F1Jz{Yx1+Ho{br0_P!@94U%hnvjuMy&h zo|d&zhYG?_((9-mLh`2FSYe=PNw9i6gj ziQB3!BT)Y6x=pGe`E*Dv1Ju%~DPi8ePI6r31?-1beh7$EEN*_1shU|%Bw z@e9@s^uaVRIv5Z#MdR!ge{=N)Or`$#)a^GW5ZSbbl7htd${gE`ZYQ8}aUC|@1k*(D z=z7q3yuub7{A9u=Nv*jR-qF9Yz~#BaZH9`v_Rd@mE_moW zL23q(A~Hx5Z9YbMai6Qm+k)Aa_9pUheomyqW(lE8E72rYHy_aODw2G-aEw zmrH}VYL7&XnY&Gmy+T;GbYS#7_M?AgiYPKQMwmcg<}oEDP{9;6L=d`zD1wC|HGw2Z zS;r`jq*8-*B|DQL(V!?YOoK2nfO;Ixo~KAAN)hbg&L#?b~~&CBdUz1qtO*#l4vH zRl=rY{KN_xz(5F-<91;?`&w`3YLt203!rW>w;xA*@L$)z#0PD&iU9dRJ%20I6Ypti|oOwjN zeTCBDZ}i)tn)pI=H6EKuO{F@DajFx ziU4Y`A8pI%VP-BmSN@KDo^{Q$(o7=Jssk^j{*>7-<>B+oO7ee~4W)m*vV???BqT6E z7q#egh;;7!BK^2KKG);mc4u?@9PQ_qrT2PVyL`u4dfsh3S{)z!xYJVD;%^Azdv4%~ zd=IQd{y1p+E9@T=5+U-H6)gcx6cj~ELlA`sRIpSBrlQC`*dS(^uYJN<3~}Zj;Mey{ z+$=rj6hC$cy7Xxc^AtuHY;e1`6(5_P4*tkR#tSr=&Ei7VolJz6vH|K_*V6Lq$IE4b z_MYe6@~!Sd_y>RJK%xr*G6SGUc6^>Mxe#N^-n0YR(m1CHFRN`{V)95g_H(@auIo8K z&3&6_8c`+-nc!RjbI`<2-N^|~;=@B5&r#KXHR@TeG(w)45C)w9KS2~B=9DRXK&TQY zV5_zMcA{a-pGGUV3f=go6Po)*LJ~j&Pt2J$BdFk41}|_@5Q&5$k+?!Oz<~jfiKsL|E&^4)8}MwZ7XnLUBMoFh1CC|djv5Hafez+`&5{Tk(jpj| zCZI;yr=b-j>ASWkw7TJhwK7C1P{|UV8_POPlPVubf3N_Kj2wZK1hkkO?cL~ghy=(BK z3i#07L)B>^=1Gk>O??z5a3$6kdJk=`tDeVMf4=E3Hcw(>rIC%!ttt{BC#Qi!f+OfS z(!W%3AKR{B>-W>*zO2b!T&WCljk1ETsW!RAELPSP)5NHv#kQNIRugQswyNvu}`iqas^WYIb#raCg!+g*7)p<_i8WC@;s@RVhdnRaDS{Q7Niufpe#S z1CJhCHg)u-{NR|w46F@g%OV=}Nz&&~7ezQlVkOa$7(xXC9Fh(|CK3T4k}MWE;xQb;UFP<&hulA7se5Ezj^COXLv)y5Ob zPbet*$2u#RmQflXV8zEtm}gCOybGpeWFnm{OR1NSO19?&*0 zZKpIoG&EGWN1b~F14TsNbK|fM+uz(B=nsHV1ya)MpC#B3tRF@z2tu%)e^7pOJ2jSA z5N?Q~jcE*qfGNWv2u7)dH1&vj>bJ|n8^f4!@7aVbLNX=5qG6RoE4#%G?!ekkHCJ>H zmQXqf24aj!BngvyXJ^#x-qe6L_my7hijFu~@#Dq7XfSsfW$|AH?70!Z)+=yV~Low8lf4epjlw(j1OW6(J zK|NeLEUrZX>f_QD5#LfmG7unz0&fQhl9y8C!Bjv^Og5Rpwr|P;ebxo?qUKWs2eGcL zqi5Tfj|n6@vPrW;X9wdfgbkL~oOKpjfXaii9%bYXIqF?p&QjM+miGchGC_5SL|&k6 z6-(15iJ{uqP3UB=f20~WGg^U)B%{?C7RKjhd;?*Il(059a4tM46V$>HraebZV$q@l z8g)3$5N^$3#AJYMifOuw$)E>mzAJQ5!yRh05-3c7L0)MY$5<*!A!M2%-O4qkc+4gi zh{0A)WQ#&!gNq}@n4<2<6}Kwt@N=0YK-z|Y3^5IX1_5Ore?d1>ib@Z5BtUzX+y+x} zwP-b~EGG!CBMLHSATT|TTMd!6$*tH_5j(`dNK6A>S$1q9X<~|o0tPGP#x_+qg9vXerWOot- zBdCfLaWLwce~c$5AwYO$5H+#p7t~Y)E!&27Zt%X;JXj&65WS#BM&+XO0tpCM;Pa2PS@}Cd;49nEJ8-}Q)A7MJn}i4=MTt>q0wnYLLg4wp17sS< z3Rx1Ml#Gnb!Lke~%NT%|VMYm>Xe5q6_=@;Y$(-%2e+`DadniE2aSv(Efb461G-TSx z3M7+^7>3}$_66Z1@WoIk7eVh2rhrq13&f*44<-)Octs=!xNUMAn+|tlE-qn$0dCY* zl{ZK@6waPTOZbRFC!ST|WdsPEW?&tL&?tmRAaF2l6V*_U5#sOK2kZlZ>;xR8IEqKgwF3c}VVB1@(~5Sm9&nLyJ)Y%Q`9 z50+h53BR|D<1urM9>B0}2$>&w-ip1%Izt&KstESv$)rM62vR$Y6v7ZmQcdCSNW@|U zDo+UdE#Yk6n|!jGVo)=Bf>ERmP&N^mWH`nof5tiR%?9PAZD4ibF*PSfN+u#uGIS>n zfZp<*rIsTlbYqg(aza-aqt1f?tdWj3L5|B-GB9>zjW1tv$Nm0z41Ocfc z7Kk$uu)`#A&YIMIYw ze{66~0Gc|Wi9%3~B85afHY#xw=8Eo6kA(Xuu;f}6y|I@?yOAkgQ7umqX9CL#Fc*;Y z!lNXW3XP@)IWaI)3^4dtSFLJ8nq*KSZusE_Cc)7RAp@Ao(US{Eg*Yss!llig9t#F_ znbQ*!bJY)U@xeWWlpuv3`U9~;l2CgGe*pU6NQ4Rq4h23D)HA&AUJ=4*3LJ#UUjUjp zknJ4=c0Fy&9H!~n$wrk7Bf@c4j7Do(c&Na|$Q`F1J0=I*fOHeIsS~v5f|#fA`;> zz+o`EjK&RyPq*_M^_0E-i2Dx{3^PI{LMEw0+ucDlE9+}T)HSUx#0>>wky|Jz?iHs( zow<}K&}<~PFrlMR++`S&y4>8{(YZ(vp}Dbz8m8qHHZ)}nBsvY@IN@2N@uD9+LNk(o zPQpiHGG#;`betiQIbWZV!&q`xe=z7nBnuBcJ=3#fL4*YKQn65TqXttX>8v&D8I5jo0h`W-ak|C-{e-I?fIUSs4 zL^RW};p$|FY9mX__XEvb@*s>oFeYrxobg1Up~9_Sb}-V41k7y!O9)rM#*DtDRw28x z1v(0Dl)&3zY~vd>ZH&7S(6eil7ruM%gLv0j)}%E@RE55;Co0$?~xv#BoNR4q`~a6>cG4u){u5V~W_ ztWnkW#rB~l8o?wZd^XyRb~|(yB%CnF7jJfN7eh*B*oG}Jn`I<|e~!Ga&LlQ2l+qV5 zRBaH@%>e=1o)I{5(Az<%ax%^it*GaCs$7vXiUe6P%FbP~b36~B`B5adjx=-NXMTu@ zsp3aG=NUW0h}IfNaL0p28PVb`wHizn)4Cd*9d>sRj-Sf&tz%C93}+e&OldIrPS7Cg zj3w|U2X`o6N(f_ef5H%v!^?XzXm$|GILBKzis{k`L#AfD9qeOyLm^YrkbzVV=O#DL zJ6VY`HF4i(@M*Bwio&~mwFbdKgp-8whi6C~#85p1=UylKYKw|_c(alAv} z>Dy|=Gzp@$WAHm`67*(haXeWJYaB#;TM7#{A@v13Gm1@Cf9q%~7*@2Sq^$UT>~vVu zurT1pNCq@(w_?{wXc`785)9G9dQdBXYPt!sU{^s5U6oTZrPE{^DCSZaGh{+wQfpeu zQx=|{G&HPlOx6k}0fa(^0%ZzJ6eWV>Sc#J&LP3ZG5oWnH4}oA+7W_KtqmiY{7}#>^ zhU0AtF-`9Ke{QVWQdUDms8NX8Zm9EEK{kH%;G!Q}0d_fGe)#zHDBX_*9yWF@o$L(2 zI(*0KjWP8}`?>>9FLAml*RjxqU_g+EC8Y*GW;7g=d#!mIS`%_7AStbtNmjVrkViFw z0+5NA`;V-0dec-FG>VxABFsV1WtrGl@L_KmxWCQfe^_G?=$fB7UQJaWe<8H0G3TaZ5G5KWyGov!5K zQA8+(!doi=eZ4VyaPy;d6i7{xgv(S$V~&ANaKPd@_|LqQBD_00Wl8dUqS&L0&4o6W%2iJcn@_&YMp8}D0*lw<{XRK>?;sh}-Q0cC8vT6ovCw0d20+)_f!;^C@VYsVT{~Jxe;vy8 zCJZ6od_ifML$*{~90?7MXM)Pe+NY3%AV>)8qp7}UKK-#|m}=2wSB$LVB^O!2TzLh# z3Fqh>cTn14n;0By=#hQuf{D|ul}-=l;B?eAHR=idCoYO~(hqzu5o~bdbf88XLr|E( zMP@SGPI|4w_p8caa**+^haD~C{l%)x^^ zhs;Iv3dl!^#FMHToW%nx*c6W`sS1#CKTi&ZB0KWx6wWlf-bnD@UXYhTe^wAErl^U_ z3O6F*Wz2Vx2P0gaBmj*MnJWg?w%$qNWsrBFG82KXHZ924TSdEkSYz_SFCc$hA?wbj|9>svYyI;qIHKl9spjDBdEEe*;(_ zoK}8F>t_-|5eh0YKUiS0w%cQBTS6HN(LFq6aGNUkO@YtbFc=>ce~xkokV2qQ4F*#4 zNoaWHE|8{3h~}aSFc?sIsOKunDZ0rimy5Ju46iG!mZLD353ml%Op5lu?0O+f}@?rAbe zAd-NgsGy3PX>DdfK@<^|6GU3aMr%=)FV_7=fh}HCgSsorp#iK_t-yB&?7-c3|fV&`+L`Gr_`q!>9%fxdB3v1CMV-&@BK2LLvo69^?w9 z5=67-gIHNgRWq7<3UjNH7u51IiYkfqb!ITr6S;&Af5?RjzY(7%vFbUnZm)__zwPc0=Pz(~786=xL!({VQ8m4X43S{%eUe3RcIhj9=`+6~+_QYiGo z8j%q8AXJzX+C7Ana;^lR{bAJYjSw+wbekXnS0w3$9j46jp20ihD%BKSdAD`t#gs;{VQ zWg*9}dI9vo)P3;6&`l80NHHxSG=LRKtrXBue?HfdUn$n=bfP^YNFn9!EFy`eGHFks zHHo4Eic=sAlL;3F7eVy-gTTRg)XqrY5+0I_zz*de-jSH)MU9a}IPFN|q4m5I;E*_o zcn6`#)`)qL?S;`yB3u#WP|go7l0ytgbQ~oUyC>CuVbvNEI?< ze_=Q5(Uw~}*4wUl`lXEGg#^C{`AXwycq?rJ?8i?soMEJnLFD)`G!BQylGAT(Dg;Z7 z%;n1HprE!voY73}ziG=xbKKqP?%Ms<#d zjCM+de4bW(#wdeY`^ef@C$10!+QRrPz|jN8?~cV&qUvU0j*;N5S6a6&qI6P+PdvDLQqggUzB>KQZ*f++7(w) z8DC^qlUC7;#sHPiUBi(Lh$IjUdII$)a8uo_7L;5lG8l-#HpLTQUBm{GBPC-stPe(V z0~%JsgCdEjH5gW=NftS3N+G8Re>WN+?Q|(g1k@A)4zjE+f~wF40i16$EQ>JTcEREw zbopLS6+Ne@CNrJ@^M?um&^8wa4%QH<2(mj4kH~_kLrNbP#qo9~E0;w=hGtnICP+$% ziVE>y!c`1_q!s3J4HC#6Ei;_iC@8ljl~6EKIOW!v2!b+b%z;UzrLARKe?^R=Aq*@` zcPMSGY6V*vs-~Kzn*!OVsctkhME3Arf+?yNgcFz>ou}R?Di|DacAjvM2=>oFepx=z z1`t&gh-G_GPn1;~8^eH|vO}2;*>^{EP>5i3*0{ze1jI#1gt0QMf~{pu6l-oy_@qWf z4&;2vU0fM;YzBd#Q&Q50eCc8oaF4I)dnK?9748Y7W6buk{A1IG_Yh*-+e>HGHgh667HhPAK zLrnE}G&?pxVS}=YRmWq%8utzZM=6p6LFemzgfOeWBLpG~e@76eIo*Ic19Fr}u65Lt z$w5H`4{Tuo;8Sb?hGb&Ifl@cru^_>fq9KkL!YGDi!f4|hG!c;7DN^bwgRT=IsEz}S zN+G8x(H@H|aG6LqIwmw03>$17V0Ui!7EI znSte;Kq*KAr%VIsUU<@&b-L(!ag#*RkUD~GFax%#LX{wa7_+7_$EkyeqllcM1u+FX zfexq;N7(xtbEE*)1tw>ouK$7^z=7ToJHWu8CW_1)e^yBVEvbPCz__3WkRLP|I)o`G z;$3PC10nU`Hwm$@oswh*l|vm879hZK2bPMMBBZ1s+KNg%XAqE7f##7nPzf_ZL{#}? zDG1yqlsljR@K0fo=$w|j`&WJhe-R#gbf#w)$YfFsR&x5>fAyC8%mPJzbrx>9PJ_Q+)abeM5 ze+$4?&QLOZF+LFyJ1Zx`boT>uyj+YR42vQl6gX2|v3uk-a}9D$vW#w&AvQ@Q*`mQB zSQ4$q0OvJ;GKvPV6hPoJg%rdJ0WlN@gfgBZ<{uG9jf;fLK{y!>S~NmZ!|0dfO6&;` z*TQl6W}>D@0^mM3B*%!s&^&yH0tWpuf8@aeAu-rwQ42{xM2kbxQ8HwZg!@SefY1~^ z?xapnM``MVuuOpJ5C}mEG9U|1w?AR6z&G7WiD8h1%L8c1-U=k`aA&|x8KET0aF|38 z5#-JBvf$Yfp^$+%okb5wMvd^L5TrTv1sZceX@V{owh&e+|^^ zEL}t(zIZgiMS!GKNs)(9^paDO834gWBDAC7O41T&GtZvzh?)ojk%(n!C6$Svph+SK z!@V&{EgU>jy;Z0W6pM@UQpUNZtF7^5?#qDq);Nrd5<6alGlv_fGf z5ywqw+ZApu+3JJHW$1SFdWh9he=HFmA_sxmU8-mgyh3`MgzXS%W=9?+2NE732gE$n zwpg095RxpO$8|)?V~VD@xlK4(BuAFfQ8bh^9LPtLbV~;aa7tZS6&!p;yr-Z>LrwBV zL687~JDiyS*Ha{P#At~KQ&OoRQArp>d?x2JLPUfDz(>32g`WiRkwcgsf1QOz2ajX! zNUKUFCb@#F7Qhacdm*o^5D7bI(20dez20Ji^wYyQca3ZuNM}vV4tnHIA!`OiITd7d z{EHn>aSV)N20=ka86)WMrTn(#v6%>X!Hk<>p<*QL_R}j#~_~|!+#;i$arU*P9*ig0>}y70uf=xOtQol zKqWXe=_iJfPo}oVI0r;0IttW`Nc=$9^e+Mq8(|`3e;lz#A>J))e=>oLfMv*QDYXeC zCwPqz^K*%|ryNNYg1d$%6iWyUFtU{J7K9@PC#VtTZY3q=>q@|3ig|MvXe_}s0iqDX6Cy|qa55##!$nqL&{YB% z1{7K_3@FuRMoEPf#ROq339UFG1FHfeCW?rfjEoqXGg@^Me=0&$lmSZTC7VgLaK0-rE~hDeF<#&f210`>zSza8{}?Xk2DU5rr!qtYA7mbDN9lBq++ zc)nLtsCsy+JH*{VlZenBs~|n%OhE^JBp5*LaV^xLf27L@_#PlD5VgW4)sU748Cr51 zM^Jhed1G1~i`of1q>C=a_U}R6-}L?F|LYB3V8hww}T4yPeN4xxP^$3}-mE$~DC<2J9$S6fk+u z2(51@Jpt+VeegciBrWs^K{5prz&8nr#~7An0bEJBb%r2OORZqIkQjt$*(D4zge(h& zG=&kSrBN;_*P+-4#D}tU0)Vk1s9RSEtrx+Je_=#8;H^W~8BVpSeC^F15SxA6dLEbFeX$LWM8g+4ppARC zGT1=xzd>#oFH%D!-szc)=0@Q*{Un}!&n<>nHYU^5!}j^&SdFK%Bf)JM0munA_J1TIAdaZX2MEeyb1upTbj*ZAF)LC& zNyl@N?A@41s%XD>G`y2`gtRV8REQxm^td2HK$xWN;YpgM_o4Y&P)otBA`EuyYy(En zY;+>Lcvlt!Bq6m^c4E_wlsJ?|#Kj2AIk|8kSrD4mJMJ&l9+Rq_4EgVT1CG>=a`}mj zJAYM3-X+zhuI>UEqdRh4Cw(X(@qNSUaDLB9pWtzstp%PJ6y^^X?OaCk`6`UXEIXc^ zRAJn7KCvFLRQyv!)J42%s#%or%{fixl}s#M^rML=*H3C48`KbuJ{v@`-m`XW=_i6f z9Fm68Uq^!yWS-LpB?t&4qE=Yu?(EG+LVxQV(H7OQ7flKmNTy7iGR1XZZLPB5cG$$$ za5#46@KHQn#{1>fIt~Jw7@p@7FH>&4ZOBYm@nu4dv=+P)@$#W)H;|>3KbVWZoEHNMvN4&}1eAzDkmmt;9|8AZOzjRsrALhv=zy29pv7Qy03KHh0Q&&h_h%0DqNmF+LLWhe`-C<6 zVszBT zJrE1E1DUv0hj*%CbZ}Vw@pW%-#((|-nIQ!U)Z`|Dax>UAYQUlb$fUq!GMTXfG{Scu=2Lhs<>X=L# zOa>Y(RwPGM*?EomhWT>1W+nsF1KIxJHN=m_SXC5r6FmCWzUv z<+2{e7qEak`l@}M`pRrp(s4Hck@9$4#usCRMgk`Htyt_`bP`~5iSStOzCS|7X)>0WZZBsnsizzVS%!#v<8a2w9n1y- z8#==w)DB_}le9?&=gEhr{M61I2ReQvu6Q2#%o3l)n$r@6Ibcx8(|_h*L4&hJjCHoS z>l!dzHE5u%!DqYq|9V(cXKb_RC3HUrT{oSR{8boRna%ljo$Nt3E-H1%9t@F zZ)Qx{DyhL!SAG^a?|=IV#Z@&!ViE+Chz|#}4a}Gz4UMfPND?jz6LOQD#)b+dQ&ngu zhsRyOOgq^GXltoB3_hp>EqvY zUGj!d)YykPiJaz1MvDRnhY0U9U_>=7CZNb7sD(lYs(2PHXn(i|BA7s~+-hidGEM8^jIr>j3I_ zA-Ek9;^Dv)8i*%i777~VQlLbLM>4xQrpGeQ4UF{4@7B(~ngPoD;aPhMo>(y?peI>u z0Y-&O%A7_D3?@W@$R!#y(GuX+ z%cZp(il`TvBt}6{49S`%dkQYULBeo^Ifl*ev(~4MQw?Qc8i=95=R2nO zuVk^PU4Ot33wd_Q%Pm06L3sCyPl`bKTWsxzsRVoZg%Du%A=>%8Xos5idos76Aam-(U5a=W5jBg?Rxn`VD?KyJcfrUpq?`>5P!z>g|NkS06avLPfZu<(R_Jf(8|!{ zhuX;~0*scIG=M$9@I(lBlM$t+LyuTM`{+bM9im7BL~S#jjT5X3|8dkxN~&uR82xDh zpyE3SP<-qGJiQS0=>xDr5+I;`cukP~b`T0cGJN|}>bM7B6<~<%fvh4FkYumGQI8DB~cf!;8lV z7Gd?~Z;n)XGH7C0B_$wf_$ zp#bTafebk?MltTwTL;F}PLiJMw(t`k4mFy`qpkrhg_CQk5ZO6Ah#u)@)cJr6Ha{r( z2fSDa-Lg=3NUKUX0=AT33&v=)3Z;^f%JlqkQ2?QVbF%ir8^N} zkzo*lQY93D85kapL0P%w6QGH=NZHbx0I*)tBZtGSZV0hNGlR%V| zzO9s89dLt`FzS~Z<2SQYs1{V{X&oej9<=mQ!kpkvDr?1-@TJYp8}FDE>wE?}(Tvz~ zrxn_&(yWH#D`5$c^5xizL{-ij%BD>NyHX+Z1Tm+XwiS+qJIseX!l2V z#NjstKqOrSV*!$7W#};^D_B?^H!4cxX=}^Tw}T*uwkrW-MH|I!i=1y7af}SwUprw# zSncF2#xxNksQCxRecsU6(Z+WFCOeDkZ|KV2F@B)UGzo-%G7dGvH#}*(`1Zqne z7*9Sxj4Kw*Alpv~8)yHuGDk-?&beihK(+yb52+f4xNY`1XCV&Pg$pdKy8!}f0@7Hm on3OMN?h1s;Ib1@dzyh&CSOQ8UC2}snihuaKk}1N3gJ8(gh!sy!6#xJL delta 18850 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 PLOO2!jnZ9f_ZKBLd2--{+wSAg^uF`$h-`8u z7`fF1Yu&k)!Uui&3vj`Dk{KrVOw4C8Hwm}tB=hchY%Mg!#}5QD(F69FoOvPyM_O_^ zO#Exanj!U^5cJ8WJ&wV#HlC&*x6cy9Z9SPD4NB`UEv4Mh)>C6Ov}U}C(MErh8EP?V zN69$ua$TFV2^CEj?*^BWZqSy6$!d`VCSI2W2xt=&o!luiRKD~-D=GFqeFth9|ec%h21WOmQau8~Gteg$1u<(0+U{4umO0 z(19UmN6k5<7`BVama;f&B$+T{DxTx2`JOJ{1ZOHJA{7dW!UaJnBT?XM<0Y^-oFXf* zkqaH+7^lm_o-h>dkD?w(_>ZiA1kTXpI#hU3O^^t?q=8Dn=&GD<76J4FXW~u$p+!#s zT0$J*3pfNdI0A4K#mJ-$RU#v=Kz0j>^VvGGYdl7TWS2p1 zJp#T!;k=5TW6J=$P&k_fSax~LPEHFSnl7#G3~uZxk`Pdw&e1dzxZ}ouIyx|60c28O zGMP-+kimoy96WR33h@DP6NqYFLsY(RwPGM*?A54#`y98r#(rcDZu#t;mQY$=V#qVpb?-j)J_m$VF5Nk zN46ZABW0m+Dh>=60s!HE$f@P5-Klw3$%%jlJlQEjK|-==Ha4umr*p2*%}{fk8Ol`- z`f?E;OL#m$Z=si`p4FETsnlvHuE)lZKGJHZ(HBHJpCvC$eQE*HD*Q+^MnFCx7G{zN z2ON3KAG1F@@NtJDservj5 zNr5sM41*G7@iOE!X6%p2H~JK0v7Cj}Iteg2#P}?C-yf^7noOmp8;jY?Drv_7)?uO^ z_}nqFM{@yyhR(3abpx1#r0o(xx$Oj9c`{U#*7+&C}N>gJjrFL5?U_>RuKf7na*I0aH0>;xL88QI5$nCdJZ~3B$P1k zgfxf^rX)tR)?wNmF)-teFPk1Zb0G_;sWTZHUEIs})f}@8X@Cw7Rla?C)pSktBX_*u zLU<-0vZe`1^#v6IcXpB18(V>=-}i;XGN!0(LO_ymMBep(fw;+n0NPs6WPu{!p*Iwq zwlpwNDw?Z7JLAvqpF7w+1CiAOQ&xkFkq3$$?GV21?njOqfwWDaBe>opNpe9qLoh6e zk|pU!E`AN&XuW(P!W|8(&SGadl2M|-f+4~?jTjLPOG&6Q2&y3vfy*5U*^+^5IUSIJ z{U-&nOjv<`D?=7%BXa<&sY9oNKkJtKX-*Dm;z9?J)wq9RCQ zW|$#~Rw6`*gn@#hB8sP=zQAc02%BMGQqW7q5~y2&jCMVa%7CZFA~$q~ zO)lv(Fk&E!Gf2R)n52qmds2qjC$MitI!BA?*(Gu zNgJ3E4Gnp? z1K}DSr*eFvFS>V}1V`(PQDjY1if^g~^`wZ%A_0{$#LZzwh1#akv=HAL7d-|P?!uGK)Xu{2ta{HoU`N`1L7cB_GT{tvq%DI9HUK$^5Kj7k zFWRE>(Su_vLy{jzNk9~2w7H}K?GK6|L&TVkEixQ>f(PJ&A`t8nKp6aF&3J2AM!3Xh}Ripza&9y#O@ah7r5gw5F z3CY*4G!b~yq6m1r0w5?UVo-)a9PxdBz4g)izhE&{5k!#~Z9u>vJnUUW#|jGoZKc`f z^nugJx(3C-VqHodoSU>}Z8y|rB^V}>1bKfXE;wsXPpWt;>&na<_zN# zAque|g$ap8iQ$v9Tr?(VLzo#`2uz2UF2q_Qu5i{>GH4yzkq?+6Cf@nksI+f1VHIPA zoa-jghdD{&7K=2D7T6XDnMVZ pZPPFgQw~(zni;ul&a72UgkKPgYA%8 split(gtfs_reference_files$file) |> @@ -97,4 +99,6 @@ for(file in names(gtfs_reference)) { gtfs_reference[[file]][["field_types"]] <- field_types } +attributes(gtfs_reference)$revision_date <- attributes(reference_fields)$revision_date + usethis::use_data(gtfs_reference, internal = F, overwrite = T) diff --git a/inst/reference/parse_markdown.R b/inst/reference/parse_markdown.R index d969f8f..463a17e 100644 --- a/inst/reference/parse_markdown.R +++ b/inst/reference/parse_markdown.R @@ -57,6 +57,12 @@ parse_fields = function(reference.md) { } i <- i+1 } + + # Revision Date + revision_date = gsub("**Revised ", "", ref_lines[3], fixed = T) + revision_date <- readr::parse_date(strsplit(revision_date, "\\. See")[[1]][1], "%b %d, %Y") + attributes(field_reference_list)$revision_date <- revision_date + return(field_reference_list) } From 92c9d34350241e50c3a53a97b8c6598779a21240 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Thu, 29 Aug 2024 16:59:55 +0200 Subject: [PATCH 10/12] use tools::file_path_sans_ext Co-authored-by: mark padgham --- R/import_gtfs.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/import_gtfs.R b/R/import_gtfs.R index f561a34..cf5f1c8 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -380,7 +380,7 @@ read_geojson <- function(file.geojson) { } remove_file_ext = function(file) { - gsub("\\.txt$", "", gsub("\\.geojson$", "", file)) + tools::file_path_sans_ext(file) } append_file_ext = function(file) { From 90a0943f39020ef8fb93519ea2c2a7ea06d13f04 Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Mon, 16 Sep 2024 09:43:47 +0200 Subject: [PATCH 11/12] fix append_file_ext, increase test coverage 100% coverage except deprecated get_gtfs_standards --- R/data.R | 25 ++++++++++++++++++ R/get_gtfs_standards.R | 18 +++---------- R/import_gtfs.R | 15 ++++++----- inst/tinytest/test_import_gtfs.R | 6 +++++ man/gtfs_reference.Rd | 45 ++++++++++++++++---------------- 5 files changed, 64 insertions(+), 45 deletions(-) diff --git a/R/data.R b/R/data.R index 3b1a44f..1435e15 100644 --- a/R/data.R +++ b/R/data.R @@ -24,3 +24,28 @@ #' @source [https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md](https://github.com/google/transit/blob/master/gtfs/spec/en/reference.md) #' @keywords data "gtfs_reference" + +.doc_field_types = function() { # nocov start + fields <- lapply(gtfsio::gtfs_reference, `[[`, "fields") + fields <- do.call("rbind", fields) + + type_assignment <- unique(fields[,c("Type", "gtfsio_type")]) + type_assignment <- type_assignment[!startsWith(type_assignment$Type, "Foreign ID"),] + type_assignment <- type_assignment[order(type_assignment$gtfsio_type),] + + type_assignment <- lapply(split(type_assignment, type_assignment$Type), function(ta) { + if(nrow(ta) > 1) { + ta$gtfsio_type <- paste0(ta$gtfsio_type, collapse = ", ") + ta <- ta[1,] + } + ta + }) + type_assignment <- do.call("rbind", type_assignment) + + doc <- c("\\itemize{", + paste0("\\item{", type_assignment$Type, " = \`", + type_assignment$gtfsio_type, "\`}"), + "}\n") + + return(paste(doc, collapse = "\n")) +} # nocov end diff --git a/R/get_gtfs_standards.R b/R/get_gtfs_standards.R index eca76c8..0af4bb2 100644 --- a/R/get_gtfs_standards.R +++ b/R/get_gtfs_standards.R @@ -1,3 +1,5 @@ +# nocov start + #' Generate GTFS standards #' #' @description @@ -408,18 +410,4 @@ translate_types <- function(text_file, r_equivalents) { ) } -.doc_field_types = function() { # nocov start - fields <- lapply(gtfsio::gtfs_reference, `[[`, "fields") - fields <- do.call("rbind", fields) - - type_assignment <- unique(fields[,c("Type", "gtfsio_type")]) - type_assignment <- type_assignment[!startsWith(type_assignment$Type, "Foreign ID"),] - type_assignment <- type_assignment[order(type_assignment$gtfsio_type),] - - doc <- c("\\itemize{", - paste0("\\item{", type_assignment$Type, " = \`", - type_assignment$gtfsio_type, "\`}"), - "}\n") - - return(paste(doc, collapse = "\n")) -} # nocov end +# nocov end diff --git a/R/import_gtfs.R b/R/import_gtfs.R index cf5f1c8..2f1c369 100644 --- a/R/import_gtfs.R +++ b/R/import_gtfs.R @@ -384,16 +384,17 @@ remove_file_ext = function(file) { } append_file_ext = function(file) { - vapply(file, function(f) { - file_ext <- gtfsio::gtfs_reference[[f]]["file_ext"] - if (length(file_ext) == 0) { - # use default for argument-specified non-standard files, behaviour defined in test_import_gtfs.R#292 + vapply(file, function(.f) { + file_ext <- gtfsio::gtfs_reference[[remove_file_ext(.f)]][["file_ext"]] + if (is.null(file_ext)) { + # use default for argument-specified non-standard files, + # behaviour defined in test_import_gtfs.R#292 file_ext <- "txt" } - if(grepl(paste0("\\.", file_ext, "$"), f)) { - return(f) # file extension already present + if(endsWith(.f, paste0(".", file_ext))) { + return(.f) # file extension already present } else { - return(paste0(f, ".", file_ext)) + return(paste0(.f, ".", file_ext)) } }, ".txt", USE.NAMES = FALSE) } diff --git a/inst/tinytest/test_import_gtfs.R b/inst/tinytest/test_import_gtfs.R index 74c2e49..a83cd21 100644 --- a/inst/tinytest/test_import_gtfs.R +++ b/inst/tinytest/test_import_gtfs.R @@ -433,3 +433,9 @@ locations_feed <- import_gtfs(system.file("extdata/locations_feed.zip", package expect_inherits(locations_feed[["locations"]], "list") expect_equal(names(locations_feed[["locations"]]), c("type", "name", "crs", "features")) + +# file extension handling +file_exts <- gtfsio:::append_file_ext(c(names(gtfs_reference), "dummy")) +expect_equal(file_exts[which(names(gtfs_reference) == "locations")], "locations.geojson") +expect_equal(file_exts[length(file_exts)], "dummy.txt") +expect_equal(gtfsio:::append_file_ext(file_exts), file_exts) diff --git a/man/gtfs_reference.Rd b/man/gtfs_reference.Rd index 6b12974..c52def3 100644 --- a/man/gtfs_reference.Rd +++ b/man/gtfs_reference.Rd @@ -31,35 +31,34 @@ The data from the official GTFS specification document parsed to a list. \details{ GTFS Types are converted to R types in gtfsio according to the following list: \itemize{ -\item{Unique ID = \code{character}} -\item{Text = \code{character}} -\item{URL = \code{character}} -\item{Timezone = \code{character}} -\item{Language code = \code{character}} -\item{Phone number = \code{character}} -\item{Email = \code{character}} -\item{Time = \code{character}} +\item{Array = \code{geojson_array}} +\item{Color = \code{character}} +\item{Currency amount = \code{numeric}} \item{Currency code = \code{character}} +\item{Date = \code{integer}} +\item{Email = \code{character}} +\item{Enum = \verb{character, integer}} +\item{Float = \code{numeric}} \item{ID = \code{character}} -\item{String = \code{character}} -\item{Color = \code{character}} -\item{Enum = \code{character}} -\item{Text or URL or Email or Phone number = \code{character}} -\item{Array = \code{geojson_array}} -\item{Object = \code{geojson_object}} -\item{Enum = \code{integer}} \item{Integer = \code{integer}} -\item{Date = \code{integer}} +\item{Language code = \code{character}} +\item{Latitude = \code{numeric}} +\item{Longitude = \code{numeric}} +\item{Non-negative float = \code{numeric}} \item{Non-negative integer = \code{integer}} -\item{Non-zero integer = \code{integer}} -\item{Positive integer = \code{integer}} \item{Non-null integer = \code{integer}} -\item{Non-negative float = \code{numeric}} -\item{Currency amount = \code{numeric}} -\item{Float = \code{numeric}} +\item{Non-zero integer = \code{integer}} +\item{Object = \code{geojson_object}} +\item{Phone number = \code{character}} \item{Positive float = \code{numeric}} -\item{Latitude = \code{numeric}} -\item{Longitude = \code{numeric}} +\item{Positive integer = \code{integer}} +\item{String = \code{character}} +\item{Text = \code{character}} +\item{Text or URL or Email or Phone number = \code{character}} +\item{Time = \code{character}} +\item{Timezone = \code{character}} +\item{URL = \code{character}} +\item{Unique ID = \code{character}} } } \keyword{data} From 300ae88e6fbc0b69c0ef65df8b1a5f22165e64fc Mon Sep 17 00:00:00 2001 From: Flavio Poletti Date: Mon, 16 Sep 2024 09:48:22 +0200 Subject: [PATCH 12/12] add revision date to gtfs_reference docs --- R/data.R | 3 ++- .../{create_gtfs_standards.R => create_gtfs_reference_data.R} | 0 man/gtfs_reference.Rd | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) rename inst/reference/{create_gtfs_standards.R => create_gtfs_reference_data.R} (100%) diff --git a/R/data.R b/R/data.R index 1435e15..60d60cf 100644 --- a/R/data.R +++ b/R/data.R @@ -1,6 +1,7 @@ #' GTFS reference #' -#' The data from the official GTFS specification document parsed to a list. +#' The data from the official GTFS specification document parsed to a list. Revision date: +#' ``r attributes(gtfs_reference)$revision_date``. #' #' @format #' A list with data for every GTFS file. Each named list element (also a list) has diff --git a/inst/reference/create_gtfs_standards.R b/inst/reference/create_gtfs_reference_data.R similarity index 100% rename from inst/reference/create_gtfs_standards.R rename to inst/reference/create_gtfs_reference_data.R diff --git a/man/gtfs_reference.Rd b/man/gtfs_reference.Rd index c52def3..569d901 100644 --- a/man/gtfs_reference.Rd +++ b/man/gtfs_reference.Rd @@ -26,7 +26,8 @@ specifications for one GTFS file in the following structure: gtfs_reference } \description{ -The data from the official GTFS specification document parsed to a list. +The data from the official GTFS specification document parsed to a list. Revision date: +\code{2024-08-16}. } \details{ GTFS Types are converted to R types in gtfsio according to the following list: