Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jimhester committed Dec 23, 2016
0 parents commit b123964
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .Rbuildignore
@@ -0,0 +1,2 @@
^.*\.Rproj$
^\.Rproj\.user$
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
.Rproj.user
.Rhistory
.RData
13 changes: 13 additions & 0 deletions DESCRIPTION
@@ -0,0 +1,13 @@
Package: fstrings
Title: What the Package Does (one line, title case)
Version: 0.0.0.9000
Authors@R: person("Jim", "Hester", email = "james.f.hester@gmail.com", role = c("aut", "cre"))
Description: What the package does (one paragraph).
Depends: R (>= 3.3.2)
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
LinkingTo: Rcpp
Imports: Rcpp
RoxygenNote: 5.0.1.9000
Suggests: testthat
2 changes: 2 additions & 0 deletions LICENSE
@@ -0,0 +1,2 @@
YEAR: 2016
COPYRIGHT HOLDER: Jim Hester
6 changes: 6 additions & 0 deletions NAMESPACE
@@ -0,0 +1,6 @@
# Generated by roxygen2: do not edit by hand

export(f)
export(fstring)
importFrom(Rcpp,sourceCpp)
useDynLib(fstrings)
7 changes: 7 additions & 0 deletions R/RcppExports.R
@@ -0,0 +1,7 @@
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393

fstring_ <- function(x, f) {
.Call('fstrings_fstring_', PACKAGE = 'fstrings', x, f)
}

23 changes: 23 additions & 0 deletions R/fstrings.R
@@ -0,0 +1,23 @@
#' @useDynLib fstrings
#' @importFrom Rcpp sourceCpp
NULL

#' Format a string
#' @param x the string to format
#' @param sep separator used to collapse elements if \code{fun} returns more than one item.
#' @param fun Function to call to format each expression.
#' @param envir Environment to evaluate each expression in. Expressions are
#' evaluated in the order they appear.
#' @examples
#' name <- "Fred"
#' age <- 50
#' anniversary <- as.Date("1991-10-12")
#' f('My name is {name}, my age next year is {age + 1}, my anniversary is {format(anniversary, "%A, %B %d, %Y")}.')
#' @export
fstring <- function(x, sep = "", fun = as.character, envir = parent.frame()) {
fstring_(x, function(x) paste(collapse = sep, fun(eval(parse(text = x), envir = envir))))
}

#' @export
#' @rdname fstring
f <- fstring
12 changes: 12 additions & 0 deletions README.md
@@ -0,0 +1,12 @@
# fstrings

Python style [fstrings](https://www.python.org/dev/peps/pep-0498/) for R.

```r
name <- "Fred"
age <- 50
anniversary <- as.Date("1991-10-12")
f('My name is {name}, my age next year is {age + 1}, my anniversary is {format(anniversary, "%A, %B %d, %Y")}.')
#> [1] "My name is Fred, my age next year is 51, my anniversary is Saturday, Octobe
r 12, 1991."
```
16 changes: 16 additions & 0 deletions fstrings.Rproj
@@ -0,0 +1,16 @@
Version: 1.0

RestoreWorkspace: No
SaveWorkspace: No
AlwaysSaveHistory: Default

EnableCodeIndexing: Yes
Encoding: UTF-8

AutoAppendNewline: Yes
StripTrailingWhitespace: Yes

BuildType: Package
PackageUseDevtools: Yes
PackageInstallArgs: --no-multiarch --with-keep.source
PackageRoxygenize: rd,collate,namespace
24 changes: 24 additions & 0 deletions man/fstring.Rd

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

3 changes: 3 additions & 0 deletions src/.gitignore
@@ -0,0 +1,3 @@
*.o
*.so
*.dll
19 changes: 19 additions & 0 deletions src/RcppExports.cpp
@@ -0,0 +1,19 @@
// Generated by using Rcpp::compileAttributes() -> do not edit by hand
// Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393

#include <Rcpp.h>

using namespace Rcpp;

// fstring_
std::string fstring_(std::string x, Rcpp::Function f);
RcppExport SEXP fstrings_fstring_(SEXP xSEXP, SEXP fSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< std::string >::type x(xSEXP);
Rcpp::traits::input_parameter< Rcpp::Function >::type f(fSEXP);
rcpp_result_gen = Rcpp::wrap(fstring_(x, f));
return rcpp_result_gen;
END_RCPP
}
98 changes: 98 additions & 0 deletions src/fstrings.cpp
@@ -0,0 +1,98 @@
#include "Rcpp.h"

// [[Rcpp::export]]
std::string fstring_(std::string x, Rcpp::Function f) {
enum states {
text,
escape,
single_quote,
double_quote,
backtick,
brace,
comment
};

std::string out = x;
int i = 0;
int brace_level = 0;
int start = 0;
int diff = 0;
states state = text;
states prev_state = text;
for(;i < x.size();++i) {
switch(state) {
case text: {
if (x[i] == '{') {
state = brace;
brace_level = 1;
start = i + 1;
}
break;
}
case escape: { state = prev_state; break; }
case single_quote: {
if (x[i] == '\\') {
prev_state = single_quote;
state = escape;
} else if (x[i] == '\'') {
state = brace;
}
break;
}
case double_quote: {
if (x[i] == '\\') {
prev_state = double_quote;
state = escape;
} else if (x[i] == '\"') {
state = brace;
}
break;
}
case backtick: {
if (x[i] == '\\') {
prev_state = backtick;
state = escape;
} else if (x[i] == '`') {
state = brace;
}
break;
}
case comment: {
if (x[i] == '\n') {
state = brace;
}
break;
}
case brace: {
switch (x[i]) {
case '{': {
if (i > 0 && brace_level == 1 && x[i-1] == '{') {
state = text; break;
}
++brace_level;
break;
}
case '}': --brace_level; break;
case '\'': state = single_quote; break;
case '"': state = double_quote; break;
case '`': state = backtick; break;
case '#': state = comment; break;
};
if (brace_level == 0) {
std::string expr = x.substr(start, i - start);
std::string result = Rcpp::as<std::string>(f(expr));
int begin = start - 1 - diff;
int length = i - start + 2;
//Rcpp::Rcout << result << "{" << begin << ':' << length << "}";
out.replace(begin, length, result);
diff += (expr.length() + 2) - result.length();
state = text;
}
break;
}
};

}

return out;
}
4 changes: 4 additions & 0 deletions tests/testthat.R
@@ -0,0 +1,4 @@
library(testthat)
library(fstrings)

test_check("fstrings")
77 changes: 77 additions & 0 deletions tests/testthat/test-fstring.R
@@ -0,0 +1,77 @@
context("fstring")

test_that("fstring errors if the expression fails", {
expect_error(f("{NoTfOuNd}"), "object .* not found")
})

test_that("fstring works with single expressions", {
foo <- "foo"
expect_identical(foo, f("{foo}"))

foo <- 1L
expect_identical(as.character(foo), f("{foo}"))

foo <- as.raw(1)
expect_identical(as.character(foo), f("{foo}"))

foo <- TRUE
expect_identical(as.character(foo), f("{foo}"))

foo <- as.Date("2016-01-01")
expect_identical(as.character(foo), f("{foo}"))
})

test_that("fstring works with repeated expressions", {
foo <- "foo"
expect_identical(paste(foo, foo), f("{foo} {foo}"))

foo <- 1L
expect_identical(paste(as.character(foo), as.character(foo)), f("{foo} {foo}"))

foo <- as.raw(1)
expect_identical(paste(as.character(foo), as.character(foo)), f("{foo} {foo}"))

foo <- TRUE
expect_identical(paste(as.character(foo), as.character(foo)), f("{foo} {foo}"))

foo <- as.Date("2016-01-01")
expect_identical(paste(as.character(foo), as.character(foo)), f("{foo} {foo}"))
})

test_that("fstring works with multiple expressions", {
foo <- "foo"
bar <- "bar"
expect_identical(paste(foo, bar), f("{foo} {bar}"))

foo <- 1L
bar <- 2L
expect_identical(paste(as.character(foo), as.character(bar)), f("{foo} {bar}"))

foo <- as.raw(1)
bar <- as.raw(2)
expect_identical(paste(as.character(foo), as.character(bar)), f("{foo} {bar}"))

foo <- TRUE
bar <- FALSE
expect_identical(paste(as.character(foo), as.character(bar)), f("{foo} {bar}"))

foo <- as.Date("2016-01-01")
bar <- as.Date("2016-01-02")
expect_identical(paste(as.character(foo), as.character(bar)), f("{foo} {bar}"))
})

test_that("fstring with doubled braces are ignored", {
expect_identical("{{foo}}", f("{{foo}}"))
})

test_that("fstring works with complex expressions", {
`foo}\`` <- "foo"

expect_identical(`foo}\``, f("{
{
'}\\'' # { and } in comments, single quotes
\"}\\\"\" # or double quotes are ignored
`foo}\\`` # as are { in backticks
}
}"))
})

0 comments on commit b123964

Please sign in to comment.