55 WalletState ,
66 PageElement
77} from './registry'
8+ import { RateEncodingFactor } from './orderutil'
89
910const parser = new window . DOMParser ( )
1011
@@ -27,20 +28,22 @@ const BipIDs: Record<number, string> = {
2728
2829const BipSymbols = Object . values ( BipIDs )
2930
31+ const log10RateEncodingFactor = Math . round ( Math . log10 ( RateEncodingFactor ) )
32+
3033const intFormatter = new Intl . NumberFormat ( ( navigator . languages as string [ ] ) )
3134
32- const threeSigFigs = new Intl . NumberFormat ( ( navigator . languages as string [ ] ) , {
33- minimumSignificantDigits : 3 ,
34- maximumSignificantDigits : 3
35+ const fourSigFigs = new Intl . NumberFormat ( ( navigator . languages as string [ ] ) , {
36+ minimumSignificantDigits : 4 ,
37+ maximumSignificantDigits : 4
3538} )
3639
37- const fiveSigFigs = new Intl . NumberFormat ( ( navigator . languages as string [ ] ) , {
38- minimumSignificantDigits : 5 ,
39- maximumSignificantDigits : 5
40+ const oneFractionalDigit = new Intl . NumberFormat ( ( navigator . languages as string [ ] ) , {
41+ minimumFractionDigits : 1 ,
42+ maximumFractionDigits : 1
4043} )
4144
4245/* A cache for formatters used for Doc.formatCoinValue. */
43- const decimalFormatters = { }
46+ const decimalFormatters : Record < number , Intl . NumberFormat > = { }
4447
4548/*
4649 * decimalFormatter gets the formatCoinValue formatter for the specified decimal
@@ -51,25 +54,25 @@ function decimalFormatter (prec: number) {
5154}
5255
5356/* A cache for formatters used for Doc.formatFullPrecision. */
54- const fullPrecisionFormatters = { }
57+ const fullPrecisionFormatters : Record < number , Intl . NumberFormat > = { }
5558
5659/*
5760 * fullPrecisionFormatter gets the formatFullPrecision formatter for the
5861 * specified decimal precision.
5962 */
60- function fullPrecisionFormatter ( prec : number ) {
61- return formatter ( fullPrecisionFormatters , prec , prec )
63+ function fullPrecisionFormatter ( prec : number , locales ?: string | string [ ] ) {
64+ return formatter ( fullPrecisionFormatters , prec , prec , locales )
6265}
6366
6467/*
6568 * formatter gets the formatter from the supplied cache if it already exists,
6669 * else creates it.
6770 */
68- function formatter ( formatters : Record < string , Intl . NumberFormat > , min : number , max : number ) : Intl . NumberFormat {
71+ function formatter ( formatters : Record < string , Intl . NumberFormat > , min : number , max : number , locales ?: string | string [ ] ) : Intl . NumberFormat {
6972 const k = `${ min } -${ max } `
7073 let fmt = formatters [ k ]
7174 if ( ! fmt ) {
72- fmt = new Intl . NumberFormat ( ( navigator . languages as string [ ] ) , {
75+ fmt = new Intl . NumberFormat ( locales || navigator . languages as string [ ] , {
7376 minimumFractionDigits : min ,
7477 maximumFractionDigits : max
7578 } )
@@ -259,15 +262,20 @@ export default class Doc {
259262 return decimalFormatter ( prec ) . format ( v )
260263 }
261264
262- static formatThreeSigFigs ( v : number ) : string {
263- if ( v >= 1000 ) return intFormatter . format ( Math . round ( v ) )
264- return threeSigFigs . format ( v )
265+ /*
266+ * formatRateFullPrecision formats rate to represent it exactly at rate step
267+ * precision, trimming non-effectual zeros if there are any.
268+ */
269+ static formatRateFullPrecision ( encRate : number , bui : UnitInfo , qui : UnitInfo , rateStepEnc : number ) {
270+ const r = bui . conventional . conversionFactor / qui . conventional . conversionFactor
271+ const convRate = encRate * r / RateEncodingFactor
272+ const rateStepDigits = log10RateEncodingFactor - Math . floor ( Math . log10 ( rateStepEnc ) ) -
273+ Math . floor ( Math . log10 ( bui . conventional . conversionFactor ) - Math . log10 ( qui . conventional . conversionFactor ) )
274+ return fullPrecisionFormatter ( rateStepDigits ) . format ( convRate )
265275 }
266276
267- static formatFiveSigFigs ( v : number , prec ?: number ) : string {
268- if ( v >= 10000 ) return intFormatter . format ( Math . round ( v ) )
269- else if ( v < 1e5 ) return fullPrecisionFormatter ( prec ?? 8 /* rate encoding factor */ ) . format ( v )
270- return fiveSigFigs . format ( v )
277+ static formatFourSigFigs ( n : number ) : string {
278+ return formatSigFigsWithFormatters ( oneFractionalDigit , fourSigFigs , n )
271279 }
272280
273281 /*
@@ -292,6 +300,11 @@ export default class Doc {
292300 return fullPrecisionFormatter ( prec ) . format ( value )
293301 }
294302
303+ static conventionalRateStep ( rateStepEnc : number , baseUnitInfo : UnitInfo , quoteUnitInfo : UnitInfo ) {
304+ const [ qFactor , bFactor ] = [ quoteUnitInfo . conventional . conversionFactor , baseUnitInfo . conventional . conversionFactor ]
305+ return rateStepEnc / RateEncodingFactor * ( bFactor / qFactor )
306+ }
307+
295308 /*
296309 * logoPath creates a path to a png logo for the specified ticker symbol. If
297310 * the symbol is not a supported asset, the generic letter logo will be
@@ -626,3 +639,96 @@ function timeMod (t: number, dur: number) {
626639 const n = Math . floor ( t / dur )
627640 return [ n , t - n * dur ]
628641}
642+
643+ function formatSigFigsWithFormatters ( intFormatter : Intl . NumberFormat , sigFigFormatter : Intl . NumberFormat , n : number , maxDecimals ?: number , locales ?: string | string [ ] ) : string {
644+ if ( n >= 1000 ) return intFormatter . format ( n )
645+ const s = sigFigFormatter . format ( n )
646+ if ( typeof maxDecimals !== 'number' ) return s
647+ const fractional = sigFigFormatter . formatToParts ( n ) . filter ( ( part : Intl . NumberFormatPart ) => part . type === 'fraction' ) [ 0 ] . value
648+ if ( fractional . length <= maxDecimals ) return s
649+ return fullPrecisionFormatter ( maxDecimals , locales ) . format ( n )
650+ }
651+
652+ if ( process . env . NODE_ENV === 'development' ) {
653+ // Code will only appear in dev build.
654+ // https://webpack.js.org/guides/production/
655+ window . testFormatFourSigFigs = ( ) => {
656+ const tests : [ string , string , number | undefined , string ] [ ] = [
657+ [ 'en-US' , '1.234567' , undefined , '1.235' ] , // sigFigFormatter
658+ [ 'en-US' , '1.234567' , 2 , '1.23' ] , // decimalFormatter
659+ [ 'en-US' , '1234' , undefined , '1,234.0' ] , // oneFractionalDigit
660+ [ 'en-US' , '12' , undefined , '12.00' ] , // sigFigFormatter
661+ [ 'fr-FR' , '123.45678' , undefined , '123,5' ] , // oneFractionalDigit
662+ [ 'fr-FR' , '1234.5' , undefined , '1 234,5' ] , // U+202F for thousands separator
663+ // For Arabic, https://www.saitak.com/number is useful, but seems to use
664+ // slightly different unicode points and no thousands separator. I think
665+ // the Arabic decimal separator is supposed to be more like a point, not
666+ // a comma, but Google Chrome uses U+066B (Arabic Decimal Separator),
667+ // which looks like a comma to me. ¯\_(ツ)_/¯
668+ [ 'ar-EG' , '123.45678' , undefined , '١٢٣٫٥' ] ,
669+ [ 'ar-EG' , '1234' , undefined , '١٬٢٣٤٫٠' ] ,
670+ [ 'ar-EG' , '0.12345' , 3 , '٠٫١٢٣' ]
671+ ]
672+
673+ // Reproduce the NumberFormats with ONLY our desired language.
674+ for ( const [ code , unformatted , maxDecimals , expected ] of tests ) {
675+ const intFormatter = new Intl . NumberFormat ( code , { // oneFractionalDigit
676+ minimumFractionDigits : 1 ,
677+ maximumFractionDigits : 1
678+ } )
679+ const sigFigFormatter = new Intl . NumberFormat ( code , {
680+ minimumSignificantDigits : 4 ,
681+ maximumSignificantDigits : 4
682+ } )
683+ for ( const k in decimalFormatters ) delete decimalFormatters [ k ] // cleanup
684+ for ( const k in fullPrecisionFormatters ) delete fullPrecisionFormatters [ k ] // cleanup
685+ const s = formatSigFigsWithFormatters ( intFormatter , sigFigFormatter , parseFloat ( unformatted ) , maxDecimals , code )
686+ if ( s !== expected ) console . log ( `TEST FAILED: f('${ code } ', ${ unformatted } , ${ maxDecimals } ) => '${ s } ' != '${ expected } '}` )
687+ else console . log ( `✔️ f('${ code } ', ${ unformatted } , ${ maxDecimals } ) => ${ s } ✔️` )
688+ }
689+ }
690+
691+ window . testFormatRateFullPrecision = ( ) => {
692+ const tests : [ number , number , number , number , string ] [ ] = [
693+ // Two utxo assets with a conventional rate of 0.15. Conventional rate
694+ // step is 100 / 1e8 = 1e-6, so there should be 6 decimal digits.
695+ [ 1.5e7 , 100 , 1e8 , 1e8 , '0.150000' ] ,
696+ // USDC quote -> utxo base with a rate of $10 / 1 XYZ. USDC has an
697+ // conversion factor of 1e6, so $10 encodes to 1e7, 1 XYZ encodes to 1e8,
698+ // encoded rate is 1e7 / 1e8 * 1e8 = 1e7, bFactor / qFactor is 1e2.
699+ // The conventional rate step is 200 / 1e8 * 1e2 = 2e-4, so using
700+ // rateStepDigits, we should get 4 decimal digits.
701+ [ 1e7 , 200 , 1e6 , 1e8 , '10.0000' ] ,
702+ // Set a rate of 1 atom USDC for 0.01 BTC. That atomic rate will be 1 /
703+ // 1e6 = 1e-6. The encoded rate will be 1e-6 * 1e8 = 1e2. As long as our
704+ // rate step divides evenly into 100, this should work. The conventional
705+ // rate is 1e-6 / 1e-2 = 1e-4, so expect 4 decimal digits.
706+ [ 1e2 , 100 , 1e6 , 1e8 , '0.0001' ] ,
707+ // DCR-ETH, expect 6 decimals.
708+ [ 1.5e7 , 1000 , 1e9 , 1e8 , '0.015000' ] ,
709+ [ 1e6 , 1000 , 1e9 , 1e8 , '0.001000' ] ,
710+ [ 1e3 , 1000 , 1e9 , 1e8 , '0.000001' ] ,
711+ [ 100001000 , 1000 , 1e9 , 1e8 , '0.100001' ] ,
712+ [ 1000001000 , 1000 , 1e9 , 1e8 , '1.000001' ] ,
713+ // DCR-USDC, expect 3 decimals.
714+ [ 1.5e7 , 1000 , 1e6 , 1e8 , '15.000' ] ,
715+ [ 1e6 , 1000 , 1e6 , 1e8 , '1.000' ] ,
716+ [ 1e3 , 1000 , 1e6 , 1e8 , '0.001' ] ,
717+ [ 101000 , 1000 , 1e6 , 1e8 , '0.101' ] ,
718+ [ 1001000 , 1000 , 1e6 , 1e8 , '1.001' ] ,
719+ // UTXO assets but with a rate step that's not a perfect power of 10.
720+ // For a rate step of 500, a min rate would be e.g. rate step = 500.
721+ // 5e2 / 1e8 = 5e-6 = 0.000005
722+ [ 5e2 , 500 , 1e8 , 1e8 , '0.000005' ]
723+ ]
724+
725+ for ( const [ encRate , rateStep , qFactor , bFactor , expEncoding ] of tests ) {
726+ for ( const k in fullPrecisionFormatters ) delete fullPrecisionFormatters [ k ] // cleanup
727+ const bui = { conventional : { conversionFactor : bFactor } } as any as UnitInfo
728+ const qui = { conventional : { conversionFactor : qFactor } } as any as UnitInfo
729+ const enc = Doc . formatRateFullPrecision ( encRate , bui , qui , rateStep )
730+ if ( enc !== expEncoding ) console . log ( `TEST FAILED: f(${ encRate } , ${ bFactor } , ${ qFactor } , ${ rateStep } ) => ${ enc } != ${ expEncoding } ` )
731+ else console . log ( `✔️ f(${ encRate } , ${ bFactor } , ${ qFactor } , ${ rateStep } ) => ${ enc } ✔️` )
732+ }
733+ }
734+ }
0 commit comments