import { Opcode , RuleType } from '@phensley/cldr-ext-rbnf/lib/rbnftypes' ; import { parseRBNF , RBNFNode } from '../parsing/parser.rbnf' ; import { JSONRoot , JSONRulesetInner , ROOT_KEYS } from './json' ; import { FrequencySet } from '../utils' ; const IGNORED = new Set ( [ 'lenient-parse' ] ) ; const PLURALS = [ 'zero' , 'one' , 'two' , 'few' , 'many' , 'other' ] ; type Pair = [ string , JSONRulesetInner ] ; /**
 * Collect all rulesets.
 */ const collect = ( root ) : Pair [ ] => { const res = [ ] ; for ( const key of ROOT_KEYS ) { const rulesets = root [ key ] ; if ( ! rulesets ) { continue ; } for ( const name of Object . keys ( rulesets ) ) { res . push ( [ name , rulesets [ name ] ] ) ; } } return res ; } ; /**
 * Parse an RBNF rule and throw an error if the parse failed.
 */ const parse = ( raw ) : RBNFNode [ ] => { const rule = parseRBNF ( raw ) ; if ( rule . isNothing ( ) ) { throw new Error ( `Failed to parse RBNF rule: ${ raw }` ) ; } return rule . get ( ) . _1 ; } ; /**
 * Recurse through the syntax tree and extract and intern all symbols.
 */ export class RBNFSymbolExtractor { constructor ( private symbols , private numbers ) { } /**
     * Populate the frequency-sorted symbol, base value / radix tables.
     */ extract ( root ) { const rulesets = collect ( root ) ; for ( const ruleset of rulesets ) { const rules = ruleset [ 1 ] . rules ; for ( const raw of rules ) { const { value } = raw ; if ( /^\d+(\/\d+)?$/ . test ( value ) ) { for ( const part of value . split ( '/' ) ) { this . numbers . add ( part ) ; } } const nodes = parse ( raw . rule ) ; for ( const node of nodes ) { this . extractNode ( node ) ; } } } } /**
     * Recurse into the given node, interning all symbols found.
     */ extractNode ( node ) { switch ( node . kind ) { case 'apply-unch-numfmt' : case 'apply-left-numfmt' : case 'apply-left-2-numfmt' : case 'apply-right-numfmt' : case 'literal' : this . symbols . add ( node . n ) ; break ; case 'cardinal' : case 'ordinal' : for ( const child of node . n ) { this . symbols . add ( child . n ) ; } break ; case 'optional' : for ( const child of node . n ) { this . extractNode ( child ) ; } break ; } } } /**
 * Parse RBNF rules and encode them compactly for distribution.
 */ export class RBNFEncoder { private index : Map < string , number > = new Map < string , number > ( ) ; constructor ( private symbols , private numbers ) { } encode ( root ) { let rulesets = collect ( root ) . filter ( ( r ) => ! IGNORED . has ( r [ 0 ] ) ) ; const cmp = ( a , b ) => ( a [ 0 ] < b [ 0 ] ? - 1 : a [ 0 ] > b [ 0 ] ? 1 : 0 ) ; const pub = rulesets . filter ( ( r ) => r [ 1 ] . private === 0 ) . sort ( cmp ) ; const prv = rulesets . filter ( ( r ) => r [ 1 ] . private === 1 ) . sort ( cmp ) ; rulesets = pub . concat ( prv ) ; rulesets . forEach ( ( r , i ) => { const name = r [ 0 ] ; this . index . set ( name , i ) ; } ) ; const result = this . _encode ( rulesets ) ; const names = ( pairs ) => pairs . map ( ( p ) => p [ 0 ] ) . join ( '_' ) ; return { names : [ names ( pub ) , names ( prv ) ] , rulesets : result , } ; } /**
     * PASS 2: Encode the final RBNF rules and instructions.
     */ private _encode ( rulesets ) { const symbols = this . symbols . sort ( ) ; const numbers = this . numbers . sort ( ) ; const result = [ ] ; for ( const ruleset of rulesets ) { const rules = [ ] ; let period = false ; let comma = false ; for ( const raw of ruleset [ 1 ] . rules ) { switch ( raw . value ) { case '0.x' : case 'x.x' : period = true ; break ; case '0,x' : case 'x,x' : comma = true ; break ; } } for ( const raw of ruleset [ 1 ] . rules ) { const inst = this . encodeRule ( raw . rule , symbols ) ; const { value } = raw ; if ( /^\d+$/ . test ( value ) ) { const i = numbers . indexOf ( value ) ; rules . push ( [ RuleType . NORMAL , inst , i ] ) ; } else if ( /^\d+(\/\d+)?$/ . test ( value ) ) { const p = value . split ( '/' ) ; const i = numbers . indexOf ( p [ 0 ] ) ; const j = numbers . indexOf ( p [ 1 ] ) ; rules . push ( [ RuleType . NORMAL_RADIX , inst , i , j ] ) ; } else { switch ( value ) { case '-x' : rules . push ( [ RuleType . MINUS , inst ] ) ; break ; case '0.x' : rules . push ( [ RuleType . PROPER_FRACTION , inst , 0 ] ) ; if ( ! comma ) { rules . push ( [ RuleType . PROPER_FRACTION , inst , 1 ] ) ; } break ; case '0,x' : rules . push ( [ RuleType . PROPER_FRACTION , inst , 1 ] ) ; if ( ! period ) { rules . push ( [ RuleType . PROPER_FRACTION , inst , 0 ] ) ; } break ; case 'x.x' : rules . push ( [ RuleType . IMPROPER_FRACTION , inst , 0 ] ) ; if ( ! comma ) { rules . push ( [ RuleType . IMPROPER_FRACTION , inst , 1 ] ) ; } break ; case 'x,x' : rules . push ( [ RuleType . IMPROPER_FRACTION , inst , 1 ] ) ; if ( ! period ) { rules . push ( [ RuleType . IMPROPER_FRACTION , inst , 0 ] ) ; } break ; case 'Inf' : rules . push ( [ RuleType . INFINITY , inst ] ) ; break ; case 'NaN' : rules . push ( [ RuleType . NOT_A_NUMBER , inst ] ) ; break ; } } } result . push ( rules ) ; } return result ; } /**
     * Parse and decode an RBNF rule.
     */ private encodeRule ( rule , symbols ) { const nodes = parse ( rule ) ; return nodes . map ( ( n ) => this . encodeNode ( n , symbols ) ) ; } private encodeNode ( node , symbols ) { switch ( node . kind ) { case 'literal' : { const i = symbols . indexOf ( node . n ) ; return [ Opcode . LITERAL , i ] ; } case 'apply-left-rule' : { const i = this . ruleindex ( node . n ) ; return [ Opcode . APPLY_LEFT_RULE , i ] ; } case 'apply-left-numfmt' : { const i = this . index . get ( node . n ) ; return [ Opcode . APPLY_LEFT_NUM_FORMAT , i ] ; } case 'apply-left-2-rule' : { const i = this . ruleindex ( node . n ) ; return [ Opcode . APPLY_LEFT_2_RULE , i ] ; } case 'apply-left-2-numfmt' : { const i = symbols . indexOf ( node . n ) ; return [ Opcode . APPLY_LEFT_2_NUM_FORMAT , i ] ; } case 'apply-right-rule' : { const i = this . ruleindex ( node . n ) ; return [ Opcode . APPLY_RIGHT_RULE , i ] ; } case 'apply-right-numfmt' : { const i = symbols . indexOf ( node . n ) ; return [ Opcode . APPLY_RIGHT_NUM_FORMAT , i ] ; } case 'apply-unch-rule' : { const i = this . ruleindex ( node . n ) ; return [ Opcode . UNCHANGED_RULE , i ] ; } case 'apply-unch-numfmt' : { const i = symbols . indexOf ( node . n ) ; return [ Opcode . UNCHANGED_NUM_FORMAT , i ] ; } case 'cardinal' : case 'ordinal' : { const block = [ ] ; for ( const n of node . n ) { const cat = PLURALS . indexOf ( n . category ) ; const i = symbols . indexOf ( n . n ) ; block . push ( [ cat , i ] ) ; } return node . kind === 'cardinal' ? [ Opcode . CARDINAL , block ] : [ Opcode . ORDINAL , block ] ; } case 'optional' : { const block = node . n . map ( ( n ) => this . encodeNode ( n , symbols ) ) ; return [ Opcode . OPTIONAL , block ] ; } case 'sub-left' : return [ Opcode . SUB_LEFT ] ; case 'sub-right' : return [ Opcode . SUB_RIGHT ] ; case 'sub-right-3' : return [ Opcode . SUB_RIGHT_3 ] ; } throw new Error ( `No encoder for node: ${ JSON . stringify ( node ) }` ) ; } private ruleindex ( name ) { const key = name . replace ( /^%+/ , '' ) ; const i = this . index . get ( key ) ; if ( i === undefined ) { throw new Error ( `Name references a rule outside this ruleset: ${ name }` ) ; } return i ; } }