Haiku BDateFormat and BTimeFormat: Localized Display and Time Boundaries
Format Haiku dates and times with Locale Kit conventions, explicit time zones, field-aware editing, checked buffers, and documented API caveats.
Localized date and time output is more than changing month names. Users expect their locale’s ordering, separators, long or short style, calendar conventions, and time-zone context. Haiku’s BDateFormat and BTimeFormat provide Locale Kit formatting and parsing APIs. They should be used for presentation and user input, while application storage and protocol formats remain explicit machine-readable values.
Construct formatters from the right conventions
BDateFormat can be created with a BLocale pointer (null selects the default locale), or with a BLanguage and BFormattingConventions. BTimeFormat has a default constructor for current system conventions and a language/conventions constructor. For a normal user-facing application, use the current locale so date and time order follows the user’s preferences. Use an explicit language/convention pair only when rendering a document or export for a deliberately selected locale.
Do not infer the active locale from a language code alone. Language and formatting conventions are separate pieces of state: two users who speak the same language may use different date order, separators, or 12/24-hour conventions. If the UI allows the locale to change while running, refresh formatters and redraw dependent labels rather than keeping stale strings indefinitely.
Both classes support styles that choose short or longer output. Ask the formatter for the style you need and allow the resulting string to vary in length. Never allocate a fixed display width based on one locale’s output. Layout should accommodate longer month names and reordered fields, and truncation should not silently turn a date into an ambiguous value.
Keep instants, calendar dates, and clock times distinct
A Unix time_t represents an instant in seconds, while BDate represents a calendar date and BTimeFormat is concerned with displayed time fields. These are not interchangeable. A date-only value such as a birthday should not be shifted to another date by applying a time zone; an instant such as a message timestamp should be rendered in the intended zone; a recurring local time such as “09:00 every weekday” needs an explicit calendar and zone policy.
BDateFormat::Format(BString&, time_t, style, timeZone) accepts an optional BTimeZone; the documentation says a null pointer uses the system default zone. The date-only BDate overload formats the calendar fields supplied by the caller. Choose the overload that matches your data model. Do not manufacture a midnight UTC timestamp for a date-only field and then wonder why it displays as the previous day in a western time zone.
Time-zone conversion is not string substitution. A daylight-saving transition can create missing or repeated local clock times. If parsing user-entered local time, define how ambiguous or nonexistent times should be resolved and validate the resulting value with an explicit zone-aware policy. A formatted label is not sufficient to reconstruct the original instant without locale, zone, and ambiguity context.
Use the status-returning output path
The BString format overloads return status_t, which can report allocation or locale-related errors. Check that status before using the output. The raw character-array overloads return a byte count or an error such as B_BAD_VALUE when the destination is too small. Use the returned length, allocate capacity for UTF-8 bytes rather than characters, and do not assume a localized month name contains one byte per displayed character.
BString label;
status_t status = dateFormat.Format(label, eventTime,
B_SHORT_DATE_FORMAT, &displayTimeZone);
if (status != B_OK)
return status;
DrawDateLabel(label.String());
The snippet illustrates the time-zone-aware BString overload. eventTime must be an actual instant in the representation expected by the API, and displayTimeZone must remain valid for the call. A date-only model should use the BDate overload instead. Never pass a local-time number whose interpretation is undocumented and then label it UTC.
For times, the current upstream BTimeFormat implementation formats its time_t argument through ICU as a millisecond timestamp derived from seconds, and its BString overload can take an explicit BTimeZone. There is a documentation inconsistency: the Doxygen text for the raw char* overload describes the input as seconds since midnight, while the current implementation treats it like the other time_t overloads. Do not silently depend on the disputed raw-buffer semantics. For a release you ship, verify the exact source/header contract and regression-test the selected overload; if the caller has a time-of-day rather than an instant, convert it using an explicit date/time-zone model first.
Build editors from fields, not string positions guessed by locale
BDateFormat offers an overload that returns field positions and GetFields() to describe the date fields for a style. These APIs exist to support locale-aware editing: month, day, year, and separators can appear in different orders. BTimeFormat similarly exposes field positions and time-field metadata. If building an editor from formatted text, use the returned positions and field descriptors rather than hardcoding slash-separated MM/DD/YYYY or assuming hours always precede minutes.
The returned arrays are allocated for the caller according to the documentation; free them with the allocator contract used by the target release. Check status before using either array, handle zero or unexpected field counts, and rebuild the editor when the locale/style changes. Do not split UTF-8 by guessed character indices: the formatter’s positions and the string’s encoding must be interpreted according to the exact API contract.
For most applications, a native date/time input control or structured fields are safer than making a localized display string editable. Keep the underlying BDate, instant, or time-zone-aware value as the model, then render it. Editing should update the model only after each field is validated; do not parse a partial string on every keystroke and accidentally accept an incomplete date.
Parsing is for localized user input, not interchange
The formatters expose Parse() methods, but localized parsing should be limited to input that was intended for the current locale. A string such as 03/04/2026 is ambiguous across locales. Ask the user for context, use a format that includes an unambiguous month name where appropriate, or present structured controls. Never use a localized date label as a database key, network field, or durable interchange format.
When accepting input, validate the parse status and the resulting value, including leap days, month lengths, and time-zone transitions. A parse success means the formatter could interpret the text under its conventions; it does not establish that the value is valid for the business workflow. Preserve the original input only if the product needs to explain a correction or support round-tripping.
Cache and concurrency considerations
Formatting is suitable for ordinary UI and document work, but do not perform expensive repeated formatting in a real-time media callback. Cache display strings when useful, keyed by value, locale, style, and time zone; invalidate them when any of those inputs changes. Do not cache only by timestamp because the same instant renders differently in different zones.
If formatters are shared across threads, follow the thread-safety guarantees for the exact classes and version. A conservative design gives each worker its own formatter or serializes access. Keep locale changes and cache generations explicit, so an asynchronous completion for the previous locale cannot overwrite the current label.
Verification checklist
Test short and long styles, several locales with different date order, 12- and 24-hour conventions, non-ASCII month names, leap day, year boundaries, empty and undersized buffers, explicit versus system time zone, and daylight-saving transitions. Include a date-only value and an absolute instant so the tests prove the two data models remain separate. For BTimeFormat, include a test that distinguishes epoch seconds from seconds since midnight until the upstream documentation and implementation agree.
BDateFormat and BTimeFormat should render human-readable values, not define storage formats. Use the active conventions intentionally, pass a time zone for instants, retain structured values underneath, check buffer/status results, and do not paper over the current time-format contract discrepancy.
Related:
- Haiku’s Locale Kit: Catalogs, Formatting, and Runtime Language Selection
- Haiku Time Settings: Time Zones, RTC Mode, and NTP Sync
Sources: