Our 10 featured functions

UNIQUE

Matches all 5 recorded answers

Recorded: 5 workbook cells. How we measure this · See the Rust code

Local names in 40 languages
LanguageName in Excel
ArabicUNIQUE
BulgarianUNIQUE
CzechUNIQUE
DanishENTYDIGE
GermanUNIQUE
GreekUNIQUE
English (Australia)UNIQUE
English (United Kingdom)UNIQUE
English (United States)UNIQUE
SpanishUNICOS
EstonianUNIQUE
FinnishAINUTLAATUINEN
FrenchUNIQUE
HebrewUNIQUE
CroatianUNIQUE
HungarianEGYEDI
IndonesianUNIQUE
ItalianUNIQUE
JapaneseUNIQUE
KoreanUNIQUE
LithuanianUNIQUE
LatvianUNIQUE
NorwegianUNIQUE
DutchUNIEK
PolishUNIKATOWE
Portuguese (Brazil)UNIQUE
Portuguese (Portugal)EXCLUSIVO
RomanianUNIQUE
RussianУНИК
SlovakUNIQUE
SlovenianUNIQUE
Serbian (Latin)UNIQUE
SwedishUNIK
ThaiUNIQUE
TurkishBENZERSİZ
UkrainianUNIQUE
VietnameseUNIQUE
Chinese (Simplified)UNIQUE
Chinese (Hong Kong)UNIQUE
Chinese (Traditional)UNIQUE

Names from Microsoft's support site.

What it does

UNIQUE(array, [by_col], [exactly_once])

UNIQUE removes repetition along one axis. It compares whole rows (or, with by_col set, whole columns) and returns a subset of them, in their original order of first appearance.

The flag exactly_once selects between two genuinely different operations, and Microsoft's documentation is careful about the distinction:

  • exactly_once = FALSE (default): distinct. Every row that appears at least once appears once in the output. From A, B, A, C you get A, B, C.
  • exactly_once = TRUE: unique in the database sense. Only rows that appear exactly once survive. From A, B, A, C you get B, C — A is gone entirely, not collapsed.

These are not two settings of one filter; they are "distinct" and "singletons", and confusing them silently drops data. The English word "unique" covers both, which is precisely why the argument exists.

Two structural points:

  • The unit of comparison is a whole row, not a cell. UNIQUE(A1:C100) treats two rows as equal only when all three of their cells match. To deduplicate on one column you pass that column, or you rebuild the row afterwards.

What "equal" means for two rows — case sensitivity for text, numeric near-equality, whether a blank equals a zero, whether two #N/A values are equal — is the substantive open question on this page. The documentation does not state it.

Arguments

Microsoft documents UNIQUE(array, [by_col], [exactly_once]).

ArgumentDefaultMeaning
arrayrequiredThe range or array from which to return unique rows or columns
by_colFALSETRUE compares columns; FALSE or omitted compares rows
exactly_onceFALSETRUE returns only rows/columns occurring exactly once; FALSE returns all distinct ones

The commonly misunderstood position is the third. Because both optional arguments are logicals with FALSE defaults, UNIQUE(A1:A9, TRUE) reads at a glance like "yes, unique" but actually means "compare columns" — which, for a single-column input, returns the whole column unchanged. The singleton behaviour needs the third argument: UNIQUE(A1:A9, , TRUE).

Try it

Input cells (optional)

Example: {"B2": 3, "B3": 4}. A1 holds the formula. Other cells need values before a formula can read them.

Enter a formula, then calculate.

Runs locally in your browser with OneCalc. Its answer may differ from Excel. Use English function names and commas between arguments.

Quirks

Result and edge cases

The return kind is an array — a subset of the input's rows (or columns), full width, spilled.

  • The result can be empty. UNIQUE(x, , TRUE) over data where every row repeats has nothing to return. Excel's convention for an empty array result is #CALC! (see the value universe); the Handbook has not verified that UNIQUE uses it.
  • Blanks are values. A blank row is a row, and it will appear in the distinct set. This surprises readers who apply UNIQUE to a whole column and get a blank entry from the unused rows below the data — the usual remedy being TRIMRANGE or a FILTER first.
  • Errors inside the array are values too; whether two identical error values compare equal is unverified.
  • Shape. Row mode returns full-width rows; column mode returns full-height columns. A single-element result must still be array-shaped — the ROWS/TYPE probe on the ROWS page explains why that distinction is observable.
  • Dynamic-array publication and #SPILL! are host-side adaptation, not UNIQUE semantics.

Errors

Microsoft documents one error for UNIQUE: #REF! in the cross-workbook dynamic-array case, when a linked source workbook is closed — dynamic arrays between workbooks are supported only while both are open.

No other error is documented. #SPILL! arises from publication as for any dynamic array, and #CALC! is Excel's general marker for an empty array result; neither is stated on the UNIQUE page, and the Handbook does not assert them for this function.

How to implement

Notes for implementers

  • Row equality, not cell equality, is the primitive. It needs a defined comparison for every value kind, including blanks and errors, and it needs to be consistent with whatever equality the rest of the engine uses — or to be deliberately different, and recorded as such.
  • Order of first appearance must be preserved. A hash-set implementation that returns iteration order will be wrong on almost every input and right on the tests.
  • exactly_once requires counting, not just membership: it is a second pass (or a count-keyed map), and it cannot be short-circuited on first sight of a row.
  • The empty result is a real case and needs a decided outcome rather than a panic or an empty array that later code cannot represent.

Rust code

Show the CalcFunc source of this function

Quoted from the public repository DnaCalc/DnaCalc, crates calc-func and calc-fml, at commit 9e1884369a67. Comments are the source's own. A kernel shared with other functions appears once, on its module page.

UNIQUE · 3 lines · calc-func/src/catalogue/arrays.rs on GitHub
5 unique_fn "UNIQUE" (1, 3) [R "array", V "by_col", V "exactly_once"] |s, a, sv| {
    unique(s, a, sv)
}
line · 23 lines · calc-func/src/catalogue/arrays.rs on GitHub
/// The cells of row (or column, `by_col`) `index` of `array`.
fn line(array: &CalcArray, index: usize, by_col: bool) -> Vec<CalcValue> {
    let ArrayShape { rows, cols } = array.shape();
    if by_col {
        (0..rows)
            .map(|r| {
                array
                    .get(r, index)
                    .cloned()
                    .unwrap_or_else(CalcValue::empty)
            })
            .collect()
    } else {
        (0..cols)
            .map(|c| {
                array
                    .get(index, c)
                    .cloned()
                    .unwrap_or_else(CalcValue::empty)
            })
            .collect()
    }
}
same · 13 lines · calc-func/src/catalogue/arrays.rs on GitHub
/// Whether two cells are the same for UNIQUE: numbers when equal as binary64 (OxFunc's rule), text
/// ignoring case under the collation, and otherwise equal kinds with equal values.
fn same(site: &Site<'_>, x: &CalcValue, y: &CalcValue) -> Step<bool> {
    match (x.core_ref(), y.core_ref()) {
        (CoreRef::Text(a), CoreRef::Text(b)) => {
            comparison::texts(a.utf16_code_units(), b.utf16_code_units(), Want::Equality)
                .map(Ordering::is_eq)
                .map_err(|_| site.not_modelled(None))
        }
        (CoreRef::Number(a), CoreRef::Number(b)) => Ok(a == b),
        _ => order(site, x, y).map(Ordering::is_eq),
    }
}
bucket · 23 lines · calc-func/src/catalogue/arrays.rs on GitHub
/// A coarse key under which equal cells always fall together (text by its collation sort key,
/// numbers by their bits), so each line is compared only with the lines that share it; `None`
/// when a text's sort key is not built (dc-zsn.1), whose equality to the other texts is then not
/// decided.
fn bucket(cells: &[CalcValue]) -> Option<String> {
    let mut key = String::new();
    for cell in cells {
        match cell.core_ref() {
            CoreRef::Number(n) => key.push_str(&format!("n{:x}|", (n + 0.0).to_bits())),
            CoreRef::Text(t) => {
                key.push('t');
                for byte in comparison::text_bucket(t.utf16_code_units())? {
                    key.push_str(&format!("{byte:02x}"));
                }
                key.push('|');
            }
            CoreRef::Logical(b) => key.push_str(if b { "T|" } else { "F|" }),
            CoreRef::Error(code) => key.push_str(&format!("e{}|", code as u8)),
            _ => key.push_str("b|"),
        }
    }
    Some(key)
}
unique · 73 lines · calc-func/src/catalogue/arrays.rs on GitHub
fn unique(site: &Site<'_>, a: &[CalcValue], sv: &mut EvalServices<'_>) -> Step<CalcValue> {
    let args = prepared(site, a, sv)?;
    let array = as_array(&args[0]);
    let by_col = flag(site, args.get(1))?;
    let exactly_once = flag(site, args.get(2))?;
    let ArrayShape { rows, cols } = array.shape();
    let len = if by_col { cols } else { rows };
    let lines: Vec<Vec<CalcValue>> = (0..len).map(|i| line(&array, i, by_col)).collect();
    // Each line's group: the first earlier line equal to it, or itself.
    let mut buckets: HashMap<String, Vec<usize>> = HashMap::new();
    let mut group = vec![0usize; len];
    for i in 0..len {
        let key = bucket(&lines[i]).ok_or_else(|| site.not_modelled(None))?;
        let members = buckets.entry(key).or_default();
        let mut found = None;
        for &j in members.iter() {
            let mut equal = true;
            for (x, y) in lines[i].iter().zip(&lines[j]) {
                if !same(site, x, y)? {
                    equal = false;
                    break;
                }
            }
            if equal {
                found = Some(j);
                break;
            }
        }
        match found {
            Some(j) => group[i] = j,
            None => {
                group[i] = i;
                members.push(i);
            }
        }
    }
    let mut counts = vec![0usize; len];
    for &g in &group {
        counts[g] += 1;
    }
    let kept: Vec<usize> = (0..len)
        .filter(|&i| group[i] == i && (!exactly_once || counts[i] == 1))
        .collect();
    if kept.is_empty() {
        return fail(WorksheetErrorCode::Calc);
    }
    let (out_rows, out_cols) = if by_col {
        (rows, kept.len())
    } else {
        (kept.len(), cols)
    };
    let mut cells = Vec::with_capacity(out_rows * out_cols);
    for r in 0..out_rows {
        for c in 0..out_cols {
            let value = if by_col {
                &lines[kept[c]][r]
            } else {
                &lines[kept[r]][c]
            };
            cells.push(value.clone());
        }
    }
    Ok(CalcValue::array(
        CalcArray::new(
            ArrayShape {
                rows: out_rows,
                cols: out_cols,
            },
            cells,
        )
        .expect("a shape of its cells"),
    ))
}
text_bucket · 14 lines · calc-func/src/catalogue/comparison.rs on GitHub
/// A key under which texts the comparison operators hold equal always fall together, for a
/// grouping that compares only the texts sharing a key (UNIQUE): the weights the second pass
/// compares but the diacritics (texts the first pass holds equal differ in ASCII case alone,
/// which the weights ignore), or `None` when the collation builds none for `text`.
pub(super) fn text_bucket(text: &[u16]) -> Option<Vec<u8>> {
    if text.iter().any(|&code| sort_key::refused(code)) {
        return None;
    }
    let parts = sort_key::parts(text).ok()?;
    let mut key = parts.primary;
    key.push(1);
    key.extend_from_slice(&parts.rest);
    Some(key)
}
refused · 8 lines · calc-func/src/catalogue/comparison/sort_key.rs on GitHub
/// Whether a unit has no measured comparison route. Partial Jamo groups reach
/// `parts`, which still refuses a comparison needing their unknown bytes.
pub(super) fn refused(code: u16) -> bool {
    match code {
        0x0001..=0x007F => false,
        _ => matches!(table().weights[usize::from(code)], Weight::Refused),
    }
}

Kernels shared with other functions

Argument and result plumbing

No implementation has been published with a Handbook test suite for this function.

Version differences

No comparison between specific Excel builds is recorded here yet. This does not establish that all builds behave alike.

Alternatives

Relationships

  • SORT orders; UNIQUE deduplicates. SORT(UNIQUE(x)) is the canonical pairing.
  • FILTER selects by predicate; UNIQUE selects by novelty. UNIQUE(FILTER(...)) is the common pipeline.
  • COUNTIF is the classic pre-dynamic-array way to compute both behaviours: COUNTIF(range, x) = 1 is the exactly_once test, and a running COUNTIF over the prefix is the distinct test. Anyone maintaining older workbooks will meet these.
  • TOCOL(…, 1) removes blanks, which is often what a reader actually wanted when they reached for UNIQUE on a padded column.
  • Data ▸ Remove Duplicates is the destructive command form; UNIQUE is the formula form and leaves the source intact.

Named variants

No named variants are recorded yet.

Suggest a correction or contribute an implementation · For agents