Text functions

NUMBERVALUE

Matches all 8 recorded answers

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

Local names in 40 languages
LanguageName in Excel
ArabicNUMBERVALUE
BulgarianNUMBERVALUE
CzechNUMBERVALUE
DanishTALVÆRDI
GermanNUMBERVALUE
GreekNUMBERVALUE
English (Australia)NUMBERVALUE
English (United Kingdom)NUMBERVALUE
English (United States)NUMBERVALUE
SpanishVALOR.NUMERO
EstonianNUMBERVALUE
FinnishNROARVO
FrenchVALEURNOMBRE
HebrewNUMBERVALUE
CroatianNUMBERVALUE
HungarianSZÁMÉRTÉK
IndonesianNUMBERVALUE
ItalianNUMBERVALUE
JapaneseNUMBERVALUE
KoreanNUMBERVALUE
LithuanianNUMBERVALUE
LatvianNUMBERVALUE
NorwegianNUMBERVALUE
DutchNUMERIEKE.WAARDE
PolishWARTOŚĆ.LICZBOWA
Portuguese (Brazil)NUMBERVALUE
Portuguese (Portugal)VALOR.NÚMERO
RomanianNUMBERVALUE
RussianЧЗНАЧ
SlovakNUMBERVALUE
SlovenianNUMBERVALUE
Serbian (Latin)NUMBERVALUE
SwedishTALVÄRDE
ThaiNUMBERVALUE
TurkishSAYIDEĞERİ
UkrainianNUMBERVALUE
VietnameseNUMBERVALUE
Chinese (Simplified)NUMBERVALUE
Chinese (Hong Kong)NUMBERVALUE
Chinese (Traditional)NUMBERVALUE

Names from Microsoft's support site.

What it does

NUMBERVALUE(text, [decimal_separator], [group_separator])

NUMBERVALUE(text, [decimal_separator], [group_separator]) parses text as a number using the two separator characters you name, instead of the two the host locale would have chosen.

That is the entire point of the function, and it is worth stating sharply: VALUE asks the machine what a decimal point looks like; NUMBERVALUE lets the formula say so. A workbook that reads "1.234,56" with NUMBERVALUE(A1, ",", ".") gets 1234.56 on every machine on earth. The same string through VALUE gets a different answer, or an error, depending on where the workbook is opened.

The parse Microsoft documents is not a plain decimal scan. Three rules make it its own grammar:

  1. Whitespace is ignored anywhere in text, including in the middle. " 3 000 " parses as 3000 even without a group separator.
  2. Group separators are cosmetic before the decimal separator and fatal after it. They may appear anywhere in the integer part — Microsoft's page does not require them to be in groups of three — but a group separator occurring after the decimal separator is an error.
  3. Percent signs are trailing and cumulative. A % in text divides the result by 100, and multiple percent signs compound: "9%%" is 9 / 100 / 100 = 0.0009.

The third rule is the one that surprises people. It is not a typo tolerance; it is arithmetic.

Arguments

ArgumentMeaningDefault
textThe string to parse. Required.—
decimal_separatorThe character separating the integer and fractional parts.The host locale's decimal separator
group_separatorThe character separating groups within the integer part.The host locale's group separator

Two documented details about the separator arguments:

  • Only the first character of each is used. Passing ",." as a separator is not an error and not a two-character separator; it is ,.

If you are using NUMBERVALUE for the reason it was added, supply both separators.

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

Returns Number.

Documented boundary behaviours:

  • Empty text returns 0, not an error and not an empty result. This is a real difference from VALUE, which does not treat the empty string as zero.
  • Whitespace-only text follows from the ignore-whitespace rule and the empty-string rule, though Microsoft states the two rules separately rather than combining them.

Empty cells, missing arguments and incoming error values follow the shared call model; see Coercion and lifting. Nothing here is specific to NUMBERVALUE except the empty-string-is-zero rule above, which is a function-level decision rather than an engine one.

Errors

As documented on Microsoft's NUMBERVALUE page:

ErrorCondition
#VALUE!decimal_separator and group_separator are the same character.
#VALUE!A group separator occurs after the decimal separator in text.
#VALUE!Any argument is not valid — including text that does not parse under the given separators.

The Handbook has not verified this list against Excel, and in particular has not established whether "not valid" covers cases such as an empty-string separator argument or a separator that is a digit.

How to implement

Notes for implementers

The locale dependence does not disappear when the separators are supplied — it only shrinks. Currency symbols, sign conventions and non-ASCII digits are not covered by the two separator arguments, and Microsoft's page does not say what the parser does with them. An implementation has to decide, and should record the decision as a decision.

Cumulative percent handling has to be applied after the numeric parse, not as a prefix scan, and each % divides once. Implementations that treat % as a flag rather than a count will be right on "9%" and wrong on "9%%".

The "only the first character is used" rule for separators means the separator arguments cannot be validated by length. They are validated by comparison — the two must differ — and that comparison is on the first characters, not the whole strings.

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.

NUMBERVALUE · 1 lines · calc-func/src/catalogue/text_format.rs on GitHub
5 numbervalue_fn "NUMBERVALUE" (1, 3) [V "text", V "decimal_separator", V "group_separator"] |s, a, sv| { numbervalue(s, a) }
numbervalue · 60 lines · calc-func/src/catalogue/text_format.rs on GitHub
/// NUMBERVALUE: the separators default to the locale's.
fn numbervalue(site: &Site<'_>, a: &[CalcValue]) -> Step<CalcValue> {
    let text = text_arg(site, a, 0)?;
    if text.len_utf16_code_units() > MAX_READER_TEXT {
        return fail(WorksheetErrorCode::Value);
    }
    let separator = |index: usize, default: &str| -> Step<Option<char>> {
        let given_text;
        let text = if given(a, index) {
            given_text = text_arg(site, a, index)?.to_string_lossy();
            given_text.as_str()
        } else {
            default
        };
        match text.chars().next() {
            Some(c) => Ok(Some(c)),
            None => fail(WorksheetErrorCode::Value),
        }
    };
    let (decimal_default, group_default) = if given(a, 1) && given(a, 2) {
        (".", ",")
    } else {
        let (_, profile) = profile(site)?;
        (profile.decimal_separator, profile.thousands_separator)
    };
    let decimal = separator(1, decimal_default)?;
    let group = separator(2, group_default)?;
    let host_profile = site.environment().and_then(|environment| environment.profile.as_ref())
        .filter(|profile| value_text::measured_host_reader(profile)
            && decimal == Some('.') && group == Some(' '));
    if host_profile.is_some_and(|profile| text.to_string_lossy().chars()
        .any(|ch| value_text::measured_space(ch, profile))) {
        return fail(WorksheetErrorCode::Value);
    }
    let result = match k::numbervalue(&text, decimal, group) {
        // A numeric range error is final, including a zero coefficient. The host
        // grammar fallback must not admit the same rejected exponent as VALUE.
        Err(k::NumberValueError::Range) => return fail(WorksheetErrorCode::Value),
        result => result.map_err(|_| WorksheetErrorCode::Value),
    };
    if result.is_ok() || host_profile.is_none() || !matches!(a[0].core_ref(), CoreRef::Text(_)) {
        return out_number(site, result);
    }
    // The measured host admits the same currency, digit and date grammar as
    // VALUE after ASCII spaces and trailing percents are removed. Keep the
    // existing numeric parser and its percent arithmetic for its prior forms.
    let compact: String = text.to_string_lossy().chars().filter(|&ch| ch != ' ').collect();
    // Slash/colon forms have no new service evidence; keep their prior route.
    if compact.contains(['/', ':']) {
        return out_number(site, result);
    }
    let base = compact.trim_end_matches('%');
    let percent_count = (compact.len() - base.len()) as u32;
    let value = super::kit::text_value(base);
    match read_value(site, &[value], 0) {
        Ok(reading) => site.number(k::numbervalue_percent(reading.value(), percent_count)),
        Err(Exit::Error(WorksheetErrorCode::Value)) => out_number(site, result),
        Err(other) => Err(other),
    }
}
out_number · 6 lines · calc-func/src/catalogue/text_format.rs on GitHub
fn out_number(site: &Site<'_>, result: Result<f64, WorksheetErrorCode>) -> Step<CalcValue> {
    match result {
        Ok(n) => site.number(n),
        Err(code) => fail(code),
    }
}
numbervalue · 90 lines · calc-func/src/ported/text_more.rs on GitHub
pub(crate) fn numbervalue(
    text: &ExcelText,
    decimal_separator: Option<char>,
    group_separator: Option<char>,
) -> Result<f64, NumberValueError> {
    if decimal_separator.is_some() && decimal_separator == group_separator {
        return Err(NumberValueError::Syntax);
    }
    let mut raw: String = text
        .to_string_lossy()
        .chars()
        .filter(|ch| !ch.is_whitespace())
        .collect();
    if raw.is_empty() {
        // NUMBERVALUE("") is 0 (Microsoft's NUMBERVALUE page).
        return Ok(0.0);
    }
    let mut percent_count = 0u32;
    while raw.ends_with('%') {
        raw.pop();
        percent_count += 1;
    }
    if raw.is_empty() {
        return Err(NumberValueError::Syntax);
    }
    let mut chars = raw.chars().peekable();
    let mut sign = 1.0;
    if let Some(ch) = chars.peek().copied()
        && (ch == '+' || ch == '-')
    {
        sign = if ch == '-' { -1.0 } else { 1.0 };
        chars.next();
    }
    let mut normalized = String::new();
    let mut seen_digit = false;
    let mut seen_decimal = false;
    let mut seen_exponent = false;
    for ch in chars {
        if ch.is_ascii_digit() {
            normalized.push(ch);
            seen_digit = true;
            continue;
        }
        if decimal_separator == Some(ch) {
            if seen_decimal || seen_exponent {
                return Err(NumberValueError::Syntax);
            }
            normalized.push('.');
            seen_decimal = true;
            continue;
        }
        if group_separator == Some(ch) && !seen_decimal {
            continue;
        }
        if matches!(ch, 'e' | 'E') && seen_digit && !seen_exponent {
            normalized.push('e');
            seen_exponent = true;
            continue;
        }
        if matches!(ch, '+' | '-') && normalized.ends_with('e') {
            normalized.push(ch);
            continue;
        }
        return Err(NumberValueError::Syntax);
    }
    if !seen_digit {
        return Err(NumberValueError::Syntax);
    }
    let (mantissa, exponent) = if let Some((mantissa, exponent)) = normalized.split_once('e') {
        if matches!(exponent, "" | "+" | "-") {
            return Err(NumberValueError::Syntax);
        }
        (mantissa, exponent.parse::<i64>().map_err(|_| NumberValueError::Range)?)
    } else {
        (normalized.as_str(), 0)
    };
    let fraction_digits = mantissa.split_once('.').map_or(0, |(_, fraction)| fraction.len());
    let digits = mantissa.replace('.', "");
    let scale = exponent.checked_sub(fraction_digits as i64).ok_or(NumberValueError::Range)?;
    let position = scale.checked_add(digits.trim_start_matches('0').len() as i64)
        .ok_or(NumberValueError::Range)?;
    // NUMBERVALUE checks this range even for an all-zero coefficient and before
    // applying trailing percents (fn12's explicit-separator Excel controls).
    if !(-308..=308).contains(&position) {
        return Err(NumberValueError::Range);
    }
    let mut value = crate::value_text::decimal(&digits, scale).ok_or(NumberValueError::Range)?;
    value *= sign;
    Ok(numbervalue_percent(value, percent_count))
}
numbervalue_percent · 7 lines · calc-func/src/ported/text_more.rs on GitHub
/// The existing NUMBERVALUE percent arithmetic, shared with its host reader.
pub(crate) fn numbervalue_percent(mut value: f64, percent_count: u32) -> f64 {
    for _ in 0..percent_count {
        value /= 100.0;
    }
    value
}

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

  • VALUE is the locale-following text-to-number conversion. The pair is not legacy/modern — VALUE is not superseded and Microsoft continues to document both. They differ in one axis only: where the separator characters come from. Reach for NUMBERVALUE whenever the text's format is a property of the data rather than of the machine — parsed files, web sources, fixed feed formats.
  • TEXT is the inverse direction and has the mirror-image locale problem: TEXT takes a format string that is itself interpreted in a language. There is no TEXT counterpart to NUMBERVALUE's explicit separators; see TEXT.
  • Implicit coercion — what happens when text lands in a numeric argument slot — is a third, separate rule set, described in Coercion and lifting. It is neither VALUE nor NUMBERVALUE, and the chapter is explicit that Excel's full text-to-number recognizer is broader than the pinned baseline.
  • DOLLAR and FIXED produce formatted numeric text that NUMBERVALUE can often read back, given the right separators.

Named variants

No named variants are recorded yet.

Suggest a correction or contribute an implementation · For agents