string-kit
Version:
A string manipulation toolbox, featuring a string formatter (inspired by sprintf), a variable inspector (output featuring ANSI colors and HTML) and various escape functions (shell argument, regexp, html, etc).
1,499 lines (1,141 loc) • 42.1 kB
JavaScript
/*
String Kit
Copyright (c) 2014 - 2021 Cédric Ronvel
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
String formater, inspired by C's sprintf().
*/
"use strict" ;
const inspect = require( './inspect.js' ).inspect ;
const inspectError = require( './inspect.js' ).inspectError ;
const escape = require( './escape.js' ) ;
const ansi = require( './ansi.js' ) ;
const unicode = require( './unicode.js' ) ;
const naturalSort = require( './naturalSort.js' ) ;
const StringNumber = require( './StringNumber.js' ) ;
/*
%% a single %
%s string
%S string, interpret ^ formatting
%r raw string: without sanitizer
%n natural: output the most natural representation for this type, object entries are sorted by keys
%N even more natural: avoid type hinting marks like bracket for array
%f float
%k number with metric system prefixes
%e for exponential notation (e.g. 1.23e+2)
%K for scientific notation (e.g. 1.23 × 10²)
%i %d integer
%u unsigned integer
%U unsigned positive integer (>0)
%P number to (absolute) percent (e.g.: 0.75 -> 75%)
%p number to relative percent (e.g.: 1.25 -> +25% ; 0.75 -> -25%)
%T date/time display, using ISO date, or Intl module
%t time duration, convert ms into h min s, e.g.: 2h14min52s or 2:14:52
%m convert degree into degree, minutes and seconds
%h hexadecimal (input is a number)
%x hexadecimal (input is a number), force pair of symbols (e.g. 'f' -> '0f')
%o octal
%b binary
%X hexadecimal: convert a string into hex charcode, force pair of symbols (e.g. 'f' -> '0f')
%z base64
%Z base64url
%I call string-kit's inspect()
%Y call string-kit's inspect(), but do not inspect non-enumerable
%O object (like inspect, but with ultra minimal options)
%E call string-kit's inspectError()
%J JSON.stringify()
%v roman numerals, additive variant (e.g. 4 is IIII instead of IV)
%V roman numerals
%D drop
%F filter function existing in the 'this' context, e.g. %[filter:%a%a]F
%a argument for a function
Candidate format:
%A for automatic type? probably not good: it's like %n Natural
%c for char? (can receive a string or an integer translated into an UTF8 chars)
%C for currency formating?
%B for Buffer objects?
*/
exports.formatMethod = function( ... args ) {
var arg ,
str = args[ 0 ] ,
autoIndex = 1 ,
length = args.length ;
if ( typeof str !== 'string' ) {
if ( ! str ) { str = '' ; }
else if ( typeof str.toString === 'function' ) { str = str.toString() ; }
else { str = '' ; }
}
var runtime = {
hasMarkup: false ,
shift: null ,
markupStack: []
} ;
if ( this.markupReset && this.startingMarkupReset ) {
str = ( typeof this.markupReset === 'function' ? this.markupReset( runtime.markupStack ) : this.markupReset ) + str ;
}
//console.log( 'format args:' , arguments ) ;
// /!\ each changes here should be reported on string.format.count() and string.format.hasFormatting() too /!\
// Note: the closing bracket is optional to prevent ReDoS
str = str.replace( /\^\[([^\]]*)]?|\^(.)|(%%)|%([+-]?)([0-9]*)(?:\[([^\]]*)\])?([a-zA-Z])/g ,
( match , complexMarkup , markup , doublePercent , relative , index , modeArg , mode ) => {
var replacement , i , tmp , fn , fnArgString , argMatches , argList = [] ;
//console.log( 'replaceArgs:' , arguments ) ;
if ( doublePercent ) { return '%' ; }
if ( complexMarkup ) { markup = complexMarkup ; }
if ( markup ) {
if ( this.noMarkup ) { return '^' + markup ; }
return markupReplace.call( this , runtime , match , markup ) ;
}
if ( index ) {
index = parseInt( index , 10 ) ;
if ( relative ) {
if ( relative === '+' ) { index = autoIndex + index ; }
else if ( relative === '-' ) { index = autoIndex - index ; }
}
}
else {
index = autoIndex ;
}
autoIndex ++ ;
if ( index >= length || index < 1 ) { arg = undefined ; }
else { arg = args[ index ] ; }
if ( modes[ mode ] ) {
replacement = modes[ mode ]( arg , modeArg , this ) ;
if ( this.argumentSanitizer && ! modes[ mode ].noSanitize ) { replacement = this.argumentSanitizer( replacement ) ; }
if ( this.escapeMarkup && ! modes[ mode ].noEscapeMarkup ) { replacement = exports.escapeMarkup( replacement ) ; }
if ( modeArg && ! modes[ mode ].noCommonModeArg ) { replacement = commonModeArg( replacement , modeArg ) ; }
return replacement ;
}
// Function mode
if ( mode === 'F' ) {
autoIndex -- ; // %F does not eat any arg
if ( modeArg === undefined ) { return '' ; }
tmp = modeArg.split( ':' ) ;
fn = tmp[ 0 ] ;
fnArgString = tmp[ 1 ] ;
if ( ! fn ) { return '' ; }
if ( fnArgString && ( argMatches = fnArgString.match( /%([+-]?)([0-9]*)[a-zA-Z]/g ) ) ) {
//console.log( argMatches ) ;
//console.log( fnArgString ) ;
for ( i = 0 ; i < argMatches.length ; i ++ ) {
relative = argMatches[ i ][ 1 ] ;
index = argMatches[ i ][ 2 ] ;
if ( index ) {
index = parseInt( index , 10 ) ;
if ( relative ) {
if ( relative === '+' ) { index = autoIndex + index ; } // jshint ignore:line
else if ( relative === '-' ) { index = autoIndex - index ; } // jshint ignore:line
}
}
else {
index = autoIndex ;
}
autoIndex ++ ;
if ( index >= length || index < 1 ) { argList[ i ] = undefined ; }
else { argList[ i ] = args[ index ] ; }
}
}
if ( ! this || ! this.fn || typeof this.fn[ fn ] !== 'function' ) { return '' ; }
return this.fn[ fn ].apply( this , argList ) ;
}
return '' ;
}
) ;
if ( runtime.hasMarkup && this.markupReset && this.endingMarkupReset ) {
str += typeof this.markupReset === 'function' ? this.markupReset( runtime.markupStack ) : this.markupReset ;
}
if ( this.extraArguments ) {
for ( ; autoIndex < length ; autoIndex ++ ) {
arg = args[ autoIndex ] ;
if ( arg === null || arg === undefined ) { continue ; }
else if ( typeof arg === 'string' ) { str += arg ; }
else if ( typeof arg === 'number' ) { str += arg ; }
else if ( typeof arg.toString === 'function' ) { str += arg.toString() ; }
}
}
return str ;
} ;
exports.markupMethod = function( str ) {
if ( typeof str !== 'string' ) {
if ( ! str ) { str = '' ; }
else if ( typeof str.toString === 'function' ) { str = str.toString() ; }
else { str = '' ; }
}
var runtime = {
hasMarkup: false ,
shift: null ,
markupStack: []
} ;
if ( this.parse ) {
let markupObjects , markupObject , match , complexMarkup , markup , raw , lastChunk ,
output = [] ;
// Note: the closing bracket is optional to prevent ReDoS
for ( [ match , complexMarkup , markup , raw ] of str.matchAll( /\^\[([^\]]*)]?|\^(.)|([^^]+)/g ) ) {
if ( raw ) {
if ( output.length ) { output[ output.length - 1 ].text += raw ; }
else { output.push( { text: raw } ) ; }
continue ;
}
if ( complexMarkup ) { markup = complexMarkup ; }
markupObjects = markupReplace.call( this , runtime , match , markup ) ;
if ( ! Array.isArray( markupObjects ) ) { markupObjects = [ markupObjects ] ; }
for ( markupObject of markupObjects ) {
lastChunk = output.length ? output[ output.length - 1 ] : null ;
if ( typeof markupObject === 'string' ) {
// This markup is actually a text to add to the last chunk (e.g. "^^" markup is converted to a single "^")
if ( lastChunk ) { lastChunk.text += markupObject ; }
else { output.push( { text: markupObject } ) ; }
}
else if ( ! markupObject ) {
// Null is for a markup's style reset
if ( lastChunk && lastChunk.text.length && Object.keys( lastChunk ).length > 1 ) {
// If there was style and text on the last chunk, then this means that the new markup starts a new chunk
// markupObject can be null for markup reset function, but we have to create a new chunk
output.push( { text: '' } ) ;
}
}
else {
if ( lastChunk && lastChunk.text.length ) {
// If there was text on the last chunk, then this means that the new markup starts a new chunk
output.push( Object.assign( { text: '' } , ... runtime.markupStack ) ) ;
}
else {
// There wasn't any text added, so append the current markup style to the current chunk
if ( lastChunk ) { Object.assign( lastChunk , markupObject ) ; }
else { output.push( Object.assign( { text: '' } , markupObject ) ) ; }
}
}
}
}
return output ;
}
if ( this.markupReset && this.startingMarkupReset ) {
str = ( typeof this.markupReset === 'function' ? this.markupReset( runtime.markupStack ) : this.markupReset ) + str ;
}
str = str.replace( /\^\[([^\]]*)]?|\^(.)/g , ( match , complexMarkup , markup ) => markupReplace.call( this , runtime , match , complexMarkup || markup ) ) ;
if ( runtime.hasMarkup && this.markupReset && this.endingMarkupReset ) {
str += typeof this.markupReset === 'function' ? this.markupReset( runtime.markupStack ) : this.markupReset ;
}
return str ;
} ;
// Used by both formatMethod and markupMethod
function markupReplace( runtime , match , markup ) {
var markupTarget , key , value , replacement , colonIndex ;
if ( markup === '^' ) { return '^' ; }
if ( this.shiftMarkup && this.shiftMarkup[ markup ] ) {
runtime.shift = this.shiftMarkup[ markup ] ;
return '' ;
}
if ( markup.length > 1 && this.dataMarkup && ( colonIndex = markup.indexOf( ':' ) ) !== -1 ) {
key = markup.slice( 0 , colonIndex ) ;
markupTarget = this.dataMarkup[ key ] ;
if ( markupTarget === undefined ) {
if ( this.markupCatchAll === undefined ) { return '' ; }
markupTarget = this.markupCatchAll ;
}
runtime.hasMarkup = true ;
value = markup.slice( colonIndex + 1 ) ;
if ( typeof markupTarget === 'function' ) {
replacement = markupTarget( runtime.markupStack , key , value ) ;
// method should manage markup stack themselves
}
else {
replacement = { [ markupTarget ]: value } ;
stackMarkup( runtime , replacement ) ;
}
return replacement ;
}
if ( runtime.shift ) {
markupTarget = this.shiftedMarkup?.[ runtime.shift ]?.[ markup ] ;
runtime.shift = null ;
}
else {
markupTarget = this.markup?.[ markup ] ;
}
if ( markupTarget === undefined ) {
if ( this.markupCatchAll === undefined ) { return '' ; }
markupTarget = this.markupCatchAll ;
}
runtime.hasMarkup = true ;
if ( typeof markupTarget === 'function' ) {
replacement = markupTarget( runtime.markupStack , markup ) ;
// method should manage markup stack themselves
}
else {
replacement = markupTarget ;
stackMarkup( runtime , replacement ) ;
}
return replacement ;
}
// internal method for markupReplace()
function stackMarkup( runtime , replacement ) {
if ( Array.isArray( replacement ) ) {
for ( let item of replacement ) {
if ( item === null ) { runtime.markupStack.length = 0 ; }
else { runtime.markupStack.push( item ) ; }
}
}
else {
if ( replacement === null ) { runtime.markupStack.length = 0 ; }
else { runtime.markupStack.push( replacement ) ; }
}
}
// Note: the closing bracket is optional to prevent ReDoS
exports.stripMarkup = str => str.replace( /\^\[[^\]]*]?|\^./g , match =>
match === '^^' ? '^' :
match === '^ ' ? ' ' :
''
) ;
exports.escapeMarkup = str => str.replace( /\^/g , '^^' ) ;
const DEFAULT_FORMATTER = {
argumentSanitizer: str => escape.control( str , true ) ,
extraArguments: true ,
color: false ,
noMarkup: false ,
escapeMarkup: false ,
endingMarkupReset: true ,
startingMarkupReset: false ,
markupReset: ansi.reset ,
shiftMarkup: {
'#': 'background'
} ,
markup: {
":": ansi.reset ,
" ": ansi.reset + " " ,
"-": ansi.dim ,
"+": ansi.bold ,
"_": ansi.underline ,
"/": ansi.italic ,
"!": ansi.inverse ,
"b": ansi.blue ,
"B": ansi.brightBlue ,
"c": ansi.cyan ,
"C": ansi.brightCyan ,
"g": ansi.green ,
"G": ansi.brightGreen ,
"k": ansi.black ,
"K": ansi.brightBlack ,
"m": ansi.magenta ,
"M": ansi.brightMagenta ,
"r": ansi.red ,
"R": ansi.brightRed ,
"w": ansi.white ,
"W": ansi.brightWhite ,
"y": ansi.yellow ,
"Y": ansi.brightYellow
} ,
shiftedMarkup: {
background: {
":": ansi.reset ,
" ": ansi.reset + " " ,
"b": ansi.bgBlue ,
"B": ansi.bgBrightBlue ,
"c": ansi.bgCyan ,
"C": ansi.bgBrightCyan ,
"g": ansi.bgGreen ,
"G": ansi.bgBrightGreen ,
"k": ansi.bgBlack ,
"K": ansi.bgBrightBlack ,
"m": ansi.bgMagenta ,
"M": ansi.bgBrightMagenta ,
"r": ansi.bgRed ,
"R": ansi.bgBrightRed ,
"w": ansi.bgWhite ,
"W": ansi.bgBrightWhite ,
"y": ansi.bgYellow ,
"Y": ansi.bgBrightYellow
}
} ,
dataMarkup: {
fg: ( markupStack , key , value ) => {
var str = ansi.fgColor[ value ] || ansi.trueColor( value ) ;
markupStack.push( str ) ;
return str ;
} ,
bg: ( markupStack , key , value ) => {
var str = ansi.bgColor[ value ] || ansi.bgTrueColor( value ) ;
markupStack.push( str ) ;
return str ;
}
} ,
markupCatchAll: ( markupStack , key , value ) => {
var str = '' ;
if ( value === undefined ) {
if ( key[ 0 ] === '#' ) {
str = ansi.trueColor( key ) ;
}
else if ( typeof ansi[ key ] === 'string' ) {
str = ansi[ key ] ;
}
}
markupStack.push( str ) ;
return str ;
}
} ;
// Aliases
DEFAULT_FORMATTER.dataMarkup.color = DEFAULT_FORMATTER.dataMarkup.c = DEFAULT_FORMATTER.dataMarkup.fgColor = DEFAULT_FORMATTER.dataMarkup.fg ;
DEFAULT_FORMATTER.dataMarkup.bgColor = DEFAULT_FORMATTER.dataMarkup.bg ;
exports.createFormatter = ( options ) => exports.formatMethod.bind( Object.assign( {} , DEFAULT_FORMATTER , options ) ) ;
exports.format = exports.formatMethod.bind( DEFAULT_FORMATTER ) ;
exports.format.default = DEFAULT_FORMATTER ;
exports.formatNoMarkup = exports.formatMethod.bind( Object.assign( {} , DEFAULT_FORMATTER , { noMarkup: true } ) ) ;
// For passing string to Terminal-Kit, it will interpret markup on its own
exports.formatThirdPartyMarkup = exports.formatMethod.bind( Object.assign( {} , DEFAULT_FORMATTER , { noMarkup: true , escapeMarkup: true } ) ) ;
exports.createMarkup = ( options ) => exports.markupMethod.bind( Object.assign( {} , DEFAULT_FORMATTER , options ) ) ;
exports.markup = exports.markupMethod.bind( DEFAULT_FORMATTER ) ;
// Count the number of parameters needed for this string
exports.format.count = function( str , noMarkup = false ) {
var markup , index , relative , autoIndex = 1 , maxIndex = 0 ;
if ( typeof str !== 'string' ) { return 0 ; }
// This regex differs slightly from the main regex: we do not count '%%' and %F is excluded
// Note: the closing bracket is optional to prevent ReDoS
var regexp = noMarkup ?
/%([+-]?)([0-9]*)(?:\[[^\]]*\])?[a-zA-EG-Z]/g :
/%([+-]?)([0-9]*)(?:\[[^\]]*\])?[a-zA-EG-Z]|(\^\[[^\]]*]?|\^.)/g ;
for ( [ , relative , index , markup ] of str.matchAll( regexp ) ) {
if ( markup ) { continue ; }
if ( index ) {
index = parseInt( index , 10 ) ;
if ( relative ) {
if ( relative === '+' ) { index = autoIndex + index ; }
else if ( relative === '-' ) { index = autoIndex - index ; }
}
}
else {
index = autoIndex ;
}
autoIndex ++ ;
if ( maxIndex < index ) { maxIndex = index ; }
}
return maxIndex ;
} ;
// Tell if this string contains formatter chars
exports.format.hasFormatting = function( str ) {
if ( str.search( /\^(.?)|(%%)|%([+-]?)([0-9]*)(?:\[([^\]]*)\])?([a-zA-Z])/ ) !== -1 ) { return true ; }
return false ;
} ;
// --- Format MODES ---
const modes = {} ;
exports.format.modes = modes ; // <-- expose modes, used by Babel-Tower for String Kit interop'
// string
modes.s = ( arg , modeArg ) => {
var subModes = stringModeArg( modeArg ) ;
if ( typeof arg === 'string' ) { return arg ; }
if ( arg === null || arg === undefined || arg === false ) { return subModes.empty ? '' : '(' + arg + ')' ; }
if ( arg === true ) { return '(' + arg + ')' ; }
if ( typeof arg === 'number' ) { return '' + arg ; }
if ( typeof arg.toString === 'function' ) { return arg.toString() ; }
return '(' + arg + ')' ;
} ;
modes.r = arg => modes.s( arg ) ;
modes.r.noSanitize = true ;
// string, interpret ^ formatting
modes.S = ( arg , modeArg , options ) => {
var subModes = stringModeArg( modeArg ) ;
// We do the sanitizing part on our own
var interpret = options.escapeMarkup ? str => ( options.argumentSanitizer ? options.argumentSanitizer( str ) : str ) :
str => exports.markupMethod.call( options , options.argumentSanitizer ? options.argumentSanitizer( str ) : str ) ;
if ( typeof arg === 'string' ) { return interpret( arg ) ; }
if ( arg === null || arg === undefined || arg === false ) { return subModes.empty ? '' : '(' + arg + ')' ; }
if ( arg === true ) { return '(' + arg + ')' ; }
if ( typeof arg === 'number' ) { return '' + arg ; }
if ( typeof arg.toString === 'function' ) { return interpret( arg.toString() ) ; }
return interpret( '(' + arg + ')' ) ;
} ;
modes.S.noSanitize = true ;
modes.S.noEscapeMarkup = true ;
modes.S.noCommonModeArg = true ;
// natural (WIP)
modes.N = ( arg , modeArg ) => genericNaturalMode( arg , modeArg , false ) ;
modes.n = ( arg , modeArg ) => genericNaturalMode( arg , modeArg , true ) ;
// float
modes.f = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
var subModes = floatModeArg( modeArg ) ,
sn = new StringNumber( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
if ( subModes.rounding !== null ) { sn.round( subModes.rounding ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
return sn.toString( subModes.leftPadding , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
} ;
modes.f.noSanitize = true ;
// roman numeral, additive variant
modes.v = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
var subModes = floatModeArg( modeArg ) ,
sn = StringNumber.additiveRoman( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
if ( subModes.rounding !== null ) { sn.round( subModes.rounding ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
return sn.toString( subModes.leftPadding , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
} ;
modes.v.noSanitize = true ;
// roman numeral
modes.V = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
var subModes = floatModeArg( modeArg ) ,
sn = StringNumber.roman( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
if ( subModes.rounding !== null ) { sn.round( subModes.rounding ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
return sn.toString( subModes.leftPadding , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
} ;
modes.V.noSanitize = true ;
// absolute percent
modes.P = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
arg *= 100 ;
var subModes = floatModeArg( modeArg ) ,
sn = new StringNumber( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
// Force rounding to zero by default
if ( subModes.rounding !== null || ! subModes.precision ) { sn.round( subModes.rounding || 0 ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
return sn.toNoExpString( subModes.leftPadding , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) + '%' ;
} ;
modes.P.noSanitize = true ;
// relative percent
modes.p = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
arg = ( arg - 1 ) * 100 ;
var subModes = floatModeArg( modeArg ) ,
sn = new StringNumber( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
// Force rounding to zero by default
if ( subModes.rounding !== null || ! subModes.precision ) { sn.round( subModes.rounding || 0 ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
// 4th argument force a '+' sign
return sn.toNoExpString( subModes.leftPadding , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal , true ) + '%' ;
} ;
modes.p.noSanitize = true ;
// metric system
modes.k = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { return '0' ; }
var subModes = floatModeArg( modeArg ) ,
sn = new StringNumber( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
if ( subModes.rounding !== null ) { sn.round( subModes.rounding ) ; }
// Default to 3 numbers precision
if ( subModes.precision || subModes.rounding === null ) { sn.precision( subModes.precision || 3 ) ; }
return sn.toMetricString( subModes.leftPadding , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
} ;
modes.k.noSanitize = true ;
// exponential notation, a.k.a. "E notation" (e.g. 1.23e+2)
modes.e = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
var subModes = floatModeArg( modeArg ) ,
sn = new StringNumber( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
if ( subModes.rounding !== null ) { sn.round( subModes.rounding ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
return sn.toExponential() ;
} ;
modes.e.noSanitize = true ;
// scientific notation (e.g. 1.23 × 10²)
modes.K = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { arg = 0 ; }
var subModes = floatModeArg( modeArg ) ,
sn = new StringNumber( arg , { decimalSeparator: '.' , groupSeparator: subModes.groupSeparator } ) ;
if ( subModes.rounding !== null ) { sn.round( subModes.rounding ) ; }
if ( subModes.precision ) { sn.precision( subModes.precision ) ; }
return sn.toScientific() ;
} ;
modes.K.noSanitize = true ;
// integer
modes.d = modes.i = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg === 'number' ) { return '' + Math.floor( arg ) ; }
return '0' ;
} ;
modes.i.noSanitize = true ;
// unsigned integer
modes.u = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg === 'number' ) { return '' + Math.max( Math.floor( arg ) , 0 ) ; }
return '0' ;
} ;
modes.u.noSanitize = true ;
// unsigned positive integer
modes.U = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg === 'number' ) { return '' + Math.max( Math.floor( arg ) , 1 ) ; }
return '1' ;
} ;
modes.U.noSanitize = true ;
// /!\ Should use StringNumber???
// Degree, minutes and seconds.
// Unlike %t which receive ms, here the input is in degree.
modes.m = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { return '(NaN)' ; }
var minus = '' ;
if ( arg < 0 ) { minus = '-' ; arg = -arg ; }
var degrees = epsilonFloor( arg ) ,
frac = arg - degrees ;
if ( ! frac ) { return minus + degrees + '°' ; }
var minutes = epsilonFloor( frac * 60 ) ,
seconds = epsilonFloor( frac * 3600 - minutes * 60 ) ;
if ( seconds ) {
return minus + degrees + '°' + ( '' + minutes ).padStart( 2 , '0' ) + '′' + ( '' + seconds ).padStart( 2 , '0' ) + '″' ;
}
return minus + degrees + '°' + ( '' + minutes ).padStart( 2 , '0' ) + '′' ;
} ;
modes.m.noSanitize = true ;
// Date/time
// Minimal Date formating, only support sort of ISO ATM.
// It will be improved later.
modes.T = ( arg , modeArg ) => {
// Always get a copy of the arg
try {
arg = new Date( arg ) ;
}
catch ( error ) {
return '(invalid)' ;
}
if ( Number.isNaN( arg.getTime() ) ) {
return '(invalid)' ;
}
var datePart = '' ,
timePart = '' ,
str = '' ,
subModes = dateTimeModeArg( modeArg ) ,
roundingType = subModes.roundingType ,
forceDecimalSeparator = subModes.useAbbreviation ;
// For instance, we only support the ISO-like type
if ( subModes.years ) {
if ( datePart ) { datePart += '-' ; }
datePart += arg.getFullYear() ;
}
if ( subModes.months ) {
if ( datePart ) { datePart += '-' ; }
datePart += ( '' + ( arg.getMonth() + 1 ) ).padStart( 2 , '0' ) ;
}
if ( subModes.days ) {
if ( datePart ) { datePart += '-' ; }
datePart += ( '' + arg.getDate() ).padStart( 2 , '0' ) ;
}
if ( subModes.hours ) {
if ( timePart && ! subModes.useAbbreviation ) { timePart += ':' ; }
timePart += ( '' + arg.getHours() ).padStart( 2 , '0' ) ;
if ( subModes.useAbbreviation ) { timePart += 'h' ; }
}
if ( subModes.minutes ) {
if ( timePart && ! subModes.useAbbreviation ) { timePart += ':' ; }
timePart += ( '' + arg.getMinutes() ).padStart( 2 , '0' ) ;
if ( subModes.useAbbreviation ) { timePart += 'min' ; }
}
if ( subModes.seconds ) {
if ( timePart && ! subModes.useAbbreviation ) { timePart += ':' ; }
timePart += ( '' + arg.getSeconds() ).padStart( 2 , '0' ) ;
if ( subModes.useAbbreviation ) { timePart += 's' ; }
}
if ( datePart ) {
if ( str ) { str += ' ' ; }
str += datePart ;
}
if ( timePart ) {
if ( str ) { str += ' ' ; }
str += timePart ;
}
return str ;
} ;
modes.T.noSanitize = true ;
// Time duration, transform ms into H:min:s
modes.t = ( arg , modeArg ) => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { return '(NaN)' ; }
var h , min , s , sn , sStr ,
sign = '' ,
subModes = timeDurationModeArg( modeArg ) ,
roundingType = subModes.roundingType ,
hSeparator = subModes.useAbbreviation ? 'h' : ':' ,
minSeparator = subModes.useAbbreviation ? 'min' : ':' ,
sSeparator = subModes.useAbbreviation ? 's' : '.' ,
forceDecimalSeparator = subModes.useAbbreviation ;
s = arg / 1000 ;
if ( s < 0 ) {
s = -s ;
roundingType *= -1 ;
sign = '-' ;
}
if ( s < 60 && ! subModes.forceMinutes ) {
sn = new StringNumber( s , { decimalSeparator: sSeparator , forceDecimalSeparator } ) ;
sn.round( subModes.rounding , roundingType ) ;
// Check if rounding has made it reach 60
if ( sn.toNumber() < 60 ) {
sStr = sn.toString( 1 , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
return sign + sStr ;
}
s = 60 ;
}
min = Math.floor( s / 60 ) ;
s = s % 60 ;
sn = new StringNumber( s , { decimalSeparator: sSeparator , forceDecimalSeparator } ) ;
sn.round( subModes.rounding , roundingType ) ;
// Check if rounding has made it reach 60
if ( sn.toNumber() < 60 ) {
sStr = sn.toString( 2 , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
}
else {
min ++ ;
s = 0 ;
sn.set( s ) ;
sStr = sn.toString( 2 , subModes.rightPadding , subModes.rightPaddingOnlyIfDecimal ) ;
}
if ( min < 60 && ! subModes.forceHours ) {
return sign + min + minSeparator + sStr ;
}
h = Math.floor( min / 60 ) ;
min = min % 60 ;
return sign + h + hSeparator + ( '' + min ).padStart( 2 , '0' ) + minSeparator + sStr ;
} ;
modes.t.noSanitize = true ;
// unsigned hexadecimal
modes.h = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg === 'number' ) { return '' + Math.max( Math.floor( arg ) , 0 ).toString( 16 ) ; }
return '0' ;
} ;
modes.h.noSanitize = true ;
// unsigned hexadecimal, force pair of symboles
modes.x = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg !== 'number' ) { return '00' ; }
var value = '' + Math.max( Math.floor( arg ) , 0 ).toString( 16 ) ;
if ( value.length % 2 ) { value = '0' + value ; }
return value ;
} ;
modes.x.noSanitize = true ;
// unsigned octal
modes.o = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg === 'number' ) { return '' + Math.max( Math.floor( arg ) , 0 ).toString( 8 ) ; }
return '0' ;
} ;
modes.o.noSanitize = true ;
// unsigned binary
modes.b = arg => {
if ( typeof arg === 'string' ) { arg = parseFloat( arg ) ; }
if ( typeof arg === 'number' ) { return '' + Math.max( Math.floor( arg ) , 0 ).toString( 2 ) ; }
return '0' ;
} ;
modes.b.noSanitize = true ;
// String to hexadecimal, force pair of symboles
modes.X = arg => {
if ( typeof arg === 'string' ) { arg = Buffer.from( arg ) ; }
else if ( ! Buffer.isBuffer( arg ) ) { return '' ; }
return arg.toString( 'hex' ) ;
} ;
modes.X.noSanitize = true ;
// base64
modes.z = arg => {
if ( typeof arg === 'string' ) { arg = Buffer.from( arg ) ; }
else if ( ! Buffer.isBuffer( arg ) ) { return '' ; }
return arg.toString( 'base64' ) ;
} ;
// base64url
modes.Z = arg => {
if ( typeof arg === 'string' ) { arg = Buffer.from( arg ) ; }
else if ( ! Buffer.isBuffer( arg ) ) { return '' ; }
return arg.toString( 'base64' ).replace( /\+/g , '-' )
.replace( /\//g , '_' )
.replace( /[=]{1,2}$/g , '' ) ;
} ;
// Inspect
const I_OPTIONS = {} ;
modes.I = ( arg , modeArg , options ) => genericInspectMode( arg , modeArg , options , I_OPTIONS ) ;
modes.I.noSanitize = true ;
// More minimalist inspect
const Y_OPTIONS = {
noFunc: true ,
enumOnly: true ,
noDescriptor: true ,
useInspect: true ,
useInspectPropertyBlackList: true
} ;
modes.Y = ( arg , modeArg , options ) => genericInspectMode( arg , modeArg , options , Y_OPTIONS ) ;
modes.Y.noSanitize = true ;
// Even more minimalist inspect
const O_OPTIONS = { minimal: true , bulletIndex: true , noMarkup: true } ;
modes.O = ( arg , modeArg , options ) => genericInspectMode( arg , modeArg , options , O_OPTIONS ) ;
modes.O.noSanitize = true ;
// Inspect error
const E_OPTIONS = {} ;
modes.E = ( arg , modeArg , options ) => genericInspectMode( arg , modeArg , options , E_OPTIONS , true ) ;
modes.E.noSanitize = true ;
// JSON
modes.J = arg => arg === undefined ? 'null' : JSON.stringify( arg ) ;
// drop
modes.D = () => '' ;
modes.D.noSanitize = true ;
// ModeArg formats
// The format for commonModeArg
const COMMON_MODE_ARG_FORMAT_REGEX = /([a-zA-Z])(.[^a-zA-Z]*)/g ;
// The format for specific mode arg
const MODE_ARG_FORMAT_REGEX = /([a-zA-Z]|^)([^a-zA-Z]*)/g ;
// Called when there is a modeArg and the mode allow common mode arg
// CONVENTION: reserve upper-cased letters for common mode arg
function commonModeArg( str , modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( COMMON_MODE_ARG_FORMAT_REGEX ) ) {
if ( k === 'L' ) {
let width = unicode.width( str ) ;
v = + v || 1 ;
if ( width > v ) {
str = unicode.truncateWidth( str , v - 1 ).trim() + '…' ;
width = unicode.width( str ) ;
}
if ( width < v ) { str = ' '.repeat( v - width ) + str ; }
}
else if ( k === 'R' ) {
let width = unicode.width( str ) ;
v = + v || 1 ;
if ( width > v ) {
str = unicode.truncateWidth( str , v - 1 ).trim() + '…' ;
width = unicode.width( str ) ;
}
if ( width < v ) { str = str + ' '.repeat( v - width ) ; }
}
}
return str ;
}
const FLOAT_MODES = {
leftPadding: 1 ,
rightPadding: 0 ,
rightPaddingOnlyIfDecimal: false ,
rounding: null ,
precision: null ,
groupSeparator: ''
} ;
// Generic number modes
function floatModeArg( modeArg ) {
FLOAT_MODES.leftPadding = 1 ;
FLOAT_MODES.rightPadding = 0 ;
FLOAT_MODES.rightPaddingOnlyIfDecimal = false ;
FLOAT_MODES.rounding = null ;
FLOAT_MODES.precision = null ;
FLOAT_MODES.groupSeparator = '' ;
if ( modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( MODE_ARG_FORMAT_REGEX ) ) {
if ( k === 'z' ) {
// Zero-left padding
FLOAT_MODES.leftPadding = + v ;
}
else if ( k === 'g' ) {
// Group separator
FLOAT_MODES.groupSeparator = v || ' ' ;
}
else if ( ! k ) {
if ( v[ 0 ] === '.' ) {
// Rounding after the decimal
let lv = v[ v.length - 1 ] ;
// Zero-right padding?
if ( lv === '!' ) {
FLOAT_MODES.rounding = FLOAT_MODES.rightPadding = parseInt( v.slice( 1 , -1 ) , 10 ) || 0 ;
}
else if ( lv === '?' ) {
FLOAT_MODES.rounding = FLOAT_MODES.rightPadding = parseInt( v.slice( 1 , -1 ) , 10 ) || 0 ;
FLOAT_MODES.rightPaddingOnlyIfDecimal = true ;
}
else {
FLOAT_MODES.rounding = parseInt( v.slice( 1 ) , 10 ) || 0 ;
}
}
else if ( v[ v.length - 1 ] === '.' ) {
// Rounding before the decimal
FLOAT_MODES.rounding = -parseInt( v.slice( 0 , -1 ) , 10 ) || 0 ;
}
else {
// Precision, but only if integer
FLOAT_MODES.precision = parseInt( v , 10 ) || null ;
}
}
}
}
return FLOAT_MODES ;
}
const STRING_MODES = {
empty: false
} ;
// Generic number modes
function stringModeArg( modeArg ) {
STRING_MODES.empty = false ;
if ( modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( MODE_ARG_FORMAT_REGEX ) ) {
if ( k === 'e' ) {
// Empty mode:
STRING_MODES.empty = true ;
}
}
}
return STRING_MODES ;
}
const DATE_TIME_MODES = {
useAbbreviation: false ,
rightPadding: 0 ,
rightPaddingOnlyIfDecimal: false ,
years: true ,
months: true ,
days: true ,
hours: true ,
minutes: true ,
seconds: true
} ;
// Generic number modes
function dateTimeModeArg( modeArg ) {
DATE_TIME_MODES.rightPadding = 0 ;
DATE_TIME_MODES.rightPaddingOnlyIfDecimal = false ;
DATE_TIME_MODES.rounding = 0 ;
DATE_TIME_MODES.roundingType = -1 ;
DATE_TIME_MODES.years = DATE_TIME_MODES.months = DATE_TIME_MODES.days = false ;
DATE_TIME_MODES.hours = DATE_TIME_MODES.minutes = DATE_TIME_MODES.seconds = false ;
DATE_TIME_MODES.useAbbreviation = false ;
var hasSelector = false ;
if ( modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( MODE_ARG_FORMAT_REGEX ) ) {
if ( k === 'T' ) {
DATE_TIME_MODES.years = DATE_TIME_MODES.months = DATE_TIME_MODES.days = false ;
DATE_TIME_MODES.hours = DATE_TIME_MODES.minutes = DATE_TIME_MODES.seconds = true ;
hasSelector = true ;
}
else if ( k === 'D' ) {
DATE_TIME_MODES.years = DATE_TIME_MODES.months = DATE_TIME_MODES.days = true ;
DATE_TIME_MODES.hours = DATE_TIME_MODES.minutes = DATE_TIME_MODES.seconds = false ;
hasSelector = true ;
}
else if ( k === 'Y' ) {
DATE_TIME_MODES.years = true ;
hasSelector = true ;
}
else if ( k === 'M' ) {
DATE_TIME_MODES.months = true ;
hasSelector = true ;
}
else if ( k === 'd' ) {
DATE_TIME_MODES.days = true ;
hasSelector = true ;
}
else if ( k === 'h' ) {
DATE_TIME_MODES.hours = true ;
hasSelector = true ;
}
else if ( k === 'm' ) {
DATE_TIME_MODES.minutes = true ;
hasSelector = true ;
}
else if ( k === 's' ) {
DATE_TIME_MODES.seconds = true ;
hasSelector = true ;
}
else if ( k === 'r' ) {
DATE_TIME_MODES.roundingType = 0 ;
}
else if ( k === 'f' ) {
DATE_TIME_MODES.roundingType = -1 ;
}
else if ( k === 'c' ) {
DATE_TIME_MODES.roundingType = 1 ;
}
else if ( k === 'a' ) {
DATE_TIME_MODES.useAbbreviation = true ;
}
else if ( ! k ) {
if ( v[ 0 ] === '.' ) {
// Rounding after the decimal
let lv = v[ v.length - 1 ] ;
// Zero-right padding?
if ( lv === '!' ) {
DATE_TIME_MODES.rounding = DATE_TIME_MODES.rightPadding = parseInt( v.slice( 1 , -1 ) , 10 ) || 0 ;
}
else if ( lv === '?' ) {
DATE_TIME_MODES.rounding = DATE_TIME_MODES.rightPadding = parseInt( v.slice( 1 , -1 ) , 10 ) || 0 ;
DATE_TIME_MODES.rightPaddingOnlyIfDecimal = true ;
}
else {
DATE_TIME_MODES.rounding = parseInt( v.slice( 1 ) , 10 ) || 0 ;
}
}
}
}
}
if ( ! hasSelector ) {
DATE_TIME_MODES.years = DATE_TIME_MODES.months = DATE_TIME_MODES.days = true ;
DATE_TIME_MODES.hours = DATE_TIME_MODES.minutes = DATE_TIME_MODES.seconds = true ;
}
return DATE_TIME_MODES ;
}
const TIME_DURATION_MODES = {
useAbbreviation: false ,
rightPadding: 0 ,
rightPaddingOnlyIfDecimal: false ,
rounding: 0 ,
roundingType: -1 , // -1: floor, 0: round, 1: ceil
forceHours: false ,
forceMinutes: false
} ;
// Generic number modes
function timeDurationModeArg( modeArg ) {
TIME_DURATION_MODES.rightPadding = 0 ;
TIME_DURATION_MODES.rightPaddingOnlyIfDecimal = false ;
TIME_DURATION_MODES.rounding = 0 ;
TIME_DURATION_MODES.roundingType = -1 ;
TIME_DURATION_MODES.useAbbreviation = TIME_DURATION_MODES.forceHours = TIME_DURATION_MODES.forceMinutes = false ;
if ( modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( MODE_ARG_FORMAT_REGEX ) ) {
if ( k === 'h' ) {
TIME_DURATION_MODES.forceHours = TIME_DURATION_MODES.forceMinutes = true ;
}
else if ( k === 'm' ) {
TIME_DURATION_MODES.forceMinutes = true ;
}
else if ( k === 'r' ) {
TIME_DURATION_MODES.roundingType = 0 ;
}
else if ( k === 'f' ) {
TIME_DURATION_MODES.roundingType = -1 ;
}
else if ( k === 'c' ) {
TIME_DURATION_MODES.roundingType = 1 ;
}
else if ( k === 'a' ) {
TIME_DURATION_MODES.useAbbreviation = true ;
}
else if ( ! k ) {
if ( v[ 0 ] === '.' ) {
// Rounding after the decimal
let lv = v[ v.length - 1 ] ;
// Zero-right padding?
if ( lv === '!' ) {
TIME_DURATION_MODES.rounding = TIME_DURATION_MODES.rightPadding = parseInt( v.slice( 1 , -1 ) , 10 ) || 0 ;
}
else if ( lv === '?' ) {
TIME_DURATION_MODES.rounding = TIME_DURATION_MODES.rightPadding = parseInt( v.slice( 1 , -1 ) , 10 ) || 0 ;
TIME_DURATION_MODES.rightPaddingOnlyIfDecimal = true ;
}
else {
TIME_DURATION_MODES.rounding = parseInt( v.slice( 1 ) , 10 ) || 0 ;
}
}
}
}
}
return TIME_DURATION_MODES ;
}
// Generic Natural Mode
function genericNaturalMode( arg , modeArg , delimiters ) {
var depthLimit = 2 ;
if ( modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( MODE_ARG_FORMAT_REGEX ) ) {
if ( ! k ) {
depthLimit = parseInt( v , 10 ) || 1 ;
}
}
}
return genericNaturalModeRecursive( arg , delimiters , depthLimit , 0 ) ;
}
function genericNaturalModeRecursive( arg , delimiters , depthLimit , depth ) {
if ( typeof arg === 'string' ) { return arg ; }
if ( arg === null || arg === undefined || arg === true || arg === false ) {
return '' + arg ;
}
if ( typeof arg === 'number' ) {
return modes.f( arg , '.3g ' ) ;
}
if ( arg instanceof Set ) { arg = [ ... arg ] ; }
if ( Array.isArray( arg ) ) {
if ( depth >= depthLimit ) { return '[...]' ; }
arg = arg.map( e => genericNaturalModeRecursive( e , true , depthLimit , depth + 1 ) ) ;
if ( delimiters ) { return '[' + arg.join( ',' ) + ']' ; }
return arg.join( ', ' ) ;
}
if ( Buffer.isBuffer( arg ) ) {
arg = [ ... arg ].map( e => {
e = e.toString( 16 ) ;
if ( e.length === 1 ) { e = '0' + e ; }
return e ;
} ) ;
return '<' + arg.join( ' ' ) + '>' ;
}
var proto = Object.getPrototypeOf( arg ) ;
if ( proto === null || proto === Object.prototype ) {
// Plain objects
if ( depth >= depthLimit ) { return '{...}' ; }
arg = Object.entries( arg ).sort( naturalSort )
.map( e => e[ 0 ] + ': ' + genericNaturalModeRecursive( e[ 1 ] , true , depthLimit , depth + 1 ) ) ;
if ( delimiters ) { return '{' + arg.join( ', ' ) + '}' ; }
return arg.join( ', ' ) ;
}
if ( typeof arg.inspect === 'function' ) { return arg.inspect() ; }
if ( typeof arg.toString === 'function' ) { return arg.toString() ; }
return '(' + arg + ')' ;
}
// Generic inspect
function genericInspectMode( arg , modeArg , options , modeOptions , isInspectError = false ) {
var outputMaxLength ,
maxLength ,
depth = 3 ,
style = options && options.color ? 'color' : 'none' ;
if ( modeArg ) {
for ( let [ , k , v ] of modeArg.matchAll( MODE_ARG_FORMAT_REGEX ) ) {
if ( k === 'c' ) {
if ( v === '+' ) { style = 'color' ; }
else if ( v === '-' ) { style = 'none' ; }
}
else if ( k === 'i' ) {
style = 'inline' ;
}
else if ( k === 'l' ) {
// total output max length
outputMaxLength = parseInt( v , 10 ) || undefined ;
}
else if ( k === 's' ) {
// string max length
maxLength = parseInt( v , 10 ) || undefined ;
}
else if ( ! k ) {
depth = parseInt( v , 10 ) || 1 ;
}
}
}
if ( isInspectError ) {
return inspectError( Object.assign( {
depth , style , outputMaxLength , maxLength
} , modeOptions ) , arg ) ;
}
return inspect( Object.assign( {
depth , style , outputMaxLength , maxLength
} , modeOptions ) , arg ) ;
}
// From math-kit module
// /!\ Should be updated with the new way the math-kit module do it!!! /!\
const EPSILON = 0.0000000001 ;
const INVERSE_EPSILON = Math.round( 1 / EPSILON ) ;
function epsilonRound( v ) {
return Math.round( v * INVERSE_EPSILON ) / INVERSE_EPSILON ;
}
function epsilonFloor( v ) {
return Math.floor( v + EPSILON ) ;
}
// Round with precision
function round( v , step ) {
// use: v * ( 1 / step )
// not: v / step
// reason: epsilon rounding errors
return epsilonRound( step * Math.round( v * ( 1 / step ) ) ) ;
}