import { Decimal , DecimalConstants } from '@phensley/decimal' ; import { PluralRules } from '@phensley/plurals' ; import { ApplyLeft2NumFormatInst , ApplyLeft2RuleInst , ApplyLeftNumFormatInst , ApplyLeftRuleInst , NormalRadixRule , NormalRule , Opcode , PLURALS , RuleType , RBNFInst , RBNFRule , SubLeftInst , } from './rbnftypes' ; import { binarySearch } from './utils' ; const DIVISORS = [ 0 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 ] . map ( ( e ) => new Decimal ( `1e${ e }` ) ) ; const { ONE , ZERO } = DecimalConstants ; const MINUS_ONE = ONE . negate ( ) ; const TEN = new Decimal ( 10 ) ; type IntegerSubInst = ApplyLeftRuleInst | ApplyLeft2RuleInst | ApplyLeft2NumFormatInst | ApplyLeftNumFormatInst | SubLeftInst ; export interface RBNFSymbols { decimal : string ; nan : string ; infinity : string ; } export type RBNFDecimalFormatter = ( pattern : string , n : Decimal ) => string ; /**
 * A collection of multiple RBNF locale-specific rulesets with shared arrays of
 * symbols and numbers.
 */ export class RBNF { protected symbols : string [ ] ; protected numbers : Decimal [ ] ; protected locales : Map < string , RBNFSet > = new Map < string , RBNFSet > ( ) ; constructor ( protected plurals , spellout ) { const { locales , symbols , numbers } = spellout ; this . symbols = symbols ? symbols . split ( '_' ) : [ ] ; this . numbers = numbers ? numbers . split ( '_' ) . map ( ( n ) => new Decimal ( n ) ) : [ ] ; for ( const id of Object . keys ( locales ) ) { const { names , rulesets } = locales [ id ] ; const [ _pub , _prv ] = names ; const pub = _pub . split ( '_' ) ; const prv = _prv . length ? _prv . split ( '_' ) : [ ] ; this . locales . set ( id , this . make ( id , plurals , pub , prv , this . numbers , this . symbols , rulesets ) ) ; } } /**
     * Builds an RBNFSet instance, allowing overriding of construction in subclasses
     * (currently used for debugging).
     */ make ( id , plurals , pub , prv , numbers , symbols , rulesets ) { return new RBNFSet ( id , plurals , pub , prv , numbers , symbols , rulesets ) ; } /**
     * Fetch the rulesets for the given locale id.
     */ get ( id ) { return this . locales . get ( id ) ; } } /**
 * A closed set of RBNF rules. By "closed" we mean that any instruction
 * that jumps to another named rule always lands in the same ruleset.
 */ export class RBNFSet { readonly language : string ; readonly index : Map < string , number > = new Map < string , number > ( ) ; readonly allnames : string [ ] ; constructor ( readonly id , readonly plurals , readonly pubnames , readonly prvnames , readonly numbers , readonly symbols , readonly rulesets ) { this . language = id === 'root' ? id : id . split ( '-' ) [ 0 ] ; pubnames . forEach ( ( n , i ) => this . index . set ( n , i ) ) ; this . allnames = pubnames . concat ( prvnames ) ; } /**
     * Return the RBNF formatted representation of a number for the given
     * language, using the given rules.
     */ format ( rulename , symbols , n , fallback ) { return new RBNFEngine ( this . language , this . plurals , symbols , this , fallback ) . format ( rulename , n ) ; } } /**
 * Evaluate rules to format numbers using RBNF.
 *
 * Some rulesets will be included with the core library (those referenced in the
 * root locale) while others will be added to the corresponding language's resource
 * pack and only available when that language has been loaded.
 */ export class RBNFEngine { private buf : string = '' ; private errors : string [ ] = [ ] ; private fractions : Set < number > = new Set < number > ( ) ; private decimal : number ; constructor ( private language , private plurals , private symbols , protected rbnf , protected fallback ) { this . decimal = symbols . decimal === '.' ? 1 : 0 ; } format ( name , n ) { const si = this . rbnf . index . get ( name ) ; if ( si !== undefined ) { this . _format ( n , si ) ; } if ( this . errors . length ) { console . log ( `ERRORS: '${ this . language }' rule '${ name }' number '${ n . toString ( ) }'  ${ this . errors . join ( '\n' ) }` ) ; } return this . buf ; } protected _format ( n , si ) { const ri = this . _match ( n , si ) ; if ( ri === - 1 ) { if ( n . isNaN ( ) ) { this . buf += this . symbols . nan ; } else if ( n . isInfinity ( ) ) { this . buf += this . symbols . infinity ; } return ; } const r = this . rbnf . rulesets [ si ] [ ri ] ; this . _evalinst ( n , r [ 1 ] , r , ri , si ) ; } protected _match ( n , si ) { const rules = this . rbnf . rulesets [ si ] ; const neg = n . isNegative ( ) ; const int = n . isInteger ( ) ; const fin = n . isFinite ( ) ; for ( let i = 0 ; i < rules . length ; i ++ ) { const r = rules [ i ] ; switch ( r [ 0 ] ) { case RuleType . NOT_A_NUMBER : if ( n . isNaN ( ) ) { return i ; } break ; case RuleType . INFINITY : if ( n . isInfinity ( ) ) { return i ; } break ; case RuleType . MINUS : if ( neg ) { return i ; } break ; case RuleType . PROPER_FRACTION : if ( ! int && fin && r [ 2 ] === this . decimal && ( ( neg && n . compare ( MINUS_ONE ) === 1 ) || ( ! neg && n . compare ( ONE ) === - 1 ) ) ) { return i ; } break ; case RuleType . IMPROPER_FRACTION : if ( ! int && fin && r [ 2 ] === this . decimal ) { return i ; } break ; case RuleType . NORMAL : case RuleType . NORMAL_RADIX : if ( fin ) { const m = n . abs ( ) ; const j = binarySearch ( rules as ( NormalRule | NormalRadixRule ) [ ] , true , i , ( e ) : number => this . rbnf . numbers [ e [ 2 ] ] . compare ( m ) ) ; if ( j < i ) { const name = this . rbnf . allnames [ si ] ; this . errors . push ( `Malformed rules in ruleset '${ name }' at ${ i } for ${ n . toString ( ) }: got ${ j }` ) ; return - 1 ; } return j ; } break ; } } return - 1 ; } protected _evalinst ( n , i , r , ri , si ) { for ( let j = 0 ; j < i . length ; j ++ ) { const inst = i [ j ] ; switch ( inst [ 0 ] ) { case Opcode . LITERAL : this . symbol ( inst [ 1 ] ) ; break ; case Opcode . OPTIONAL : if ( ZERO . compare ( n . divmod ( this . getradix ( r ) ) [ 1 ] ) !== 0 ) { this . _evalinst ( n , inst [ 1 ] , r , ri , si ) ; } break ; case Opcode . CARDINAL : case Opcode . ORDINAL : const q = n . divmod ( this . getradix ( r ) ) [ 0 ] ; const cat = this . plural ( q . stripTrailingZeros ( ) , inst [ 0 ] === Opcode . CARDINAL ) ; let subs = inst [ 1 ] . filter ( ( s ) => s [ 0 ] === cat ) ; if ( ! subs . length ) { subs = inst [ 1 ] . filter ( ( s ) => s [ 0 ] === PLURALS [ 'other' ] ) ; } if ( subs . length ) { this . symbol ( subs [ 0 ] [ 1 ] ) ; } break ; case Opcode . SUB_LEFT : case Opcode . APPLY_LEFT_RULE : case Opcode . APPLY_LEFT_2_RULE : case Opcode . APPLY_LEFT_NUM_FORMAT : case Opcode . APPLY_LEFT_2_NUM_FORMAT : if ( r [ 0 ] === RuleType . PROPER_FRACTION || r [ 0 ] === RuleType . IMPROPER_FRACTION ) { this . integersub ( n , inst , ri , si ) ; } else if ( this . fractions . has ( si ) ) { const base = r [ 0 ] === RuleType . NORMAL || r [ 0 ] === RuleType . NORMAL_RADIX ? r [ 2 ] : - 1 ; if ( base !== - 1 ) { this . numeratorsub ( n , inst , base , ri , si ) ; } } else { this . multipliersub ( n , inst , this . getradix ( r ) , ri , si ) ; } break ; case Opcode . UNCHANGED_RULE : this . _format ( n , inst [ 1 ] ) ; break ; case Opcode . UNCHANGED_NUM_FORMAT : this . buf += this . fallback ( this . rbnf . symbols [ inst [ 1 ] ] , n ) ; break ; case Opcode . SUB_RIGHT : case Opcode . SUB_RIGHT_3 : case Opcode . APPLY_RIGHT_RULE : if ( r [ 0 ] === RuleType . MINUS ) { this . _format ( n . abs ( ) , inst [ 0 ] === Opcode . APPLY_RIGHT_RULE ? inst [ 1 ] : si ) ; } else if ( r [ 0 ] === RuleType . PROPER_FRACTION || r [ 0 ] === RuleType . IMPROPER_FRACTION ) { const m = n . abs ( ) ; this . fractionsub ( m . subtract ( m . toInteger ( ) ) , inst , ri , si ) ; } else { this . modulussub ( n , inst , this . getradix ( r ) , ri , si ) ; } break ; default : console . log ( '   MISSING:' , inst [ 0 ] ) ; break ; } } } protected symbol ( i ) { this . buf += this . rbnf . symbols [ i ] ; } protected fractionsub ( n , inst , ri , si ) { const _si = inst [ 0 ] === Opcode . APPLY_RIGHT_RULE ? inst [ 1 ] : si ; const digits = inst [ 0 ] === Opcode . SUB_RIGHT || inst [ 0 ] === Opcode . SUB_RIGHT_3 || _si === si ; const spaces = inst [ 0 ] !== Opcode . SUB_RIGHT_3 ; if ( digits ) { this . bydigit ( n , ri , si , spaces ) ; } else { let q = n ; if ( q . scale ( ) > 9 ) { q = q . setScale ( 9 ) ; } const t = q . shiftleft ( q . scale ( ) ) . toInteger ( ) ; const m = binarySearch ( DIVISORS , false , 0 , ( e ) => { const r = e . compare ( t ) ; return r ; } ) ; const d = DIVISORS [ m ] ; const k = this . _match ( d , _si ) ; if ( k !== - 1 ) { this . fractions . add ( _si ) ; const r = this . rbnf . rulesets [ _si ] [ k ] ; this . _evalinst ( t , r [ 1 ] , r , k , _si ) ; } } } protected integersub ( n , inst , _ri , si ) { n = n . toInteger ( ) ; switch ( inst [ 0 ] ) { case Opcode . APPLY_LEFT_RULE : case Opcode . APPLY_LEFT_2_RULE : case Opcode . SUB_LEFT : this . _format ( n , inst [ 0 ] === Opcode . SUB_LEFT ? si : inst [ 1 ] ) ; break ; case Opcode . APPLY_LEFT_NUM_FORMAT : case Opcode . APPLY_LEFT_2_NUM_FORMAT : this . buf += '#' ; break ; } } protected numeratorsub ( n , inst , _base , _ri , _si ) { switch ( inst [ 0 ] ) { case Opcode . APPLY_LEFT_2_RULE : this . _format ( n , inst [ 1 ] ) ; break ; } } protected multipliersub ( n , inst , radix , _ri , si ) { const [ q ] = n . divmod ( radix ) ; switch ( inst [ 0 ] ) { case Opcode . APPLY_LEFT_RULE : case Opcode . SUB_LEFT : this . _format ( q , inst [ 0 ] === Opcode . SUB_LEFT ? si : inst [ 1 ] ) ; break ; default : console . log ( 'MISSING:' , inst [ 0 ] ) ; break ; } } protected modulussub ( n , inst , radix , _ri , si ) { const r = n . divmod ( radix ) [ 1 ] ; switch ( inst [ 0 ] ) { case Opcode . APPLY_RIGHT_RULE : case Opcode . SUB_RIGHT : this . _format ( r , inst [ 0 ] === Opcode . SUB_RIGHT ? si : inst [ 1 ] ) ; break ; } } protected plural ( n , cardinal ) { const cat = cardinal ? this . plurals . cardinal ( n ) : this . plurals . ordinal ( n ) ; return PLURALS [ cat ] ; } protected bydigit ( n , _ri , si , spaces = true ) { let i = 0 ; while ( ZERO . compare ( n ) !== 0 ) { if ( spaces && i ) { this . buf += ' ' ; } const q = n . shiftleft ( 1 ) ; const r = q . toInteger ( ) ; this . _format ( r , si ) ; n = q . subtract ( r ) ; i ++ ; } } protected getradix ( r ) { const n = this . rbnf . numbers ; return r [ 0 ] === RuleType . NORMAL_RADIX ? n [ r [ 3 ] ] : r [ 0 ] === RuleType . NORMAL ? DIVISORS [ n [ r [ 2 ] ] . integerDigits ( ) ] : TEN ; } }