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
| Language | Name in Excel |
|---|---|
| Arabic | UNIQUE |
| Bulgarian | UNIQUE |
| Czech | UNIQUE |
| Danish | ENTYDIGE |
| German | UNIQUE |
| Greek | UNIQUE |
| English (Australia) | UNIQUE |
| English (United Kingdom) | UNIQUE |
| English (United States) | UNIQUE |
| Spanish | UNICOS |
| Estonian | UNIQUE |
| Finnish | AINUTLAATUINEN |
| French | UNIQUE |
| Hebrew | UNIQUE |
| Croatian | UNIQUE |
| Hungarian | EGYEDI |
| Indonesian | UNIQUE |
| Italian | UNIQUE |
| Japanese | UNIQUE |
| Korean | UNIQUE |
| Lithuanian | UNIQUE |
| Latvian | UNIQUE |
| Norwegian | UNIQUE |
| Dutch | UNIEK |
| Polish | UNIKATOWE |
| Portuguese (Brazil) | UNIQUE |
| Portuguese (Portugal) | EXCLUSIVO |
| Romanian | UNIQUE |
| Russian | УНИК |
| Slovak | UNIQUE |
| Slovenian | UNIQUE |
| Serbian (Latin) | UNIQUE |
| Swedish | UNIK |
| Thai | UNIQUE |
| Turkish | BENZERSİZ |
| Ukrainian | UNIQUE |
| Vietnamese | UNIQUE |
| 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. FromA, B, A, Cyou getA, B, C.exactly_once=TRUE: unique in the database sense. Only rows that appear exactly once survive. FromA, B, A, Cyou getB, C—Ais 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]).
| Argument | Default | Meaning |
|---|---|---|
array | required | The range or array from which to return unique rows or columns |
by_col | FALSE | TRUE compares columns; FALSE or omitted compares rows |
exactly_once | FALSE | TRUE 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
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 thatUNIQUEuses it. - Blanks are values. A blank row is a row, and it will appear in the distinct set. This
surprises readers who apply
UNIQUEto a whole column and get a blank entry from the unused rows below the data — the usual remedy beingTRIMRANGEor aFILTERfirst. - 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/TYPEprobe on theROWSpage explains why that distinction is observable. - Dynamic-array publication and
#SPILL!are host-side adaptation, notUNIQUEsemantics.
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_oncerequires 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 GitHub5 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 GitHubfn 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
preparedcatalogue/arraysprepared_onecatalogue/arraysas_arraycatalogue/arraysintegercatalogue/arraysflagcatalogue/arrayskindcatalogue/arraysordercatalogue/arraysnumberscatalogue/comparisonin_bandcatalogue/comparisonRounded::upcatalogue/comparisonRoundings::ofcatalogue/comparisonRoundings::disjointcatalogue/comparisontextscatalogue/comparisoncomparecatalogue/comparison/collationweighedcatalogue/comparison/collationmeasured_iteration_extensioncatalogue/comparison/collationtablecatalogue/comparison/sort_keydecode64catalogue/comparison/sort_keyReader::bytecatalogue/comparison/sort_keyReader::varintcatalogue/comparison/sort_keyReader::donecatalogue/comparison/sort_keyin_rangescatalogue/comparison/sort_keybuildcatalogue/comparison/sort_keyhan_basecatalogue/comparison/sort_keyderivecatalogue/comparison/sort_keyshared_jamo_tailscatalogue/comparison/sort_keyordinary_jamo_prefixcatalogue/comparison/sort_keyindependent_jamo_suffixcatalogue/comparison/sort_keyjamo_leading_primarycatalogue/comparison/sort_keyleading_ordercatalogue/comparison/sort_keyBefore::separate_specialcatalogue/comparison/sort_keyGathered::kanacatalogue/comparison/sort_keyGathered::unitcatalogue/comparison/sort_keypartscatalogue/comparison/sort_keydiacritic_ordercatalogue/comparison/sort_keydiacriticscatalogue/comparison/sort_keylow32decimalNatural::from_u64decimalNatural::pow10decimalNatural::normalizedecimalNatural::is_zerodecimalNatural::mul_add_smalldecimalNatural::times_pow10decimalNatural::times_pow5decimalNatural::times_pow2decimalExactDecimal::binarydecimalcoerce_prepared_to_numberported/dynamic_array_reshape_family
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
SORTorders;UNIQUEdeduplicates.SORT(UNIQUE(x))is the canonical pairing.FILTERselects by predicate;UNIQUEselects by novelty.UNIQUE(FILTER(...))is the common pipeline.COUNTIFis the classic pre-dynamic-array way to compute both behaviours:COUNTIF(range, x) = 1is theexactly_oncetest, and a runningCOUNTIFover 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 forUNIQUEon a padded column.- Data ▸ Remove Duplicates is the destructive command form;
UNIQUEis 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