Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added some number formatting capability #1

Closed
wants to merge 13 commits into from
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
104 changes: 99 additions & 5 deletions lib/string-format.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 79 additions & 4 deletions src/string-format.coffee
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -37,6 +50,68 @@ 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 = '0'
align = '='
align 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.split('.')[0].length < parseInt(width)
switch align
when '='
if value[0] in "+- "
value = value[0] + fill + value[1..]
else
value = fill + value
when '<'
value = value + fill
when '>'
value = fill + value
when '^'
throw new Error("Not implemented")

value = if memoSign then "#{memoSign}#{value}" else value

return value
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if memoSign then "#{memoSign}#{value}" else value


format.transformers or= {}

format.version = '0.2.1'
6 changes: 6 additions & 0 deletions test/string-format.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,9 @@ describe 'String::format', ->
'{{{{0}}}}'.format(null).should.equal '{{0}}'
'}}{{'.format(null).should.equal '}{'
'}}x{{'.format(null).should.equal '}x{'

it "correctly formats floats", ->
'{:0}'.format(1.2345).should.equal '1.2345'
'{:03.2f}'.format(1.2345).should.equal '001.23'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python (2.7.2) behaves differently:

>>> '{:03.2f}'.format(1.2345)
'1.23'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. It helps to actually test against the python behavior. Fixed and added some additional tests.

'{:03.2f}'.format(-1.2345).should.equal '-001.23'
'{:+02.3f}'.format(1.2345).should.equal '+01.234'