sprint
Version:
Complex sprintf() implementation
230 lines (200 loc) • 6.73 kB
text/coffeescript
# 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