Skip to content

Time to revisit ifelse ?

2 messages · GILLIBERT, Andre, Mikael Jagan

#
Martin Maechler <maechler at stat.math.ethz.ch> wrote:
In my experience, base::ifelse keeping attributes of 'test' is useful for names.
It may also be useful for dimensions, but for other attributes, it may be a dangerous feature.
Otherwise, attributes of c(yes, no) should be mostly preserved in my opinion.
To be consistent with base R, it should warn if length(yes), length(no) and length(test) are not divisors of the longest, otherwise silently repeat the three vectors to get the same sizes.
This would work consistently with mathematical operators such as test+yes+no.

In my personal experience, the truncation of 'yes' and 'no' to length(test) if the most dangerous feature of ifelse().
If the function is not much slower than today ifelse(), it is not worth rewriting in C in my opinion.

Thank you for an implementation!
A few examples of misbehaviors (in my opinion):
Error in as.character.factor(x) : malformed factor
[1] a
Levels: a b

I would expect this one to output
[1] a b
Levels: a b

I tried to develop a function that behaves like mathematical operators (e.g. test+yes+no) for length & dimensions coercion rules.
Please, find the function and a few tests below:

ifelse2 <- function (test, yes, no) {
	# forces evaluation of arguments in order
	test
	yes
	no

	if (is.atomic(test)) {
		if (!is.logical(test))
			storage.mode(test) <- "logical"
	}
	else test <- if (isS4(test)) methods::as(test, "logical") else as.logical(test)

	ntest <- length(test)
	nyes <- length(yes)
	nno <- length(no)

	nn <- c(ntest, nyes, nno)
      nans <- max(nn)

	ans <- rep(c(yes[0L], no[0L]), length.out=nans)

	# check dimension consistency for arrays
	has.dim <- FALSE
	if (length(dim(test)) | length(dim(yes)) | length(dim(no))) {
		lparams <- list(test, yes, no)
		ldims <- lapply(lparams, dim)
		ldims <- ldims[!sapply(ldims, is.null)]
		ldimnames <- lapply(lparams, dimnames)
		ldimnames <- ldimnames[!sapply(ldimnames, is.null)]

		rdim <- ldims[[1]]
		rdimnames <- ldimnames[[1]]
		for(d in ldims) {
			if (!identical(d, rdim)) {
				stop(gettext("non-conformable arrays"))
			}
		}
		has.dim <- TRUE
	}

	if (any(nans %% nn)) {
		warning(gettext("longer object length is not a multiple of shorter object length"))
	}

	if (ntest != nans) {test <- rep(test, length.out=nans)}
	if (nyes != nans) {yes <- rep(yes, length.out=nans)}
	if (nno != nans) {no <- rep(no, length.out=nans)}

	idx <- which( test)
	ans[idx] <- yes[idx]

	idx <- which(!test)
	ans[idx] <- no[idx]

	if (has.dim) {
		dim(ans) <- rdim
		dimnames(ans) <- rdimnames
	}

	if (!is.null(names(test))) {
		names(ans) <- names(test)
	}

	ans
}


ifelse2(c(alpha=TRUE,beta=TRUE,gamma=FALSE),factor(c("A","B","C","X")),factor(c("A","B","C","D")))
ifelse2(c(TRUE,FALSE), as.Date("2025-04-01"), c("2020-07-05", "2022-07-05"))
ifelse2(c(a=TRUE, b=FALSE,c=TRUE,d=TRUE), list(42), list(40,45))
ifelse2(rbind(alpha=c(a=TRUE, b=FALSE),beta=c(c=TRUE,d=FALSE)), list(1:10), list(2:20,3:30))
a=rbind(alpha=c(a=TRUE, b=FALSE),beta=c(TRUE,TRUE))
b=rbind(ALPHA=c(A=TRUE, B=FALSE),BETA=c(C=TRUE,D=TRUE))
c=rbind(ALPHA2=c(A2=TRUE, B2=FALSE),BETA2=c(C2=TRUE,D2=TRUE))
ifelse2(a,b,c)
dimnames(a) <- NULL
ifelse2(a,b,c)
dimnames(b) <- NULL
ifelse2(a,b,c)
#
Andre,

There is a new thread (of length one, sadly), which you should read:

     https://stat.ethz.ch/pipermail/r-devel/2025-July/084113.html

The function of mine that you have been testing was just a fast prototype,
and much work has been done in the mean time.  Can you give the current
proposal (ifelse::ifelse1) a try and let us know if anything stands out?

Mikael
On 2025-08-01 1:13 pm, GILLIBERT, Andre wrote: