diff --git a/README.md b/README.md index 8578747..393ae96 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,16 @@ When `format` is invoked on a string, placeholders within the string are replaced with values determined by the arguments provided. A placeholder is a sequence of characters beginning with `{` and ending with `}`. +## About this Fork +dchambers's original version implemented nested variable interpolation, and +included support for _transformations_ that would provide functionality +similar to the _conversions_ in python's str.format() + +It did not implement **printf()**-style number formatting, so here I'm attempting to do that. +At the moment, only signs, integer precision, and field padding are implemented. + +## Usage + ### string.format(value1, value2, ..., valueN) Placeholders may contain numbers which refer to positional arguments: diff --git a/lib/string-format.js b/lib/string-format.js index 457694d..a42877c 100644 --- a/lib/string-format.js +++ b/lib/string-format.js @@ -1,7 +1,20 @@ // Generated by CoffeeScript 1.4.0 + +/* +Source code and build tools for this file are available at: +https://github.com/deleted/string-format + +This project attempts to implement python-style string formatting, as documented here: +http://docs.python.org/2/library/string.html#format-string-syntax + +The format spec part is not complete, but it can handle field padding, float precision, and such +*/ + + (function() { - var format, lookup, resolve, - __slice = [].slice; + var applyFormat, format, lookup, resolve, + __slice = [].slice, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; format = String.prototype.format = function() { var args, explicit, idx, implicit, message, @@ -17,7 +30,7 @@ idx = 0; explicit = implicit = false; message = 'cannot switch from {} to {} numbering'.format(); - return this.replace(/([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, function(match, literal, key, transformer) { + return this.replace(/([{}])\1|[{](.*?)(?:!([^:]+?)?)?(?::(.+?))?[}]/g, function(match, literal, key, transformer, formatSpec) { var fn, value, _ref, _ref1, _ref2; if (literal) { return literal; @@ -35,7 +48,11 @@ } value = (_ref1 = args[idx++]) != null ? _ref1 : ''; } - value = value.toString(); + if (formatSpec) { + value = applyFormat(value, formatSpec); + } else { + value = "" + value; + } if (fn = format.transformers[transformer]) { return (_ref2 = fn.call(value)) != null ? _ref2 : ''; } else { @@ -66,7 +83,86 @@ } }; - format.transformers = {}; + applyFormat = function(value, formatSpec) { + var align, comma, fill, hash, isNumeric, pattern, precision, sign, type, width, zeropad, _ref, _ref1; + pattern = /([^{}](?=[<>=^]))?([<>=^])?([-+\x20])?(\#)?(0)?(\d+)?(,)?(?:\.(\d+))?([bcdeEfFgGnosxX%])?/; + _ref = formatSpec.match(pattern).slice(1), fill = _ref[0], align = _ref[1], sign = _ref[2], hash = _ref[3], zeropad = _ref[4], width = _ref[5], comma = _ref[6], precision = _ref[7], type = _ref[8]; + if (zeropad) { + fill || (fill = '0'); + align || (align = '='); + } + align || (align = '>'); + fill || (fill = ' '); + switch (type) { + case 'b': + case 'c': + case 'd': + case 'o': + case 'x': + case 'X': + case 'n': + isNumeric = true; + value = '' + parseInt(value, 10); + break; + case 'e': + case 'E': + case 'f': + case 'F': + case 'g': + case 'G': + case 'n': + case '%': + isNumeric = true; + value = parseFloat(value); + if (precision) { + value = value.toFixed(parseInt(precision)); + } else { + value = "" + value; + } + break; + case 's': + isNumeric = false; + value = "" + value; + } + if (isNumeric && sign) { + if (sign === "+" || sign === " ") { + if (value[0] !== '-') { + value = sign + value; + } + } + } + /* + if isNumeric and value.charAt(0) in "+-" + memoSign = value.charAt 0 + value = value.substr 1 + */ + + if (fill) { + value = '' + value; + while (value.length < parseInt(width)) { + switch (align) { + case '=': + if (_ref1 = value.charAt(0), __indexOf.call("+- ", _ref1) >= 0) { + value = value.charAt(0) + fill + value.slice(1); + } else { + value = fill + value; + } + break; + case '<': + value = value + fill; + break; + case '>': + value = fill + value; + break; + case '^': + throw new Error("Not implemented"); + } + } + } + return value; + }; + + format.transformers || (format.transformers = {}); format.version = '0.2.1'; diff --git a/src/string-format.coffee b/src/string-format.coffee index 042a1d7..4745ea1 100644 --- a/src/string-format.coffee +++ b/src/string-format.coffee @@ -1,3 +1,13 @@ +# vim: ts=2:sw=2:expandtab +### +Source code and build tools for this file are available at: +https://github.com/deleted/string-format + +This project attempts to implement python-style string formatting, as documented here: +http://docs.python.org/2/library/string.html#format-string-syntax + +The format spec part is not complete, but it can handle field padding, float precision, and such +### format = String::format = (args...) -> if args.length is 0 @@ -8,8 +18,8 @@ format = String::format = (args...) -> message = 'cannot switch from {} to {} numbering'.format() @replace \ - /([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, - (match, literal, key, transformer) -> + /([{}])\1|[{](.*?)(?:!([^:]+?)?)?(?::(.+?))?[}]/g, + (match, literal, key, transformer, formatSpec) -> return literal if literal if key.length @@ -21,7 +31,10 @@ format = String::format = (args...) -> throw new Error message 'explicit', 'implicit' if explicit value = args[idx++] ? '' - value = value.toString() + if formatSpec + value = applyFormat value, formatSpec + else + value = "#{value}" if fn = format.transformers[transformer] then fn.call(value) ? '' else value @@ -37,6 +50,74 @@ resolve = (object, key) -> value = object[key] if typeof value is 'function' then value.call object else value -format.transformers = {} +# An implementation of http://docs.python.org/2/library/string.html#format-specification-mini-language +applyFormat = (value, formatSpec) -> + pattern = /// + ([^{}](?=[<>=^]))?([<>=^])? # fill & align + ([-+\x20])? # sign + (\#)? # integer base specifier + (0)? # zero-padding + (\d+)? # width + (,)? # use a comma thousands-seperator + (?:\.(\d+))? # precision + ([bcdeEfFgGnosxX%])? # type + /// + [fill, align, sign, hash, zeropad, width, comma, precision, type] = formatSpec.match(pattern)[1..] + if zeropad + fill or= '0' + align or= '=' + align or= '>' + fill or= ' ' + + switch type + when 'b', 'c', 'd', 'o', 'x', 'X', 'n' # integer + isNumeric = yes + value = '' + parseInt(value, 10) + when 'e','E','f','F','g','G','n','%' # float + isNumeric = true + value = parseFloat(value) + if precision + value = value.toFixed(parseInt(precision)) + else + value = "#{value}" + when 's' #string + isNumeric = false + value = "#{value}" + + if isNumeric and sign + if sign in ["+"," "] + if value[0] != '-' + value = sign + value + + ### + if isNumeric and value.charAt(0) in "+-" + memoSign = value.charAt 0 + value = value.substr 1 + ### + + if fill + value = ''+value + while value.length < parseInt(width) + switch align + when '=' + # Forces the padding to be placed after the sign (if any) but before the digits. + if value.charAt(0) in "+- " + value = value.charAt(0) + fill + value[1..] + else + value = fill + value + when '<' + # Forces the field to be left-aligned within the available space (this is the default for most objects). + value = value + fill + when '>' + # Forces the field to be right-aligned within the available space (this is the default for numbers). + value = fill + value + when '^' + throw new Error("Not implemented") + + #value = if memoSign then "#{memoSign}#{value}" else value + + return value + +format.transformers or= {} format.version = '0.2.1' diff --git a/test/string-format.coffee b/test/string-format.coffee index 525fc7b..92e177a 100644 --- a/test/string-format.coffee +++ b/test/string-format.coffee @@ -101,3 +101,21 @@ describe 'String::format', -> '{{{{0}}}}'.format(null).should.equal '{{0}}' '}}{{'.format(null).should.equal '}{' '}}x{{'.format(null).should.equal '}x{' + + it "correctly pads integer values", -> + '{:4d}'.format(1).should.equal ' 1' + '{:04d}'.format(1).should.equal '0001' + '{:04d}'.format(-1).should.equal '-001' + '{:+04d}'.format(1).should.equal '+001' + '{: 04d}'.format(1).should.equal ' 001' + '{:x>04d}'.format(1).should.equal 'xxx1' + '{:x<04d}'.format(1).should.equal '1xxx' + + it "correctly formats floats", -> + '{:0}'.format(1.2345).should.equal '1.2345' + '{:.2f}'.format(1.2345).should.equal '1.23' + '{:.1f}'.format(-1.2345).should.equal '-1.2' + '{:+.2f}'.format(1.23456).should.equal '+1.23' + '{:06.2f}'.format(1.2345).should.equal '001.23' + '{:06.2f}'.format(-1.2345).should.equal '-01.23' + '{:+07.3f}'.format(1.2345).should.equal '+01.234'