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
dchamber's original version implemented nested variable interpolation, and
Copy link
Owner

Choose a reason for hiding this comment

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

Should be davidchambers's (according to the Chicago Manual of Style). :D

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
108 changes: 103 additions & 5 deletions lib/string-format.js

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

85 changes: 81 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)
Copy link
Owner

Choose a reason for hiding this comment

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

There are a few stray spaces in this line. Also, the parens should be dropped.

else
value = value.toString()
Copy link
Owner

Choose a reason for hiding this comment

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

The CoffeeScript idiom for this is…

value = "#{value}"

if fn = format.transformers[transformer] then fn.call(value) ? ''
else value

Expand All @@ -37,6 +50,70 @@ 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
Copy link
Owner

Choose a reason for hiding this comment

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

+ has no special meaning inside a character class, so needn't be escaped. - needn't be escaped if it is placed immediately after the opening [. The character class could therefore be written [-+\x20].

(\#)? # 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 = "="
Copy link
Owner

Choose a reason for hiding this comment

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

Single quotes, please. :)

if ! align
align = '>'
Copy link
Owner

Choose a reason for hiding this comment

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

align or= '>'


switch type
when 'b', 'c', 'd', 'o', 'x', 'X', 'n' # integer
isNumeric = true
Copy link
Owner

Choose a reason for hiding this comment

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

I think yes would read better here (it's an alias for true).

value = '' + parseInt(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 = "#{parseInt value}"

Are we intentionally omitting the radix?

Copy link
Author

Choose a reason for hiding this comment

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

I just haven't implemented it because I didn't need it. I thought it better to display a decimal int than error out.

Copy link
Owner

Choose a reason for hiding this comment

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

According to MDN, parseInt('08') evaluates as 0 in many ECMAScript 3 implementations.

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 && sign
Copy link
Owner

Choose a reason for hiding this comment

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

Use and rather than &&.

if sign in ["+"," "]
if value[0] != '-'
value = sign + value

if isNumeric && value[0] in "+-"
memoSign = value[0]
value = value[1..]
Copy link
Owner

Choose a reason for hiding this comment

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

For maximum compatibility, this should read something like…

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")

if memoSign
value = memoSign + 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 = format.transformers || {}
Copy link
Owner

Choose a reason for hiding this comment

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

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'