Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added percent functions. #130

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ Encoding: UTF-8
Language: en-GB
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.2.3
RoxygenNote: 7.3.2
VignetteBuilder: knitr
10 changes: 10 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
# Generated by roxygen2: do not edit by hand

S3method("[",percent)
S3method(Math,percent)
S3method(Summary,percent)
S3method(as.character,percent)
S3method(format,percent)
S3method(mean,percent)
S3method(print,percent)
S3method(rep,percent)
S3method(unique,percent)
export(age_calculate)
export(age_from_chi)
export(age_group)
export(as_percent)
export(chi_check)
export(chi_pad)
export(create_age_groups)
Expand Down
209 changes: 209 additions & 0 deletions R/percent.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#' Percentages
#'
#' @description
#'
#' `percent` is a lightweight S3 class allowing for pretty
#' printing of proportions as percentages. \cr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' printing of proportions as percentages. \cr
#' printing of proportions as percentages.

Copy link
Contributor Author

@Nic-Chr Nic-Chr Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the entire description you want as just that 1 line? Not sure it fully reflects why users might want to use percents.
Edit: Nvm I think it's just line 6.

Copy link
Member

@Moohan Moohan Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my 'suggestion' was just to remove the \cr at the end of line 6 which I assume is a typo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no I typically use it to add a new line, but maybe there's better syntax for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've checked and can't find an alternative that works surprisingly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The \cr appears verbatim in the .Rd file (

printing of proportions as percentages. \cr
). I would just put a new line in, no special characters needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an obvious one but how do I get the documentation to display that line on a new line right below the previous text? If I insert a new line using Enter on my keyboard it places it 2 lines below.

#' It aims to remove the need for creating character vectors of percentages.
#'
#' @param x [`numeric`] vector of proportions.
#'
#' @returns
#' A class of object `percent`.
#'
#' @details
#'
#' ### Rounding
#'
#' The rounding for percent vectors differs to that of base R rounding,
#' namely in that halves are rounded up instead of rounded to even.
#' This means that `round(x)` will round the percent vector `x` using
#' halves-up rounding (like in the janitor package).
#'
#' ### Formatting
#'
#' By default all percentages are formatted to 2 decimal places which can be
#' overwritten using `format()` or using `round()` if your required digits are
#' less than 2. It's worth noting that the digits argument in
#' `format.percent` uses decimal rounding instead of the usual
#' significant digit rounding that `format.default()` uses.
#'
#' @examples
#' library(phsmethods)
#'
Moohan marked this conversation as resolved.
Show resolved Hide resolved
#' # Convert proportions to percentages
#' as_percent(seq(0, 1, 0.1))
#'
#' # You can use round() as usual
#' p <- as_percent(15.56 / 100)
#' round(p)
#' round(p, digits = 1)
#'
#' p2 <- as_percent(0.0005)
#' signif(p2, 2)
#' floor(p2)
#' ceiling(p2)
#'
#' # We can do basic math operations as usual
#'
#' # Order of operations matters
#' 10 * as_percent(c(0, 0.5, 2))
#' as_percent(c(0, 0.5, 2)) * 10
#'
#' as_percent(0.1) + as_percent(0.2)
#'
#' # Formatting options
#' format(as_percent(2.674 / 100), digits = 2, symbol = " (%)")
#' # Prints nicely in data frames (and tibbles)
#' library(dplyr)
#' starwars %>%
#' count(eye_color) %>%
#' mutate(perc = as_percent(n/sum(n))) %>%
#' arrange(desc(perc)) %>% # We can do numeric sorting with percent vectors
#' mutate(perc_rounded = round(perc))
#' @export
#' @rdname percent
as_percent <- function(x){
if (!is.numeric(x)){
stop("x must be a numeric vector of proportions")
Moohan marked this conversation as resolved.
Show resolved Hide resolved
}
new_percent(x)
}
new_percent <- function(x){
class(x) <- "percent"
x
}
round_half_up <- function(x, digits = 0){
if (is.null(digits) || (length(digits) == 1 && digits == Inf)){
return(x)
}
trunc(
abs(x) * 10^digits + 0.5 +
sqrt(.Machine$double.eps)
) /
10^digits * sign(x)
}
signif_half_up <- function(x, digits = 6){
if (is.null(digits) || (length(digits) == 1 && digits == Inf)){
return(x)
}
round_half_up(x, digits - ceiling(log10(abs(x))))
}

#' @export
as.character.percent <- function(x, digits = 2, ...){
stringr::str_c(unclass(round(x, digits) * 100), "%")
}

#' @export
format.percent <- function(x, symbol = "%", trim = TRUE,
Moohan marked this conversation as resolved.
Show resolved Hide resolved
digits = 2,
...){
out <- stringr::str_c(
format(unclass(round(x, digits) * 100), trim = trim, digits = NULL, ...),
symbol
)
out[is.na(x)] <- NA
names(out) <- names(x)
out
}

#' @export
print.percent <- function(x, max = NULL, trim = TRUE,
digits = 2,
...){
out <- x
N <- length(out)
if (N == 0){
print("as_percent(numeric())")
return(invisible(x))
}
if (is.null(max)) {
max <- getOption("max.print", 9999L)
}
suffix <- character()
max <- min(max, N)
if (max < N) {
out <- out[seq_len(max)]
suffix <- stringr::str_c(
" [ reached 'max' / getOption(\"max.print\") -- omitted",
N - max, "entries ]\n",
sep = " "
)
}
print(format(out, trim = trim, digits = digits), ...)
cat(suffix)
invisible(x)
}

#' @export
`[.percent` <- function(x, ..., drop = TRUE){
cl <- oldClass(x)
class(x) <- NULL
out <- NextMethod("[")
class(out) <- cl
out
}
Comment on lines +137 to +144
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just ensures that the next appropriate method for subsetting is used (in this case numeric vector subsetting) and adds the appropriate class back. That means x[] will always return a percent vector, given that x here is a percent vector.
You can see a pretty similar method used in zoo::[.yearmon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maybe get rid of NextMethod() entirely as it can be easily written without it.


#' @export
unique.percent <- function(x, incomparables = FALSE,
fromLast = FALSE, nmax = NA, ...){
cl <- oldClass(x)
class(x) <- NULL
out <- NextMethod("unique")
class(out) <- cl
out
}

#' @export
rep.percent <- function(x, ...){
cl <- oldClass(x)
class(x) <- NULL
out <- NextMethod("rep")
class(out) <- cl
out
}

#' @export
Math.percent <- function(x, ...){
rounding_math <- switch(.Generic,
`floor` =,
`ceiling` =,
`trunc` =,
`round` =,
`signif` = TRUE, FALSE)
x <- unclass(x)
if (rounding_math){
x <- x * 100
if (.Generic == "round"){
out <- do.call(round_half_up, list(x, ...))
} else if (.Generic == "signif"){
out <- do.call(signif_half_up, list(x, ...))
} else {
out <- NextMethod(.Generic)
}
new_percent(out / 100)
} else {
out <- NextMethod(.Generic)
new_percent(out)
}
}
Moohan marked this conversation as resolved.
Show resolved Hide resolved
#' @export
Summary.percent <- function(x, ...){
summary_math <- switch(.Generic,
`sum` =,
`prod` =,
`min` =,
`max` =,
`range` = TRUE, FALSE)
x <- unclass(x)
out <- NextMethod(.Generic)
if (summary_math){
out <- new_percent(out)
}
out
}
#' @export
mean.percent <- function(x, ...){
new_percent(mean(unclass(x), ...))
}
71 changes: 71 additions & 0 deletions man/percent.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions man/phsmethods.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading