@@ -43,6 +43,8 @@ export function buildExplainMetadata(
4343
4444 // Use explicit fields if provided, otherwise fall back to parsed values
4545 const effectiveCurrency = options . explicitCurrency || parsed . currency ;
46+ const hasKnownCurrency = ! ! effectiveCurrency &&
47+ effectiveCurrency !== "UNKNOWN" ;
4648 const effectiveScale = options . explicitScale || parsed . scale ;
4749
4850 // Time scale priority:
@@ -61,7 +63,7 @@ export function buildExplainMetadata(
6163
6264 // FX information
6365 if (
64- effectiveCurrency && options . toCurrency && options . fx &&
66+ hasKnownCurrency && options . toCurrency && options . fx &&
6567 effectiveCurrency !== options . toCurrency
6668 ) {
6769 const rate = options . fx . rates [ effectiveCurrency ] ;
@@ -79,6 +81,9 @@ export function buildExplainMetadata(
7981 }
8082 }
8183
84+ // Determine if FX conversion actually occurred (based on presence of explain.fx)
85+ const didFX = ! ! explain . fx ;
86+
8287 // Magnitude information - only provide when scaling actually occurs
8388 const originalScale = effectiveScale || getScale ( originalUnit ) ;
8489 const targetScale = options . toMagnitude || originalScale ;
@@ -257,74 +262,123 @@ export function buildExplainMetadata(
257262 }
258263 } else {
259264 // Monetary/currency-based units (default behavior)
260- originalUnitString = buildOriginalUnitString (
261- effectiveCurrency ,
262- originalScale ,
263- ) ;
264- // For monetary units, only preserve original time scale if it was explicitly in the unit string
265- // (not just in metadata periodicity). This prevents adding "per month" to stock indicators.
266- const hasTimeInUnit = ! ! parsed . timeScale ;
267-
268- // Use target time scale unless conversion was explicitly blocked
269- // Check if time conversion was blocked (not just "no conversion needed")
270- const timeWasBlocked = explain . periodicity ?. adjusted === false &&
271- explain . periodicity ?. description ?. includes ( "blocked" ) ;
272- const effectiveTargetTime : TimeScale | undefined = timeWasBlocked
273- ? ( hasTimeInUnit && originalTimeScale ? originalTimeScale : undefined )
274- : ( options . toTimeScale ||
275- ( hasTimeInUnit && originalTimeScale ? originalTimeScale : undefined ) ) ;
276-
277- normalizedUnitString = buildNormalizedUnitString (
278- options . toCurrency || effectiveCurrency ,
279- targetScale ,
280- effectiveTargetTime ,
281- ) ;
282-
283- // Build full unit strings with time periods
284- originalFullUnit = buildFullUnitString (
285- effectiveCurrency ,
286- originalScale ,
287- originalTimeScale || undefined ,
288- ) ;
289- normalizedFullUnit = buildFullUnitString (
290- options . toCurrency || effectiveCurrency ,
291- targetScale ,
292- effectiveTargetTime ,
293- ) || normalizedUnitString ;
294-
295- // Per-capita: avoid adding scale label like 'millions' to currency units
296- if ( isPerCapita ) {
297- normalizedUnitString = buildNormalizedUnitString (
298- options . toCurrency || effectiveCurrency ,
299- "ones" ,
300- options . toTimeScale ,
265+ // Special handling for placeholder currency (e.g., "National currency") with no FX:
266+ // Present units using the original label without injecting a target currency.
267+ if ( ! hasKnownCurrency && ! didFX ) {
268+ const base = originalUnit ;
269+ // For placeholder currencies, reflect target time scale if provided
270+ if ( options . toTimeScale ) {
271+ originalUnitString = base ;
272+ normalizedUnitString = `${ base } per ${ options . toTimeScale } ` ;
273+ originalFullUnit = originalUnitString ;
274+ normalizedFullUnit = normalizedUnitString ;
275+ } else {
276+ originalUnitString = base ;
277+ normalizedUnitString = base ;
278+ originalFullUnit = base ;
279+ normalizedFullUnit = base ;
280+ }
281+ } else {
282+ // Known currency or FX occurred: build currency-based labels normally
283+ originalUnitString = buildOriginalUnitString (
284+ effectiveCurrency ,
285+ originalScale ,
301286 ) ;
302- normalizedFullUnit = buildFullUnitString (
303- options . toCurrency || effectiveCurrency ,
304- "ones" ,
305- options . toTimeScale ,
306- ) || normalizedUnitString ;
307- }
287+ // For monetary units, only preserve original time scale if it was explicitly in the unit string
288+ // (not just in metadata periodicity). This prevents adding "per month" to stock indicators.
289+ const hasTimeInUnit = ! ! parsed . timeScale ;
290+
291+ // Use target time scale unless conversion was explicitly blocked
292+ // Check if time conversion was blocked (not just "no conversion needed")
293+ const timeWasBlocked = explain . periodicity ?. adjusted === false &&
294+ explain . periodicity ?. description ?. includes ( "blocked" ) ;
295+ const effectiveTargetTime : TimeScale | undefined = timeWasBlocked
296+ ? ( hasTimeInUnit && originalTimeScale ? originalTimeScale : undefined )
297+ : ( options . toTimeScale ||
298+ ( hasTimeInUnit && originalTimeScale ? originalTimeScale : undefined ) ) ;
299+
300+ // Choose label currency:
301+ // - If FX occurred, use target currency
302+ // - Else, use the known effective currency
303+ const labelCurrency = didFX
304+ ? options . toCurrency
305+ : ( effectiveCurrency || undefined ) ;
308306
309- // Indicators with skipTimeInUnit: omit per-time in unit strings
310- // This includes: stock, balance, capacity, price, percentage, ratio, rate, index, etc.
311- if ( skipTimeInUnitString ) {
312307 normalizedUnitString = buildNormalizedUnitString (
313- options . toCurrency || effectiveCurrency ,
308+ labelCurrency ,
314309 targetScale ,
315- undefined ,
310+ effectiveTargetTime ,
316311 ) ;
312+
313+ // Build full unit strings with time periods
317314 originalFullUnit = buildFullUnitString (
318315 effectiveCurrency ,
319316 originalScale ,
320- undefined ,
317+ originalTimeScale || undefined ,
321318 ) ;
322319 normalizedFullUnit = buildFullUnitString (
323- options . toCurrency || effectiveCurrency ,
320+ labelCurrency ,
324321 targetScale ,
325- undefined ,
322+ effectiveTargetTime ,
326323 ) || normalizedUnitString ;
327324 }
325+
326+ // Per-capita: avoid adding scale label like 'millions' to currency units
327+ if ( isPerCapita ) {
328+ const perCapitaCurrency = didFX
329+ ? options . toCurrency
330+ : ( effectiveCurrency || undefined ) ;
331+ if ( perCapitaCurrency ) {
332+ normalizedUnitString = buildNormalizedUnitString (
333+ perCapitaCurrency ,
334+ "ones" ,
335+ options . toTimeScale ,
336+ ) ;
337+ normalizedFullUnit = buildFullUnitString (
338+ perCapitaCurrency ,
339+ "ones" ,
340+ options . toTimeScale ,
341+ ) || normalizedUnitString ;
342+ } else {
343+ // If currency is unknown, keep the base label without currency
344+ const base = originalUnit ;
345+ normalizedUnitString = options . toTimeScale
346+ ? `${ base } per ${ options . toTimeScale } `
347+ : base ;
348+ normalizedFullUnit = normalizedUnitString ;
349+ }
350+ }
351+
352+ // Indicators with skipTimeInUnit: omit per-time in unit strings
353+ // This includes: stock, balance, capacity, price, percentage, ratio, rate, index, etc.
354+ if ( skipTimeInUnitString ) {
355+ if ( hasKnownCurrency || didFX ) {
356+ const labelCurrency = didFX
357+ ? options . toCurrency
358+ : ( effectiveCurrency || undefined ) ;
359+ normalizedUnitString = buildNormalizedUnitString (
360+ labelCurrency ,
361+ targetScale ,
362+ undefined ,
363+ ) ;
364+ originalFullUnit = buildFullUnitString (
365+ effectiveCurrency ,
366+ originalScale ,
367+ undefined ,
368+ ) ;
369+ normalizedFullUnit = buildFullUnitString (
370+ labelCurrency ,
371+ targetScale ,
372+ undefined ,
373+ ) || normalizedUnitString ;
374+ } else {
375+ // Placeholder currency: keep base without per-time
376+ const base = originalUnit ;
377+ normalizedUnitString = base ;
378+ originalFullUnit = base ;
379+ normalizedFullUnit = base ;
380+ }
381+ }
328382 }
329383
330384 explain . units = {
@@ -619,11 +673,13 @@ export function buildExplainMetadata(
619673 // 🆕 Separate component fields for easy frontend access
620674 if (
621675 ! isNonCurrencyCategory && ! isNonCurrencyDomain &&
622- ( effectiveCurrency || options . toCurrency )
676+ ( hasKnownCurrency || didFX )
623677 ) {
624678 explain . currency = {
625- original : effectiveCurrency ,
626- normalized : options . toCurrency || effectiveCurrency || "USD" ,
679+ original : hasKnownCurrency ? effectiveCurrency : undefined ,
680+ normalized : didFX
681+ ? options . toCurrency
682+ : ( hasKnownCurrency ? effectiveCurrency : undefined ) ,
627683 } ;
628684 }
629685
0 commit comments