UNPKG

sprint

Version:

Complex sprintf() implementation

230 lines (200 loc) 6.73 kB
# This allows the compiler to precompile format RegExp without doing that # every time it is used. format = /// % (?: # Simply % % | # Argument number (\d+[$])? # Flags ((?:[+\x20\-#0])*) # Vector flag ((?: [*] (?: \d+ [$] )? )? v)? # Width (\d* | [*] (?: \d+ [$] )?) # Precision (?: [.] (\d+ | [*] (?: \d+ [$] )? ) )? # Length (hh?|ll?|[Lzjtq]|I(?:32|64)?)? # Type ([diuDUfFeEgGxXoOscpnbBr]) ) ///g # 32-bit numbers are internal ECMAScript limitation. In fact, you cannot # exactly represent 2 ** 64 in ECMAScript, so emulating it would be # mostly useless. errorMessage = "64-bit numbers aren't supported by sprint()!" intSizeTable = h: 2 hh: 1 l: 4 ll: new RangeError errorMessage L: 4 z: 4 j: 4 t: 4 I: 4 I32: 4 I64: new RangeError errorMessage q: new RangeError errorMessage sprint = (string, values...) -> arrayObjects = ['[object Array]', '[object Arguments]'] # IE doesn't understand toString() toString = Object.prototype.toString # Detect values sent as array if toString.call(values[0]) in arrayObjects and values.length is 1 values = dearrayified = values[0] i = -1 padString = (string, length, joiner, leftPad) -> string = "#{string}" if string.length > length string else if leftPad # new Array(2 + 1).join('-') creates '--' because it generates array of # undefined elements (when converted to strings they become nothing). # Every array value is joined using specified value. Note that joining # only happens inside strings, so you need to add 1 to array # constructor in order to get required number of characters. "#{string}#{new Array(length - string.length + 1).join joiner}" else "#{new Array(length - string.length + 1).join joiner}#{string}" "#{string}".replace format, (string, argument, flags, vector, length, precision, intSize, type) -> intSize ?= 'L' return '%' if string is '%%' leftPad = '-' in flags alignCharacter = if '0' in flags and not leftPad then '0' else ' ' abs = (number, signed = no) -> # Special case to avoid processing in commonly used %d modifier return parseInt number, 10 if intSize is 'L' and (number >= 0 or signed) entry = intSizeTable[intSize] throw entry if entry instanceof Error bits = entry * 8 number = parseInt(number, 10) % Math.pow 2, bits highValue = Math.pow(2, bits) - 1 if signed and number >= Math.pow 2, bits - 1 number = -Math.pow(2, bits) + number if signed then number else number >>> 0 toExponential = -> argument = (+argument).toExponential(precision) if special and '.' not in argument argument = argument.replace 'e', '.e' argument.toLowerCase().replace /\d+$/, (string) -> padString string, 3, 0 padInteger = (string) -> # Special casing if +string is 0 and +precision is 0 '' # This behavior happens in other programming languages else if defaultPrecision string else alignCharacter = ' ' padString string, precision, '0' if vector character = if vector[0] is '*' if vector.length > 2 values[parseInt(vector[1..], 10) - 1] else values[++i] else '.' length = if length[0] is '*' if length.length is 1 values[++i] else values[parseInt(length[1..]) - 1] else if not length 0 else length precision = if precision and precision[0] is '*' if precision.length is 1 values[++i] else values[parseInt(precision[1..], 10) - 1] else if not precision defaultPrecision = yes 6 else precision # After removing 1 we get value equal to 0. Zero is negative value, so # we need tricks to avoid any problems with values equal to 0. argument = values[(parseInt(argument, 10) or ++i + 1) - 1] # Detect negative zero if argument is 0 if 1 / argument is -Infinity argument = '-0' argument = if argument? then argument else '' special = '#' in flags args = if vector letter.charCodeAt 0 for letter in argument else [argument] result = for argument in args argument = switch type when 'd', 'i', 'D' padInteger abs argument, yes when 'u', 'U' padInteger abs argument when 'f', 'F' argument = (+argument).toFixed(precision).toLowerCase() # Dot shouldn't be added if argument is NaN or Infinity if special and '.' not in argument and not /^-?[a-z]+$/.test argument argument += '.' argument when 'e', 'E' toExponential() when 'g', 'G' arg = Math.abs argument if +arg is 0 or 0.0001 <= Math.abs(arg) < Math.pow(10, precision) # Precision doesn't include magical dot argument = "#{argument}".substring 0, +precision + 1 if special argument.replace /[.]?$/, '.' else argument.replace /[.]$/, '' else toExponential().replace(/[.]?0+e/, 'e') when 'x', 'X' prefix = if special and +argument isnt 0 then '0x' else '' "#{prefix}#{padInteger abs(argument).toString 16}" when 'b', 'B' prefix = if special and +argument isnt 0 then '0b' else '' "#{prefix}#{padInteger abs(argument).toString 2}" when 'o', 'O' prefix = if special then '0' else '' "#{prefix}#{padInteger abs(argument).toString 8}" when 'r' if dearrayified and i is 0 argument = dearrayified argument = JSON.stringify argument if defaultPrecision argument else argument.substring 0, precision when 's' if defaultPrecision argument else argument.substring 0, precision when 'c' String.fromCharCode argument else throw new Exception "Unrecognized %type (?). Shouldn't happen." argument = "#{argument}" if type is type.toUpperCase() argument = argument.toUpperCase() if argument[0] isnt '-' if '+' in flags argument = "+#{argument}" else if ' ' in flags argument = " #{argument}" padString argument, length, alignCharacter, leftPad result.join character # If it is Node, make alias of `require('sprint')('format') if module?.exports? module.exports = sprint (module?.exports ? this).sprint = sprint