
##
##
## More functions are in 
##    /Users/rsaporta/git/misc/rscripts/utils/extract-type functions.r
##
##

shorten <- function(string, nchars=20, thresh=3, ellipsis="..") {
## if string is longer than nchars + thresh, cuts off any characters longer than nchar, then pastes ellipsis
## thresh:  allow for string to not be cropped if it is only slightly longer
## ellipsis should be characters to paste to the end of a cropped string.
## if ellipsis is FALSE, NULL, NA etc then no ellipsis will be used (even though the string will still be cropped if too long)
## if ellipsis is TRUE, this is the same as using the default value of ".."

  if (is.NNNI(ellipsis))
    ellipsis <- ""
  if (is.logical(ellipsis))
    ellipsis <- ifelse(ellipsis, "..", "")

  nc.e <- nchar(ellipsis)

  nc <- nchar(string)

  ret <- ifelse (nc > nchars + thresh,
    yes = substr(string, 1, nchars-nc.e) %>% paste0(ellipsis),
    no = string
  )

  return(ret)
}

cleanWS <- function(x) {
## Cleans multiple whitespaces to a single whitespaces, and trims whitespace from edge 
  gsub("^\\s+|\\s+$", "", 
        gsub(" +", " ", x)
      )
}

escape_the_escape <- function(x, esc="\\") {
  pat  <- pasteC(rep(esc, 2))
  repl <- pasteC(rep(pat, 2))

  return(gsub(pat, repl, x))
}

padString <- function(x, width=mnchar(x), align=c("right", "left")) {
  align <- match.arg(align)
  if (align == "left")
    width <- -width

  width %>% paste0("%",.,"s") %>% sprintf(x) 
}

ltrim <- function(x) {
  return(trim(x, side="left"))
}
rtrim <- function(x) {
  return(trim(x, side="right"))
}
trim <- function(x, side=c("both", "left", "right")) {
## Remove whitespace, while preserving NAs. 

  ## if empty, return it as is
  if (!length(x))
    return(x)

  ## Iterate
  if (is.list(x))
    return(lapply(x, trim, side=side))

  ## Match the arg
  side %<>% tolower %>% match.arg(c("both", "left", "right"))
  pat.r <- "\\s+$"
  pat.l <- "^\\s+"
  pat <- switch(side, "both"=paste(pat.l, pat.r, sep="|"), "left"=pat.l, "right"=pat.r)


  nas <- is.na(x)
  ret <- gsub(pat, "", x)
  if (any(nas))
    ret[nas] <- NA
  return(ret)
}

removeSpecificCharacters <- function(x, replace="_", chars_to_remove=c("@", "*", "#", "\\", "`", "{", "}", "[", "]"), verbose=FALSE) {
  verboseMsg(verbose, "Removing  ", pasteC(chars_to_remove, C=" "), "     \tReplacing with '", replace, "'", sep="", time=FALSE)
  pat <- regOr(chars_to_remove, escape=TRUE, asterisk="+")
  gsub(pat, replace, x)
}

  
removeNonAlphaNumeric <- function(x, replace="_") {
  gsub("[^[:alnum:] ]", replace, x)
}

mnchar <- function(x, ..., na.rm=FALSE) {
  ## We duplicate the line of code because it is faster than copying x <- x[!is.na(x)] or filtering x[TRUE]
  if (na.rm)
    max(nchar(as.character(x[!is.na(x)]), ...))
  else 
    max(nchar(as.character(x), ...))
}
    

spaceToUnderscore <- function(x) {
  gsub(" ", "_", x)
}
underscoreToSpace <- function(x) {
  gsub("_", " ", x)
}

regExUpperLower <- function(x) {
## Useful for times when the ignore.case argument is not an available (eg: sending query strings to postgres)
  gsub("([a-zA-Z])", "[\\U\\1\\L\\1]", x, perl=TRUE)
}

mgsub <- function(pattern, replacement, x, ..., fixed=TRUE) {
# like an mapply on gsub, but done iteratively. 

  if(length(pattern) != length(replacement))
    stop("pattern and replacement differ should be the same length")

  ## TODO: add recycling and error-check 

  for(i in 1:length(pattern))
    x <- gsub(pattern[i], replacement[i], x, ..., fixed=fixed)
  
  return(x)
}


cleanChars <- function(text, replacement="_", Whitelist=NULL) {
  #
  #  Allows:  Digits, Letters, ' ', '-', '_', '.', '+'
  # wrapper for gsub, with regex pre-composed
  # replaces all non-basic chararcters with replacement (underscore by default)
  # Whitelist is non-functional for now  # TODO

  if (!is.null(Whitelist))
    stop("Whitelist is non-functional")

  Simple_regex <- "[^-\\.\\+\\_0-9a-zA-Z ]"
  gsub(Simple_regex, replacement, text)
}

replaceBadCharsUnderscore <- function(str, WhiteList=NULL) {
  stop ("use cleanChars() instead")
}



basicStringCleaning <- function(x, convertToChar=!is.character(x), chars_to_clean_dups=c("\"", "\'")) {
  if (!isTRUE(convertToChar))
    removeDuplicateChars(cleanWS(x), chars=chars_to_clean_dups)
  else
    removeDuplicateChars(cleanWS(as.character(x)), chars=chars_to_clean_dups)
}

removeDuplicateChars <- function(x, chars=c('\"', "\'"), min_reps=2, max_reps=2, showChanges=0) {
  ## Validate input
  if (is.null(min_reps))
    min_reps <- ""
  if (is.null(max_reps))
    max_reps <- ""
  
  if (isTRUE(showChanges))
    showChanges <- 100
  if (!identical(showChanges, FALSE))
    showChanges <- min(showChanges, length(x))

  #  ## Using one single call to gsub with the chars separated by "|" does not work
  #  ##  since it will also catch two consecutive elements of different characters
  #  ## EG: 
  #  if (FALSE) 
  #  {
  #    x <- c('""FIRE LANE"" SIGN POLE', "''FIRE LANE'' SIGN POLE", "'\"FIRE LANE\"' SIGN POLE")
  #    pat <- sprintf("%s{%s,%s}", regOr(chars), min_reps, max_reps)
  #    ret <- gsub(pat, '\\1', x)
  #
  #    cat("\n (see last entry)\n", sprintf("%24s   ~~>  %24s\n", x, ret), sep="")
  #  }

  ## For verbose output, bank the original
  if (showChanges)
    orig <- copy(x)

  ## Cleanup
  for (ch in chars) {
    pat <- sprintf("(%s){%s,%s}", ch, min_reps, max_reps)
    x <- gsub(pat, '\\1', x)
  }

  ## verbose output
  if (showChanges)
      cat("\n Below are the first ", showChanges, " changes made\n--------------------------------------\n", sprintf("%34s   ~~>  %34s\n", orig, x)[1:showChanges], sep="")

  return(x)
}


camelSplit <- function(x, alsoSplit=c(" ", "_", "-", "."), isolate="%", dontUseNames.of.data.frame=FALSE) {
## Splits a camelCase string into words, also splitting on any value in 'alsoSplit'
## If x has length == 1, returns a vector of length equal to the number of words found
## If x has length >  1, returns a list of vectors
## If x has length == 0, returns x

  if (!length(alsoSplit))
    stop("alsoSplit must have length at least one")
  if (!length(x)) {
    warning ("x has no length")
    return(x)
  }
  if (!dontUseNames.of.data.frame && is.data.frame(x))
    x <- names(x)

  isolate <- setdiff(isolate, "")
  if (length(isolate)) {
    if (!is.character(isolate))
      stop ("isolate should be a character vector or NULL")
    pat.iso <- sprintf("(.)?%s(.)?", regOr(isolate, escape=TRUE))
    repl.iso  <- sprintf("\\1%s\\2%1$s\\3", alsoSplit[[1]])
    x <- gsub(pat.iso, repl.iso, x)
    x <- gsub(pat.iso, repl.iso, x)
  }

  pat.gsub  <- "([a-z])([A-Z])"
  repl.gsub <- sprintf("\\1%s\\2", alsoSplit[[1]])
  pat.split <- regOr(alsoSplit, escape=TRUE)
  ret <- lapply(
              strsplit(gsub(pat.gsub, repl.gsub, x), pat.split)
              , function(x) x[x != ""])

  if (length(x) == 1)
    return (ret[[1]])
  return(ret)
}


#  getHighestMatchBelow <- function(matches, below, next_highest_if_none_found=TRUE) {
#    ##  OLD: 
#    ##   matches[matches < below] %>% max
#  
#    ret <- max(which(matches < below), -1)
#    if (ret == -1) {
#      if (next_highest_if_none_found)
#        ret <- min(which(matches>=below))
#      else {
#        warning ("no matches below ", below, "  ---  returning NA", call.=FALSE)
#        return (NA)
#      }
#    } 
#    matches[ret]
#  }


getNextMatchBetween <- function(matches, min_boundary, max_boundary
    , direction=c("previous_from_max", "next_from_min", "middle")
    , nomatch=c("NA", "next_from_min", "previous_from_max", "min_boundary", "max_boundary"), showWarnings=TRUE
    , default_match_length=1) {
## This function is used inside of strsplitToMaxCharSize()
## ARGS:
##   min_boundary is inclusinve, ie   matches >= min_boundary
##   max_boundary is exclusive,  ie   matches <  max_boundary

## absolute bounds are .... ?   matches[1, length(matches)]
## If there are duplicates in matches, the first value is used

  direction <- match.arg(direction)
  nomatch   <- match.arg(nomatch)

  if (!is.numeric(min_boundary) || is.na(min_boundary))
    stop ("min_boundary must be a non-NA integer")
  if (!is.numeric(max_boundary) || is.na(max_boundary))
    stop ("max_boundary must be a non-NA integer")

  # browser(text="in getNextMatchBetween() before matches_bounded")
  # browser(text="in getNextMatchBetween() before matches_bounded", expr=max_boundary >= 43)

  matches_bounded <- matches[matches >= min_boundary & matches < max_boundary]
  if (!length(matches_bounded)) {
    if (showWarnings)
        warning ("No matches in interval [", min_boundary, ", ", max_boundary, ")" , call.=FALSE)
    ret <- {switch(nomatch,
              "NA" = NA, 
            , "next_from_min" = min(matches[matches >= min_boundary])
               ## Alternative by index, but then min/max options would not work
               ##       matches %>% {. >= min_boundary} %>% which %>% min(., length(matches))
            , "previous_from_max" = max(matches[matches < max_boundary])
            , "min_boundary" = min_boundary
            , "max_boundary" = max_boundary
           )}
  } else { # // end !length(matches_bounded)
    ret <- {switch(direction,
            , "next_from_min" = min(matches_bounded)
            , "previous_from_max" = max(matches_bounded)
            , "middle" = matches_bounded[matches_bounded %>% {. - midpoint(min_boundary, max_boundary)} %>% abs %>% which.min]
           )}
  }

  ## set the match.length attribute
  match.length <- attr(matches, "match.length")[matches == ret][[1]]
  if (is.null(match.length))
    match.length <- default_match_length
  attr(ret, "match.length") <- match.length
  return(ret)
}



strsplitToMaxCharSize <- function(string, pattern, chars=1e6, ignore.case=FALSE, fixed=FALSE, perl=FALSE, safetybreak.max_iterations=5000, showWarnings=TRUE, clearTrailingEmptyString=TRUE) {
## If string ends in pattern, the last element will be a blank string, ""
  ## Input check
  is.char_of_length1(string,  fail.if.not=TRUE)
  is.char_of_length1(pattern, fail.if.not=TRUE)

  matches <- gregexpr(pattern, string, ignore.case=ignore.case, fixed=fixed, perl=perl)[[1]]
  largestMatch <- max(matches)

  if (any(matches == -1)) {
    warning ("could not find pattern in string. Returning string unchanged", call.=FALSE)
    return(string)
  }

  ## check for only a single match that spans the whole string
  ## This is necessarry since the first value of 'starts' is always 1 
  ##     and the last value of 'stops' is always nchar(string)
  ## This without this check, the entire string would be returned
  if (length(matches) == 1  &&  attr(matches, "match.length") == nchar(string)) {
    if (showWarnings)
          warning ("The pattern matched the entire string, returning blank")
    ## return either a blank string or character(0), depending on the parameter clearTrailingEmptyString
    return(if(clearTrailingEmptyString) character() else "")
  }


  ## Initialize
  iteration_counter <- 0
  starts <- c(1)
  stops <- c()

  ## while there is a match somewhere beyond the next 'chars'-many substring, keep iterating
  while (starts[length(starts)] + chars <= largestMatch) {
    next_split <- getNextMatchBetween(matches, min=starts[length(starts)], max=starts[length(starts)] + chars, direction="previous_from_max", nomatch="next_from_min", showWarnings=FALSE)
    ## SAFETY BREAK
    iteration_counter %<>% add(1)
    if (iteration_counter > safetybreak.max_iterations)
      stop("iteration_counter exceeds max (", safetybreak.max_iterations, ")")
    stops <-  c(stops, next_split - 1)
    starts <- c(starts, next_split + attr(next_split, "match.length", exact=TRUE))
  }

  ## If the string ENDS with a match, and the largestMatch did not naturally make it as a split point
  ##    which it generally will not, if 'chars' is larger than nchar(<last_substring>)
  ##    Then add it manually
  ##
  ## First, check if 'largestMatch_plus_length' is in 'starts', then check if it is the same length as 'string'
  ##  (the minus one is because we are checking if the end  of the "previous" substring is the end of the whole string)
  ## Also, be sure that iteration_counter > 0, since otherwise that means that while loop was never entered, and "whole match" already captured, if any match exists
  largestMatch_plus_length <- largestMatch + tail(attr(matches, "match.length"), 1)
  if (!(largestMatch_plus_length %in% starts)  &&   largestMatch_plus_length - 1 == nchar(string) && iteration_counter > 0) {
      start_to_add <- largestMatch + tail(attr(matches, "match.length"), 1)
      stops_to_add <- largestMatch - 1

      starts <- c(starts, start_to_add)
      stops  <- c(stops,  stops_to_add)
  }

  stops <- c(stops, nchar(string))
  ## note that if 'string' ends in a match, the 'starts'-'stops' combinations will end with
  ##    nchar(string) + 1,   nchar(string)
  ## In other words, the substr() command will split from a "larger"
  ##     character index which will yield a blank string ("")


  ## ERROR CHECK -- 'starts' and 'stops' should always have the same length. If not, fail.
  if (length(starts) != length(stops))
    stop(sprintf("Internal Error:  Differing lengths for 'stops' (%i) and 'starts' (%i)", length(stops), length(starts)), call.=TRUE)

  ## SPLIT THE STRING!
  ret <- mapply(substr, start=starts, stop=stops, x=string, SIMPLIFY=TRUE, USE.NAMES=FALSE)

  ## Check for any strings beyond chars
  if (showWarnings &&  any(nchar(ret) > chars))
    warning ("Some results have character length larger than 'chars'")

  if (clearTrailingEmptyString && tail(ret, 1) == "")
    ret <- head(ret, -1)


  ## RETURN
  return(ret)
}

## TESTING strsplitToMaxCharSize()
if (FALSE) {
  TestString <- "Hello_World_how_are_you_doing_veryWell_thank_you_"
  pat <-  "_"  ## underscore

  strsplitToMaxCharSize(TestString, pattern=pat, chars=3)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=3, clearTrailingEmptyString=FALSE)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=5)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=5, clearTrailingEmptyString=FALSE)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=8)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=18)
  strsplitToMaxCharSize("_t", pattern="_t", chars=18, clearTrailingEmptyString=TRUE)

  TestString <- "Hello,\nWorld,\nhow,\nare,\nyou,\ndoing,\nveryWell,\nthank,\nyou,\n"
  pat <-  ",\n"  ## underscore

  strsplitToMaxCharSize(TestString, pattern=pat, chars=3)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=3, clearTrailingEmptyString=FALSE)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=5)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=5, clearTrailingEmptyString=FALSE)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=8)
  strsplitToMaxCharSize(TestString, pattern=pat, chars=18)

  strsplitToMaxCharSize(TestString, pattern=",", chars=3)
  strsplitToMaxCharSize(TestString, pattern="\n", chars=3)
  strsplitToMaxCharSize(TestString, pattern="\\n", chars=3)
}





regOr <- function(vec, brackets=TRUE, asterisk=NULL, escape=FALSE, underbound=FALSE, whole=FALSE, whitespace=FALSE, or_start=FALSE, or_end=FALSE) { 
# combines a string vector into a regex `or` statement

  if (whole && !brackets) {
    warning("Cannot use whole without brackets. Please add `^` `$` anchors manually")
  }

  if (escape)
    vec <- escapeRegEx(vec)

  if (or_start)
    vec %<>% c("^", .)
  if (or_end)
    vec %<>% c(., "$")

  ## Allow for logical values in asterisk
  if (is.logical(asterisk)) {
    message("NOTE: the 'asterisk' should be a character such as '+' or '*' - the name is a misnomoer")
    asterisk <- ifelse(isTRUE(asterisk), "*", "")
  }

  # ws will be pasted.  If not grabbing whitespace, make this NULL
  ws <- if (whitespace) "\\s*" else NULL

  # The main string to return 
  ret <- paste0(ws, c(vec), ws, collapse="|")

  # If not adding brackets, then we're all set. 
  if (!brackets)
    return(paste0(ret, asterisk))

  # else return
  ret <- paste0("(", ret, ")", asterisk, collapse="")

  if (whole) {
    # Two separate lines to combat vectorization of paste
    ret <- paste0("^", ret)
    ret <- paste0(ret, "$")
  } 

  if (underbound)
    ret %<>% sprintf("(_|\\b)%s(_|\\b)", .)

  return(ret)
}


regexAll <- function(pattern, stringVec, replace="@@@", ignore.case=FALSE, fixed=FALSE, perl=FALSE, value=FALSE) {
# quick comparisons of the different family of regex options

# Look behind / ahead example
if (FALSE) {
  ret <- c("I am some string that does not have value", "Another server-process is running with [16659], cannot start a new one. Exiting.")
  pat <- "(?<=is running with \\[)\\d{1,}(?=\\])"
  regexAll(pat, ret, perl=TRUE, rep=FALSE)
}



  ret <- list("OriginalString"=stringVec
            ,   "grep"     = grep    (pattern, stringVec, ignore.case=ignore.case, fixed=fixed, perl=perl, value=value)
            ,   "grepl"    = grepl   (pattern, stringVec, ignore.case=ignore.case, fixed=fixed, perl=perl) 
            ,   "regexpr"  = regexpr (pattern, stringVec, ignore.case=ignore.case, fixed=fixed, perl=perl) 
            ,   "gregexpr" = gregexpr(pattern, stringVec, ignore.case=ignore.case, fixed=fixed, perl=perl)  
            ,   "regexec"  = {if (!perl)  ## regexec does not accept perl expressions
                               regexec (pattern, stringVec, ignore.case=ignore.case, fixed=fixed) }
            )
  if (!is.null(replace) && !identical(replace, FALSE)) 
    ret <- c(ret, list(
        "sub"     = sub (pattern, replace, stringVec, ignore.case=ignore.case, fixed=fixed, perl=perl)
     , "gsub"     = gsub(pattern, replace, stringVec, ignore.case=ignore.case, fixed=fixed, perl=perl)
      ))
  return(ret)
}



greplAny <- function (pattern, x, ignore.case=FALSE, perl=FALSE, fixed=FALSE, useBytes=FALSE) {
# searchs for ANY value of pattern in `x`

  found <- sapply(pattern, grepl, x, ignore.case=ignore.case, perl=perl, fixed=fixed, useBytes=useBytes )
  ret <- rowSums(found) >= 1

  setNames(ret, x)
}

## This changes everything
cleanAndWords <- function(x, words=c("AND","And", "and", "&", "Y", "y", "E", "e"), replace="&") {
  spacers <- c("_", ".", " ", ",", ":", ";", "'", "\"", "/", "\\\\")

  ## PAT 1: same character before and after
  pat1 <- sprintf("%s%s(\\1)", regOr(spacers, escape=TRUE), regOr(words, escape=FALSE))
  ## PAT 2: certain expected characters before, followed by space or comma-space
  pat2 <- sprintf("(,|;|:|/|\\\\)%s(,? )", regOr(words, escape=FALSE))

  ## attempt to combine both patterns fails at the end
  # pat <- ((pat1)|pat2)(words)(\\1|fails-here)

  ## replace: preserve the word boundaries
  repl <- sprintf("\\1%s\\3", replace)
  x %>% gsub(pat1, repl, .) %>% gsub(pat2, repl, .)
}

## EXAMPLES
if (FALSE) {
  x_and_examples <- c("holl.and.oats", "hello_and world", "hall:and oats", "hall:and, oats")
  cleanAndWords(x_and_examples)
}

## I started to develop this, the idea behind it is that I want something that uses
## (\\b)(word)(\\1), but thats more pain in the ass to make than I originally noticed
## On top of that, there are some characters where left and right bounds may not be the same (ie, a comma)
## thus, left this for now
# Not used  replaceWords <- function(x, words=c("AND","And", "and", "&", "Y", "y", "E", "e"), replace="&", left_bound=regOr(spacers, esc=TRUE, or_start=TRUE), right_bound="\\1|$", single_letter_special_treatment=TRUE) {
# Not used  ## Note, this will not catch words inside brackets or parenthesis. 
# Not used    if (length(replace) > 1)
# Not used      stop ("replace must be length exactly 1. In replaceWords(), all words are replaced with the same single word\n\nHINT: Did you want mgsub()?", call.=FALSE)
# Not used  
# Not used    print(left_bound); print(right_bound)
# Not used    return(NULL)
# Not used    spacers=c("_", ".", " ")
# Not used    pat <- sprintf("%s%s(\\1)", regOr(spacers, escape=TRUE), regOr(words, escape=FALSE))
# Not used    repl <- sprintf("\\1%s\\3", replace)
# Not used    gsub(pat, repl, x)
# Not used  }; replaceWords()


# old version removeExt <- function(x, replace="", showWarnings=TRUE) {
# old version   if (length(x) > 1)
# old version     return (sapply(x, removeExt, replace=replace, showWarnings=showWarnings))
# old version   if (length(x) < 1) {
# old version     verboseMsg(showWarnings, "x has no length")
# old version     return(x)
# old version   }
# old version 
# old version   bn <- basename(x)
# old version   if (!grepl("\\.", bn)) {
# old version     verboseMsg(showWarnings, "No ext detected for '", x, "'")
# old version     return(x)
# old version   }
# old version 
# old version   ext <- removeText(".*\\.", bn)
# old version 
# old version   if (nchar(replace) && !grepl("^\\.", replace))
# old version     replace %<>% paste0(".", .)
# old version 
# old version   ext %>% paste0(".", .) %>% gsub(rep=replace, x=x)
# old version }

removeExt <- function(x, replace="", allow_dots_in_ext=TRUE, showWarnings=TRUE) {
  # if (length(x) > 1)
  #   return (sapply(x, removeExt, replace=replace, showWarnings=showWarnings))
  # if (length(x) < 1) {
  #   verboseMsg(showWarnings, "x has no length")
  #   return(x)
  # }

  extractExt(x, allow_dots_in_ext=allow_dots_in_ext) %>% 
      paste0("\\.", .) %>% 
      removeText(x, end_only=TRUE)
}


extractExt <- function(x, allow_dots_in_ext=TRUE, showWarnings=TRUE) {
##  x <- c("file", "file.txt", "file.lookml.view", "file.with.three.exts") %>% selfname_
##  extractExt(x, allow_dots_in_ext=TRUE)
##                  file             file.txt     file.lookml.view file.with.three.exts
##                    ""                "txt"        "lookml.view"    "with.three.exts"
##  extractExt(x, allow_dots_in_ext=FALSE)
##                  file             file.txt     file.lookml.view file.with.three.exts
##                    ""                "txt"               "view"               "exts"

  if (length(x) < 1) {
    verboseMsg(showWarnings, "x has no length")
    return(x)
  }

  if (!allow_dots_in_ext)
    gsub("(.+\\.|^.*?$)", "", x)
  else
    sub(".*?(\\.|$)", "", x)
}

removeText <- function(pattern, x, start_only=FALSE, end_only=FALSE, ignore.case=FALSE, perl=FALSE, fixed=FALSE, func=if(g) gsub else sub, g=TRUE) {
  if (is.null(pattern))
    return(x)
  
  if (start_only && !grepl("^\\^", pattern))
    pattern <- paste0("^", pattern)
  if (end_only && !grepl("\\$$", pattern))
    pattern <- paste0(pattern, "$")

  func <- match.fun(func)

  if (length(pattern) > 1 && length(pattern) == length(x))
    mapply(func, pattern=pattern, replace="", x=x, ignore.case=ignore.case, perl=perl, fixed=fixed) %>% setNames(nm=names(x), obj=.)
  else
    func(pattern=pattern, replace="", x=x, ignore.case=ignore.case, perl=perl, fixed=fixed)
}



if (FALSE) {
  ## Test cases for removeWord
  x <- c("A. World", "A World", "hello a. world", "hello a world", "hello\nthe\nworld", "hello the world", "h.the.world") %>% selfname_

  words <- c("the", "a", "an", "inc", "llc", "ltd")

  removeWord(words=words, x=x)
  removeWord(words="a", x=x)
  removeWord(words="hello", x=x)
  removeWord(words=c("world", "a"), x=x)
  removeWord(words=c("hello", "a"), x=x)
}

removeWord <- function(words, x, ignore.case=TRUE, escape_words=FALSE, perl=FALSE, fixed=FALSE, single_letter_special_treatment=TRUE) {

  if (!length(words))
      return(x)

  ## single-letter words get special treatment

  ## Give "A" special treatment, but only if it is one of multiple words being removed
  has_single_letter_words <- length(words > 1) && any(nchar(words) == 1) && single_letter_special_treatment

  if (has_single_letter_words) {
    single_let_words <- words[nchar(words) == 1]
    words %<>% setdiff(single_let_words)
    if (length(single_let_words) > 1)
      message("single_let_words is ", pasteQand(single_let_words))
  }

  ## This pattern, when removed, takes one whitespace away with it (if a whitespace is available).
  if (length(words)) {
    frmt <- "(\\s%1$s\\b|\\b%1$s\\s|\\b%1$s\\b)"
    pat <- regOr(words, escape = escape_words) %>% sprintf(frmt, .)
    ## Put 'a' back, but wtih special pattern
    if (has_single_letter_words)
      pat <- regOr(single_let_words) %>% sprintf("(\\b%s\\s|%s)", ., pat)
  } else {
    pat <- "\\ba\\s"
  }

  removeText(pattern=pat, x=x, ignore.case=ignore.case, perl=perl, fixed=fixed, func=gsub, start_only=FALSE, end_only=FALSE)
}

