FormattedNumber.tsx
1 import { normalizeBN, valueToBigNumber } from '@aave/math-utils'; 2 import { Typography } from '@mui/material'; 3 import { Variant } from '@mui/material/styles/createTypography'; 4 import type { 5 TypographyProps, 6 TypographyPropsVariantOverrides, 7 } from '@mui/material/Typography/Typography'; 8 import type { OverridableStringUnion } from '@mui/types'; 9 import type { ElementType } from 'react'; 10 11 interface CompactNumberProps { 12 value: string | number; 13 visibleDecimals?: number; 14 roundDown?: boolean; 15 compactThreshold?: number; 16 } 17 18 const POSTFIXES = ['', 'K', 'M', 'B', 'T', 'P', 'E', 'Z', 'Y']; 19 20 export const compactNumber = ({ 21 value, 22 visibleDecimals = 2, 23 roundDown, 24 compactThreshold, 25 }: CompactNumberProps) => { 26 const bnValue = valueToBigNumber(value); 27 28 let integerPlaces = bnValue.toFixed(0).length; 29 if (compactThreshold && Number(value) <= compactThreshold) { 30 integerPlaces = 0; 31 } 32 const significantDigitsGroup = Math.min( 33 Math.floor(integerPlaces ? (integerPlaces - 1) / 3 : 0), 34 POSTFIXES.length - 1 35 ); 36 const postfix = POSTFIXES[significantDigitsGroup]; 37 let formattedValue = normalizeBN(bnValue, 3 * significantDigitsGroup).toNumber(); 38 if (roundDown) { 39 // Truncates decimals after the visible decimal point, i.e. 10.237 with 2 decimals becomes 10.23 40 formattedValue = 41 Math.trunc(Number(formattedValue) * 10 ** visibleDecimals) / 10 ** visibleDecimals; 42 } 43 const prefix = new Intl.NumberFormat('en-US', { 44 maximumFractionDigits: visibleDecimals, 45 minimumFractionDigits: visibleDecimals, 46 }).format(formattedValue); 47 48 return { prefix, postfix }; 49 }; 50 51 function CompactNumber({ value, visibleDecimals, roundDown }: CompactNumberProps) { 52 const { prefix, postfix } = compactNumber({ value, visibleDecimals, roundDown }); 53 54 return ( 55 <> 56 {prefix} 57 {postfix} 58 </> 59 ); 60 } 61 62 export type FormattedNumberProps = TypographyProps<ElementType, { component?: ElementType }> & { 63 value: string | number; 64 symbol?: string; 65 visibleDecimals?: number; 66 compact?: boolean; 67 percent?: boolean; 68 symbolsColor?: string; 69 symbolsVariant?: OverridableStringUnion<Variant | 'inherit', TypographyPropsVariantOverrides>; 70 roundDown?: boolean; 71 compactThreshold?: number; 72 }; 73 74 export function FormattedNumber({ 75 value, 76 symbol, 77 visibleDecimals, 78 compact, 79 percent, 80 symbolsVariant, 81 symbolsColor, 82 roundDown, 83 compactThreshold, 84 ...rest 85 }: FormattedNumberProps) { 86 const number = percent ? Number(value) * 100 : Number(value); 87 88 let decimals: number = visibleDecimals ?? 0; 89 if (number === 0) { 90 decimals = 0; 91 } else if (visibleDecimals === undefined) { 92 if (number >= 1 || percent || symbol === 'USD') { 93 decimals = 2; 94 } else { 95 decimals = 7; 96 } 97 } 98 99 const minValue = 10 ** -(decimals as number); 100 const isSmallerThanMin = number !== 0 && Math.abs(number) < Math.abs(minValue); 101 let formattedNumber = isSmallerThanMin ? minValue : number; 102 const forceCompact = compact !== false && (compact || number > 99_999); 103 104 // rounding occurs inside of CompactNumber as the prefix, not base number is rounded 105 if (roundDown && !forceCompact) { 106 formattedNumber = Math.trunc(Number(formattedNumber) * 10 ** decimals) / 10 ** decimals; 107 } 108 109 return ( 110 <Typography 111 {...rest} 112 sx={{ 113 display: 'inline-flex', 114 flexDirection: 'row', 115 alignItems: 'center', 116 position: 'relative', 117 ...rest.sx, 118 }} 119 noWrap 120 > 121 {isSmallerThanMin && ( 122 <Typography 123 component="span" 124 sx={{ mr: 0.5 }} 125 variant={symbolsVariant || rest.variant} 126 color={symbolsColor || 'text.secondary'} 127 > 128 {'<'} 129 </Typography> 130 )} 131 {symbol?.toLowerCase() === 'usd' && !percent && ( 132 <Typography 133 component="span" 134 sx={{ mr: 0.5 }} 135 variant={symbolsVariant || rest.variant} 136 color={symbolsColor || 'text.secondary'} 137 > 138 $ 139 </Typography> 140 )} 141 142 {!forceCompact ? ( 143 new Intl.NumberFormat('en-US', { 144 maximumFractionDigits: decimals, 145 minimumFractionDigits: decimals, 146 }).format(formattedNumber) 147 ) : ( 148 <CompactNumber 149 value={formattedNumber} 150 visibleDecimals={decimals} 151 roundDown={roundDown} 152 compactThreshold={compactThreshold} 153 /> 154 )} 155 156 {percent && ( 157 <Typography 158 component="span" 159 sx={{ ml: 0.5 }} 160 variant={symbolsVariant || rest.variant} 161 color={symbolsColor || 'text.secondary'} 162 > 163 % 164 </Typography> 165 )} 166 {symbol?.toLowerCase() !== 'usd' && typeof symbol !== 'undefined' && ( 167 <Typography 168 component="span" 169 sx={{ ml: 0.5 }} 170 variant={symbolsVariant || rest.variant} 171 color={symbolsColor || 'text.secondary'} 172 > 173 {symbol} 174 </Typography> 175 )} 176 </Typography> 177 ); 178 }