Profiles

meant-format (Rust)

The Rust crate for the amount tag: AmountFormat, IntType and the error codes, matched case by case against @meantjs/format.

rustcrates/meant-format/src/amount.rs
use meant_format::{AmountFormat, IntType};

// in a function that returns Result<_, meant_format::FormatError>
let usdc = AmountFormat::new(6).with_symbol("USDC")?.with_candid(IntType::Nat64);
assert_eq!(usdc.parse("1.5 usdc")?, "1500000");
assert_eq!(usdc.display_u128(1_500_000)?, "1.5 USDC");

meant-format converts between the amounts people write and the base units a contract call carries, for the amount tag of spec §5.3 (and §15 for EVM positions). Base units are canonical decimal strings, so nothing passes through a float and nothing is rounded, wrapped or clamped. For the same input it gives the same answer as @meantjs/format: parse and display values shows both side by side, values and exact conversion explains the rules, and the JavaScript package's exports are on the JavaScript API reference.

Which version this page describes

This page describes 0.2.0, published on crates.io on 2026-10-29 and the version in the repository's manifest. Depend on it exactly, as on every pre-1.0 crate here:

tomlCargo.toml
[dependencies]
meant-format = "=0.2.0"

0.1.0 (2026-10-27) has the ten Candid integer types, an IntType that is not #[non_exhaustive] with ALL as an array, and no threshold. 0.2.0 is breaking, as a 0.x release may be: it adds the EVM ABI integer types and with_threshold (CHANGELOG).

The crate#

ItemKindWhat it is
AmountFormatstructThe amount tag at one position: decimals, symbol, integer type, threshold.
IntTypeenum, #[non_exhaustive]The 70 integer types an amount can have: 10 Candid, 60 EVM ABI.
FormatErrorstructA refused value: a Code and a message.
Codeenum, #[non_exhaustive]The 8 stable error codes.
MAX_INPUT_UTF16_LENconst1024: the longest text the crate reads, in UTF-16 code units.
  • No dependencies. The decimal arithmetic is on strings, so no big-integer library is needed. serde_json is a dev-dependency, for the parity vectors, and never reaches a consumer.
  • no_std. The crate is #![no_std] and needs only alloc. It is #![forbid(unsafe_code)].
  • The std feature, on by default, only adds impl std::error::Error for FormatError. core::error::Error would make the feature unnecessary, but it needs Rust 1.81. Turn default features off for a target with no standard library; CI checks the library on thumbv7em-none-eabihf that way.
  • MSRV. rust-version = "1.78"; the MSRV workflow runs the crate's tests on Rust 1.78.0.
  • Stability. Error codes are the stable surface. Message text is written for a person or a model and may change between releases, and in 0.x so may the text display produces.

AmountFormat#

AmountFormatstruct
rustcrates/meant-format/src/amount.rs
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AmountFormat {
    decimals: u8,
    symbol: Option<String>,
    candid: IntType,
    threshold: Option<String>,
    threshold_message: Option<String>,
}

The fields are private: build one with new and the with_* methods, which take and return it by value. A format with a symbol or a threshold is checked when it is built, so a format you hold is always valid.

AmountFormat::DEFAULT_THRESHOLD_MESSAGEconst
rustcrates/meant-format/src/amount.rs
pub const DEFAULT_THRESHOLD_MESSAGE: &'static str = "Unlimited";

What display shows for a value at or above the threshold when with_threshold gives no message: ERC-7730's default for a token amount.

AmountFormat::newfn
rustcrates/meant-format/src/amount.rs
pub fn new(decimals: u8) -> Self

A token with decimals decimals (0 to 255), no symbol, at a nat position, with no threshold. It cannot fail.

AmountFormat::with_symbolfn
rustcrates/meant-format/src/amount.rs
pub fn with_symbol(mut self, symbol: &str) -> Result<Self, FormatError>

Sets the symbol that display appends and parse accepts. It must be 1 to 32 characters, with no surrounding whitespace and no control or invisible characters (Unicode categories Cc, Cf, Zl and Zp, from a Unicode 16.0 table), since it is shown to the person approving a call. Otherwise Code::Arg. A canister's icrc1_symbol answer is untrusted text: this is where a right-to-left override in it is refused.

AmountFormat::with_candidfn
rustcrates/meant-format/src/amount.rs
pub fn with_candid(mut self, candid: IntType) -> Self

Sets the integer type of the position, Candid's or the EVM ABI's. It bounds the value and decides whether it may be negative.

AmountFormat::with_candid_namefn
rustcrates/meant-format/src/amount.rs
pub fn with_candid_name(self, name: &str) -> Result<Self, FormatError>

with_candid by name, as a resolved profile spells it: "nat64", or "uint256" at an EVM position. Names are case-sensitive. A name that is not an integer type is Code::TagType, with the message the amount tag applies to nat, int, nat8, nat16, nat32, nat64, int8, int16, int32, int64, uint8 to uint256, int8 to int256, not <name>.

AmountFormat::with_thresholdfn0.2.0
rustcrates/meant-format/src/amount.rs
pub fn with_threshold(
    mut self,
    threshold: &str,
    message: Option<&str>,
) -> Result<Self, FormatError>

A threshold in base units at or above which display shows message and the symbol instead of the number: an ERC-20 approval of 2256 − 1 shows as "Unlimited USDC". None uses DEFAULT_THRESHOLD_MESSAGE. Parsing is unchanged. threshold must be a canonical decimal integer, not negative, of at most 78 digits (2256 − 1 has 78); message follows the rules of with_symbol. Otherwise Code::Arg.

rustcrates/meant-format/src/amount.rs
let max = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
let usdc = AmountFormat::new(6)
    .with_symbol("USDC")?
    .with_candid(IntType::Uint256)
    .with_threshold(max, None)?;
assert_eq!(usdc.display(max)?, "Unlimited USDC");
assert_eq!(usdc.display("1500000")?, "1.5 USDC");
gettersfn
rustcrates/meant-format/src/amount.rs
pub fn decimals(&self) -> u8
pub fn symbol(&self) -> Option<&str>
pub fn candid(&self) -> IntType
pub fn threshold(&self) -> Option<&str>
pub fn threshold_message(&self) -> Option<&str>

What the format was built with. threshold_message is the message given, or DEFAULT_THRESHOLD_MESSAGE, and None when there is no threshold.

AmountFormat::parsefn
rustcrates/meant-format/src/amount.rs
pub fn parse(&self, input: &str) -> Result<String, FormatError>

Reads an amount a person wrote, such as "1.5" or "1.5 ICP", and returns the exact base-unit value as a canonical decimal string. Accepted: an optional -, digits, optionally a . with digits after it, and optionally whitespace and the token's symbol, in any ASCII case. Surrounding whitespace is ignored; it is ECMAScript's whitespace set, which includes U+FEFF and excludes U+0085, as in the reference. The checks, in order:

InputCode
Longer than MAX_INPUT_UTF16_LENEE_TOO_LONG
Thousands separators, exponents, a leading +, "1.", ".5"EE_SYNTAX
A minus sign at an unsigned position, "-0" includedEE_NEGATIVE
A symbol that is not the token's, or any symbol when the token has noneEE_SYMBOL_MISMATCH
More fractional digits than decimals, even zerosEE_EXCESS_PRECISION
A value outside the integer typeEE_RANGE
rustcrates/meant-format/src/amount.rs
let eth = AmountFormat::new(18).with_symbol("ckETH")?;
assert_eq!(eth.parse("1.000000000000000001")?, "1000000000000000001");

let byte = AmountFormat::new(0).with_candid(IntType::Nat8);
assert_eq!(byte.parse("256").unwrap_err().code(), Code::Range);
AmountFormat::displayfn
rustcrates/meant-format/src/amount.rs
pub fn display(&self, base_units: &str) -> Result<String, FormatError>

Shows a base-unit value as a person reads it: the decimal point placed by decimals, trailing fractional zeros removed, then a space and the symbol if there is one. "150000000" with 8 decimals is "1.5 ICP"; "1" is "0.00000001 ICP". A value at or above the threshold shows as the threshold message and the symbol. base_units must be a canonical decimal integer (no leading zeros, no +, no "-0", no separators) or the error is EE_SYNTAX; that rules out candid::Nat's Display, which writes 1_500_000. A value outside the integer type is EE_RANGE, and text over the length bound EE_TOO_LONG.

AmountFormat::display_u128, display_i128fn
rustcrates/meant-format/src/amount.rs
pub fn display_u128(&self, base_units: u128) -> Result<String, FormatError>
pub fn display_i128(&self, base_units: i128) -> Result<String, FormatError>

display for a value held as a native integer. The range check still applies: display_i128(-1) at a nat position is EE_RANGE.

IntType#

IntTypeenum
rustcrates/meant-format/src/int_type.rs
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum IntType {
    Nat, Int, Nat8, Nat16, Nat32, Nat64, Int8, Int16, Int32, Int64,
    Uint8, Uint16, Uint24, Int24, Uint32, Uint40, Int40, // … to
    Uint248, Int248, Uint256, Int256,
}

The integer types the amount tag applies to. The enum is generated by the int_types! macro in src/int_type.rs, one row per type with its name, sign and inclusive bounds; it is shown here abridged. More types may come, so it is #[non_exhaustive]: a match on it needs a _ arm.

VariantsName in a profileRange
Natnat0 and up, unbounded
Intintunbounded, signed
Nat8, Nat16, Nat32, Nat64nat8 … nat640 to 2N − 1
Int8, Int16, Int32, Int64int8 … int64−2N−1 to 2N−1 − 1. Both Candid and ABI types, with the same bounds, so each appears once.
Uint8, Uint16, Uint24 … Uint256 (32 variants)uint8 … uint256, every multiple of 80 to 2N − 1 (EVM ABI)
Int24, Int40, Int48 … Int256 (28 variants)int24 … int256, every multiple of 8 from 24 to 256 except 32 and 64−2N−1 to 2N−1 − 1 (EVM ABI)
IntType::ALLconst
rustcrates/meant-format/src/int_type.rs
pub const ALL: &'static [IntType]

Every type, in the reference's order: the ten Candid types, then the ABI's by width, uint before int. A slice since 0.2.0, so that a later type does not change its type.

IntType methodsfn
rustcrates/meant-format/src/int_type.rs
pub const fn candid_name(self) -> &'static str
pub const fn is_signed(self) -> bool
pub fn from_candid_name(name: &str) -> Option<Self>

candid_name is the name as a resolved profile spells it ("nat64", "uint256"). is_signed says whether the type admits negative values. from_candid_name finds the type with exactly this name, case-sensitive as in Candid and Solidity, or None.

FormatError#

FormatErrorstruct
rustcrates/meant-format/src/error.rs
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FormatError {
    code: Code,
    message: String,
}

impl FormatError {
    pub fn code(&self) -> Code
    pub fn message(&self) -> &str
}

A refused value. Switch on code(), which is stable; message() says how to fix the input and is not stable. Only the crate constructs one. Display writes the code, a colon and the message:

textcrates/meant-format/tests/amount.rs
E_EXCESS_PRECISION: "0.123456789" has 9 fractional digits but this token has 8; amounts are never rounded, so give at most 8

With the std feature, FormatError implements std::error::Error, with no source.

Code#

Codeenum
rustcrates/meant-format/src/error.rs
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Code {
    Syntax,
    Range,
    TooLong,
    Arg,
    TagType,
    ExcessPrecision,
    Negative,
    SymbolMismatch,
}

impl Code {
    pub const fn as_str(self) -> &'static str
}

as_str spells the code as the JavaScript reference does, and Display writes the same. More codes may come with more value tags, so the enum is #[non_exhaustive].

VariantCodeWhen
SyntaxEE_SYNTAXThe text does not match the grammar ("1,5", "1e3", "+1"), or a base-unit value is not a canonical decimal integer.
RangeEE_RANGEThe value is outside the range of its integer type.
TooLongEE_TOO_LONGThe text is longer than MAX_INPUT_UTF16_LEN.
ArgEE_ARGA tag argument is invalid: an empty symbol, a negative threshold.
TagTypeEE_TAG_TYPEThe tag does not apply to this type.
ExcessPrecisionEE_EXCESS_PRECISIONMore fractional digits than the token's decimals. Amounts are never rounded.
NegativeEE_NEGATIVEA negative amount for an unsigned type.
SymbolMismatchEE_SYMBOL_MISMATCHThe symbol in the input is not the token's symbol, or the token has no symbol.

MAX_INPUT_UTF16_LEN#

MAX_INPUT_UTF16_LENconst
rustcrates/meant-format/src/text.rs
pub const MAX_INPUT_UTF16_LEN: usize = 1024;

The longest text parse and display read, counted in UTF-16 code units as JavaScript's length counts them, so that work is bounded and a value the reference refuses for its length is refused here too. Whitespace counts before it is trimmed. An emoji is 4 UTF-8 bytes but 2 UTF-16 units: 512 of them are exactly at the bound, so the text is read and refused only as EE_SYNTAX; one space more is EE_TOO_LONG (tests/amount.rs, the_length_bound_counts_utf16_units).

Parity with @meantjs/format#

tests/parity.rs runs every case of tests/vectors/amount.json, generated from @meantjs/format 0.2.0, and requires the same value, or the same code and the same message: 3,412 cases, the count the README states and the test pins. The vectors are regenerated with node crates/meant-format/tests/vectors/generate-amount.mjs <candid-profile checkout>, never edited by hand. Where the two APIs differ in shape:

  • decimals is a u8, so a value outside 0 to 255 cannot be written at all.
  • A threshold message is an argument of with_threshold, so a message without a threshold, EE_ARG in JavaScript, cannot be written either.
  • The crate has display_u128 and display_i128; JavaScript's displayAmount takes a bigint.
  • Only amount: the other five value tags are in JavaScript only.