import { LanguageResolver } from '@phensley/cldr-core' ; import { pad , Code , HEADER , NOLINT_MAXLINE } from './util' ; import { buildPartitions , MapSet } from './partition' ; const ANY = '\uFFFD' ; const WILDCARD = '*' ; const UNDERSCORE = /_/ ; /**
 * Default distance threshold.
 */ export const DEFAULT_THRESHOLD = 50 ; /**
 * Maximum possible distance between two language tags.
 */ export const MAX_DISTANCE = 100 ; class Node { constructor ( readonly want , readonly have , readonly distance , readonly wildcard , readonly map ) { } } /**
 * Convert public wildcard for internal ANY string.
 */ const key = ( s ) => ( s === WILDCARD ? ANY : s ) ; /**
 * Compare two distance map nodes for sorting.
 */ const nodeCmp = ( a , b ) => { if ( a . want === b . want ) { return a . have === b . have ? 0 : a . have < b . have ? - 1 : 1 ; } return a . want === b . want ? 0 : a . want < b . want ? - 1 : 1 ; } ; /**
 * Store a tree of distances between desired and supported tags.
 */ class DistanceMap { readonly map : { [ x ] : any ; } = { } ; /**
     * Indicate if this map level is empty.
     */ empty ( ) { return Object . keys ( this . map ) . length === 0 ; } /**
     * Get the node corresponding to the pair (want, have).
     */ get ( want , have ) { const sub = this . map [ want ] ; return sub === undefined ? undefined : sub [ have ] ; } /**
     * Query wildcard for this level.
     */ any ( ) { const sub = this . map [ ANY ] ; if ( sub !== undefined ) { const node = sub [ ANY ] ; if ( node !== undefined ) { return node ; } } throw new Error ( 'Serious error: wildcard levels missing in distance map.' ) ; } /**
     * Create a node for the pair (want, have) with the given distance.
     */ put ( want , have , distance ) { let node = this . get ( want , have ) ; if ( node !== undefined ) { return node ; } let sub = this . map [ want ] ; if ( sub === undefined ) { sub = { } ; this . map [ want ] = sub ; } node = new Node ( want , have , distance , want === ANY || have === ANY , new DistanceMap ( ) ) ; sub [ have ] = node ; return node ; } toString ( ) { return '# Generated by @phensley/cldr-compiler\n\n' + this . render ( 0 ) ; } /**
     * Return all nodes for the (want, have) pairs in this map.
     */ nodes ( ) { const res = [ ] ; Object . keys ( this . map ) . forEach ( ( k1 ) => { const inner = this . map [ k1 ] ; Object . keys ( inner ) . forEach ( ( k2 ) => { const node = inner [ k2 ] ; res . push ( node ) ; } ) ; } ) ; return res ; } /**
     * Convert private ANY back to public wildcard.
     */ wildcard ( k ) { return k === ANY ? WILDCARD : k ; } private render ( level ) { const nodes = this . nodes ( ) . sort ( nodeCmp ) ; let prior = undefined ; let rows = 0 ; let res = '' ; nodes . forEach ( ( node ) => { if ( rows !== 0 ) { for ( let i = 0 ; i < level ; i ++ ) { res += '               ' ; } } rows ++ ; const want = this . wildcard ( node . want ) ; const have = this . wildcard ( node . have ) ; if ( prior === undefined || prior !== want || want === ANY ) { prior = want ; res += `${ pad ( 5 , want ) }${ want } ` ; } else { res += '      ' ; } res += `${ pad ( 5 , have ) }${ have } ` ; const dist = node . distance . toString ( ) ; res += `${ pad ( 3 , dist ) }${ dist }` ; if ( level === 2 ) { res += '\n' ; } if ( ! node . map . empty ( ) ) { res += node . map . render ( level + 1 ) ; } } ) ; return res ; } } const distanceMap = new DistanceMap ( ) ; /**
 * Insert the given rule into the distance map.
 */ const addRule = ( variables , desired , supported , distance ) => { switch ( desired . length ) { case 1 : distanceMap . put ( key ( desired [ 0 ] ) , key ( supported [ 0 ] ) , distance ) ; break ; case 2 : { const node = distanceMap . put ( key ( desired [ 0 ] ) , key ( supported [ 0 ] ) , 0 ) ; node . map . put ( key ( desired [ 1 ] ) , key ( supported [ 1 ] ) , distance ) ; break ; } case 3 : { let node = distanceMap . put ( key ( desired [ 0 ] ) , key ( supported [ 0 ] ) , 0 ) ; node = node . map . put ( key ( desired [ 1 ] ) , key ( supported [ 1 ] ) , desired [ 1 ] === WILDCARD ? DEFAULT_THRESHOLD : 0 ) ; if ( desired [ 2 ] === WILDCARD ) { node . map . put ( key ( desired [ 2 ] ) , key ( supported [ 2 ] ) , distance ) ; } else { const want = key ( desired [ 2 ] ) ; const have = key ( supported [ 2 ] ) ; const wantPartitions = want . startsWith ( '$' ) ? variables [ want ] : new Set ( [ want ] ) ; const havePartitions = have . startsWith ( '$' ) ? variables [ have ] : new Set ( [ have ] ) ; wantPartitions . forEach ( ( w ) => { havePartitions . forEach ( ( h ) => { node . map . put ( w , h , distance ) ; } ) ; } ) ; } break ; } } } ; type RawMatchRule = [ string , string , number , number , number ] ; class MatchRule { readonly desired : string [ ] ; readonly supported : string [ ] ; readonly wildcards : number ; readonly distance : number ; readonly oneway : boolean ; constructor ( m ) { this . desired = m [ 0 ] . split ( UNDERSCORE ) ; this . supported = m [ 1 ] . split ( UNDERSCORE ) ; this . wildcards = m [ 2 ] ; this . distance = m [ 3 ] ; this . oneway = m [ 4 ] === 1 ; } } /**
 * Sort a number.
 */ const numberCmp = ( a , b ) => ( a === b ? 0 : a < b ? - 1 : 1 ) ; /**
 * Sort a match based on the number of components and, secondarily,
 * the number of wildcards.
 */ const matchCmp = ( a , b ) => { const r = numberCmp ( a . desired . length , b . desired . length ) ; return r !== 0 ? r : numberCmp ( a . wildcards , b . wildcards ) ; } ; /**
 * Populate the distance map.
 */ const buildDistanceMap = ( variables , matchRules ) => { const wildcards = [ ] ; matchRules . map ( ( m ) => new MatchRule ( m ) ) . sort ( matchCmp ) . forEach ( ( match ) => { if ( match . desired . length === match . wildcards ) { wildcards . push ( match ) ; return ; } const { desired , supported , distance , oneway } = match ; addRule ( variables , desired , supported , distance ) ; if ( ! oneway ) { addRule ( variables , supported , desired , distance ) ; } } ) ; wildcards . forEach ( ( match ) => { switch ( match . wildcards ) { case 1 : distanceMap . put ( ANY , ANY , match . distance ) ; break ; case 2 : distanceMap . nodes ( ) . forEach ( ( node ) => { node . map . put ( ANY , ANY , match . distance ) ; } ) ; break ; case 3 : distanceMap . nodes ( ) . forEach ( ( outer ) => { outer . map . nodes ( ) . forEach ( ( inner ) => { inner . map . put ( ANY , ANY , match . distance ) ; } ) ; } ) ; break ; } } ) ; } ; const convert = ( dm ) : string => { const outer = { } ; Object . keys ( dm . map ) . forEach ( ( have ) => { const h = dm . map [ have ] ; const inner = { } ; Object . keys ( h ) . forEach ( ( want ) => { const node = h [ want ] ; if ( node . map . nodes ( ) . length !== 0 ) { inner [ want ] = [ node . distance , convert ( node . map ) ] ; } else { inner [ want ] = node . distance ; } } ) ; outer [ have ] = inner ; } ) ; return outer ; } ; const paradigmLocales = ( data ) => '{' + ( data as string [ ] ) . reduce ( ( p , c , i ) => { const k = LanguageResolver . resolve ( c ) . compact ( ) ; p . push ( `'${ k }':${ i }` ) ; return p ; } , [ ] as string [ ] ) . join ( ',' ) + '}' ; const convertDistanceMap = ( ) : string => { const encoded = convert ( distanceMap ) ; const raw = JSON . stringify ( encoded ) ; return raw . replace ( /"/g , '' ) . replace ( /\ufffd/gu , '$' ) ; } ; export const getDistance = ( data ) : Code [ ] => { const { variables } = buildPartitions ( data ) ; buildDistanceMap ( variables , data . matchRules ) ; let code = HEADER ; const values = paradigmLocales ( data . paradigmLocales ) ; code += NOLINT_MAXLINE ; code += `export const paradigmLocales: any = ${ values };\n\n` ; code += 'export type DistanceMap = { [x: string]: { [y: string]: DistanceNode | undefined } | undefined };\n' ; code += 'export type DistanceNode = [number, DistanceMap] | number;\n\n' ; const map = convertDistanceMap ( ) ; code += NOLINT_MAXLINE ; code += `export const distanceMap: DistanceMap = ${ map };\n` ; const table = distanceMap . toString ( ) ; return [ Code . localematcher ( [ 'autogen.distance.ts' ] , code ) , Code . top ( [ 'notes' , 'language-distance-table.txt' ] , table ) ] ; } ;