ManuUseGitHub/modulopt

View on GitHub
src/MaskBuilder.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { IColumnOption , IHoldModulopt , IOptions } from "./interfaces";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const defaultCall = ( key: string , previousKey: string , value: any ): number => {
    return 0;
};

/**
 * Masks can be written following this format 11.1111.11 (with dots to avoid mistakes)
 * so it is necessary to strip all dots out of the masks so we can process them as real
 * masks
 * @param s
 * @returns
 */
const stripDots = ( s: string ): string => s.replace( /[.]/g , "" );

const substituteTwos = ( s: string ): string => s.replace( /2/g , "1" );

export class MaskBuilder {
    private static instance: MaskBuilder;

    private constructor() {}

    public static getInstance(): MaskBuilder {
        if ( !MaskBuilder.instance ) {
            MaskBuilder.instance = new MaskBuilder();
        }

        return MaskBuilder.instance;
    }

    /**
     * Gives a bit representation for every multiChoices option
     * @param object
     * @param row entry [optionName [,default][,multiChoices] ]
     * @param cpt counter
     * @param totalOffset totalizer of the offset
     */
    public assignValuePerBit(
        modulopt: IHoldModulopt ,
        row: any ,
        cpt: number ,
        totalOffset: number
    ) {
        if ( row[ 2 ] ) {

            // compute the current element offset by looking at the length of its 3rd collumn
            const offset = row[ 2 ].length;

            // the option name is in the first collumn
            const option = row[ 0 ];

            // initialisation on the object that will host the options
            modulopt.masks[ option ] = {} as IOptions;

            // for every option that is coded on multiple bit, we compute the range of bits that
            // it takes by using the incrementation
            for ( let i = 0; i < offset; ++i ) {

                // get the pow2 of the cpt value ==> to transform into binary representation
                const bit = Math.pow( 2 , cpt + i );

                // get the representation with dots every 4th digit
                const representation = this.formatedNumberRepresentation(
                    bit ,
                    totalOffset
                );

                // undefined and function references cannot be added so we just tell what we were supposed to have
                if ( typeof row[ 2 ][ i ] === "function" || row[ 2 ][ i ] === void 0 ) {
                    row[ 2 ][ i ] = ( typeof row[ 2 ][ i ] ).toUpperCase();
                }

                // assign the matching bit to the option coded on multiple bits
                modulopt.masks[ option ][ representation ] = row[ 2 ][ i ];
            }
        }
    }

    /**
     * Every option will occupied a certain amount of space as a bit reprensentation so to be sure nothing
     * overlaps, and mostly, have consistent zero-filled (padded) masks, we have to compute the total offset
     * @param optionVector
     * @returns
     */
    public computeOffset( optionVector: any[] ) {
        let offset = 0;
        optionVector
            .filter(
                ( row ) => ( typeof row[ 1 ] === "boolean" && row.length === 2 ) || row[ 2 ]
            )
            .map( ( row ) => {

                // sum an offset stored in a cell of an array (may not exist so fallback of 1)
                offset += row[ 2 ] ? row[ 2 ].length : 1;
            } );

        // ceiled integer division multiplied by 4 so we have multiples of four. Perfect for bin reprsentation
        return Math.ceil( offset / 4 ) * 4;
    }

    /**
     * Transform a number into its sero filled dotted binary format. 1 => 0000.0000.0001
     *
     * @param optDef defines the default value for an option and its string masks
     * @param offset the offset teken by possible values of the mask in binary : 1 take , 0 leave
     * @param totalOffset indicate the whole space taken by all option mask + 0 fill included
     * @param cpt the general counter
     */
    public assignMasks( optDef: IColumnOption , totalOffset: number , cpt: number ) {
        const representation = this.formatedNumberRepresentation(
            Math.pow( 2 , cpt ) ,
            totalOffset
        );

        // (join every slices by a dot then push to the masks field)
        // [0000,0000,1000] => ["0000.0000.1000","0000.0001.0000"]
        if ( representation !== "-1" ) {
            optDef.mask = representation;
        }
    }

    /**
     * Transforms a number value to its binary representation with 1 dot every 4th bit
     * @param value
     * @param totalOffset
     * @returns
     */
    public formatedNumberRepresentation( value: number , totalOffset: number ) {

        // transform the position on the iteration into a binary representation as it is a power of 2 bin
        // 2^(cpt+i) => 1, 2, 4, 8 =>  0 , 1 , 10 , 100 , 1000
        const binRep = this.dec2bin( value );

        // = for every 4 bit represented, add a dot so it is easier to manupulate as a human =
        // (division into array of strings) => 000000001000 => [0000,0000,1000]
        const sliceOfFour: RegExpMatchArray | null = this.pad(
            binRep ,
            totalOffset
        ).match( /.{1,4}/g );

        if ( sliceOfFour ) {
            return sliceOfFour.join( "." );
        }

        return "-1";
    }

    public defineInterval( row: any , cpt: number ): number[] {
        return typeof row[ 1 ] === "boolean" && row.length === 2
            ? [ cpt ]
            : row[ 2 ]
            ? [ cpt , -1 + cpt + row[ 2 ].length ]
            : [ cpt ];
    }

    public getOptionsFromMask( modulopt: IHoldModulopt , optionMask: string ): any {
        const options: any = {};
        const masks = this.masksMappedByName( modulopt.masks );

        Object.keys( masks ).map( ( k ) => {
            options[ k ] = this.chosenFromMask( modulopt , optionMask , k );
        } );

        return options;
    }

    /**
     * Transforms decimal number into binary representation
     * @param dec
     * @returns
     */
    private dec2bin( dec: number ) {
        return ( dec >>> 0 ).toString( 2 );
    }

    /**
     * Pads zero (zero fill a number). It provides a string since 0 before any number is not significant
     * @param num the number that has to gain the padding
     * @param size the offset of the resulting string
     * @returns
     */
    private pad( num: string , size: number ): string {
        num = num.toString();
        while ( num.length < size ) num = "0" + num;
        return num;
    }

    private masksMappedByName( masks: IOptions , cb = defaultCall , previousKey = "" ) {
        const mapped: IOptions = {};

        for ( const [ key , value ] of Object.entries( masks ) ) {
            if ( typeof value === "string" || previousKey ) {
                mapped[ value ] = key;
                const result: number = cb( key , previousKey , value );
                if ( result != 0 ) {
                    return result;
                }
            } else if ( typeof value === "object" ) {
                mapped[ key ] = this.masksMappedByName( value , cb , key );
            }
        }
        return mapped;
    }

    private applyMasks( masks: IOptions , a: string , maskField: string ) {
        let result = 0;
        const onAssociation = (
            key: string ,
            previousKey: string ,
            value: any
        ): number => {
            if ( maskField === value || maskField === previousKey ) {

                // the key is the actual mask
                const b = stripDots( key as string );

                // bitwise comparison on base 2
                result = parseInt( a , 2 ) & parseInt( b , 2 );
                if ( result != 0 ) {
                    return result;
                }
            }
            return 0;
        };

        // invert keys and values of the masks so we have option name as index of the object
        this.masksMappedByName( masks , onAssociation );

        return result;
    }

    /**
     * from a string mask containing 0 - 1 - 2, that produce an other mask;
     * "2" => "true":1 ... "1" => "false":0 ... "0" => default : "-"
     * @param setMask
     */
    private guessMaskFromMask( setMask: string ): string {
        const result = [];
        let i = 0;
        setMask = stripDots( setMask );

        for ( ; i < setMask.length; ++i ) {
            const c = setMask.charAt( i );
            result.push( c === "2" ? 1 : c === "1" ? 0 : "-" );
        }
        return result.join( "" );
    }

    private defineSortOption( modulopt: IHoldModulopt , bit: number , maskField: string ) {
        const offset = modulopt.optionsOffset;
        const representation = this.formatedNumberRepresentation( bit , offset );
        return modulopt.masks[ maskField ][ representation ];
    }

    private defineBooleansOption( defaults: IOptions , bit: string , option: string ) {
        switch ( bit ) {
            case "1" :
                return true;
            case "0" :
                return false;
            default :
                return defaults[ option ];
        }
    }

    private chosenFromMask( modulopt: IHoldModulopt , setMask: string , maskField: string ) {
        const a = substituteTwos( stripDots( setMask ) );
        const c = this.guessMaskFromMask( setMask );

        const result = this.applyMasks( modulopt.masks , a , maskField );

        if ( result > 0 ) {
            const bitPosFromRight = Math.log2( result );
            const position = -1 + c.length - bitPosFromRight;

            const offset = modulopt.optionsOffset;
            const representation = this.formatedNumberRepresentation( result , offset );
            const booleanMask = modulopt.masks[ representation ];

            // does the result indicates a bit value that aims a boolean ?
            if ( typeof booleanMask === "string" ) {
                const result = this.defineBooleansOption(
                    modulopt.defaults ,
                    c[ position ] ,
                    booleanMask
                );
                return result;
            } else {

                // INFO: Treat non binar cases
                return this.defineSortOption( modulopt , result , maskField );
            }
        }
        const option = /[\d.]/g.test( maskField )
            ? modulopt.masks[ maskField ]
            : maskField;

        return modulopt.defaults[ option ];
    }
}