Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
davidchambers committed Feb 6, 2012
0 parents commit ec4f5d7
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 0 deletions.
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# String::format

String::format is a small JavaScript utility which adds a `format` method
to strings. It's inspired by and modelled on Python's [`str.format()`][1].

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 `}`.

### string.format(value1, value2, ..., valueN)

Placeholders may contain numbers which refer to positional arguments:

```coffeescript
"{0}, you have {1} unread message{2}".format("Holly", 2, "s")
# "Holly, you have 2 unread messages"
```

Unmatched placeholders produce no output:

```coffeescript
"{0}, you have {1} unread message{2}".format("Steve", 1)
# "Steve, you have 1 unread message"
```

A format string may reference a positional argument multiple times:

```coffeescript
"{0} x {0} x {0} = {1}".format(3, 3*3*3)
# "3 x 3 x 3 = 27"
```

Positional arguments may be referenced implicitly:

```coffeescript
"{}, you have {} unread message{}".format("Steve", 1)
# "Steve, you have 1 unread message"
```

A format string must not contain both implicit and explicit references:

```coffeescript
"My name is {} {}. Do you like the name {0}?".format("Lemony", "Snicket")
# ERROR: cannot switch from implicit to explicit numbering
```

`{{` and `}}` in format strings produce `{` and `}`:

```coffeescript
"{{}} creates an empty {} in {}".format("dictionary", "Python")
# "{} creates an empty dictionary in Python"
```

Dot notation may be used to reference object properties:

```coffeescript
bobby = first_name: "Bobby", last_name: "Fischer"
garry = first_name: "Garry", last_name: "Kasparov"

"{0.first_name} {0.last_name} vs. {1.first_name} {1.last_name}".format(bobby, garry)
# "Bobby Fischer vs. Garry Kasparov"
```

When referencing the first positional argument, `0.` may be omitted:

```coffeescript
repo = owner: "pypy", slug: "pypy", followers: [...]

"{owner}/{slug} has {followers.length} followers".format(repo)
# "pypy/pypy has 516 followers"
```

### String.prototype.format.transformers

“Transformers” can be attached to `String.prototype.format.transformers`:

```coffeescript
String::format.transformers.upper = -> @toUpperCase()

"Batman's preferred onomatopoeia: {0!upper}".format("pow!")
# "Batman's preferred onomatopoeia: POW!"
```

Within a transformer, `this` is the string returned by the referenced object's
`toString` method, so transformers may be used in conjunction with non-string
objects:

```coffeescript
peter_parker =
first_name: "Peter"
last_name: "Parker"
toString: -> @first_name + " " + @last_name

"NAME: {!upper}".format(peter_parker)
# "NAME: PETER PARKER"
```

A transformer could sanitizing untrusted input:

```coffeescript
String::format.transformers.escape = ->
@replace /[&<>"'`]/g, (chr) -> "&#" + chr.charCodeAt(0) + ";"

"<p class=status>{!escape}</p>".format("I <3 EICH")
# "<p class=status>I &#60;3 EICH</p>"
```

Or pluralize nouns, perhaps:

```coffeescript
String::format.transformers.s = -> "s" unless +this is 1

"{0}, you have {1} unread message{1!s}".format("Holly", 2)
# "Holly, you have 2 unread messages"

"{0}, you have {1} unread message{1!s}".format("Steve", 1)
# "Steve, you have 1 unread message"
```

String::format does not currently define any transformers.

### string.format()

If a format string is used in multiple places, one could assign it to
a variable to avoid repetition. The idiomatic alternative is to invoke
`String::format` with no arguments, which produces a reusable function:

```coffeescript
greet = "{0}, you have {1} unread message{1!s}".format()

greet("Holly", 2)
# "Holly, you have 2 unread messages"

greet("Steve", 1)
# "Steve, you have 1 unread message"
```

### Running the test suite

$ coffee tests.coffee
16 of 16 tests passed


[1]: http://docs.python.org/library/stdtypes.html#str.format
38 changes: 38 additions & 0 deletions string-format.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
format = String::format = (args...) ->

if args.length is 0
return (args...) => @format args...

idx = 0
explicit = implicit = no
error = 'cannot switch from {} to {} numbering'.format()

@replace \
/([{}])\1|[{](.*?)(?:!(.+?))?[}]/g,
(match, literal, key, transformer) ->
return literal if literal

if key.length
explicit = yes
throw error('implicit', 'explicit') if implicit
value = lookup(args, key) ? ''
else
implicit = yes
throw error('explicit', 'implicit') if explicit
value = args[idx++] ? ''

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

lookup = (object, key) ->
unless /^(\d+)([.]|$)/.test key
key = '0.' + key
while match = /(.+?)[.](.+)/.exec key
object = object[match[1]]
key = match[2]
object[key]

format.transformers = {}

format.version = '0.1.0'
54 changes: 54 additions & 0 deletions string-format.js

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

74 changes: 74 additions & 0 deletions tests.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require './string-format'


count = passes = 0

ok = (actual, expected) ->
count += 1
passes += 1 if actual is expected

throws = (fn, expected_error) ->
count += 1
try
do fn
catch error
passes += 1 if error is expected_error


ok '{0}, you have {1} unread message{2}'.format('Holly', 2, 's')
, 'Holly, you have 2 unread messages'

ok '{0}, you have {1} unread message{2}'.format('Steve', 1)
, 'Steve, you have 1 unread message'

ok 'the meaning of life is {0} ({1} x {2} is also {0})'.format(42, 6, 7)
, 'the meaning of life is 42 (6 x 7 is also 42)'

ok '{}, you have {} unread message{}'.format('Steve', 1)
, 'Steve, you have 1 unread message'

throws (-> '{} {0}'.format 'foo', 'bar')
, 'cannot switch from implicit to explicit numbering'

throws (-> '{1} {}'.format 'foo', 'bar')
, 'cannot switch from explicit to implicit numbering'

template = '{1} {}'.format()

throws (-> template 'foo', 'bar')
, 'cannot switch from explicit to implicit numbering'

ok '{{ {}: "{}" }}'.format('foo', 'bar')
, '{ foo: "bar" }'

bobby = first_name: 'Bobby', last_name: 'Fischer'
garry = first_name: 'Garry', last_name: 'Kasparov'

ok '{0.first_name} {0.last_name} vs. {1.first_name} {1.last_name}'.format(bobby, garry)
, 'Bobby Fischer vs. Garry Kasparov'

ok '{first_name} {last_name}'.format(bobby)
, 'Bobby Fischer'

String::format.transformers.s = -> 's' unless +this is 1

ok '{0}, you have {1} unread message{1!s}'.format('Holly', 2)
, 'Holly, you have 2 unread messages'

ok '{0}, you have {1} unread message{1!s}'.format('Steve', 1)
, 'Steve, you have 1 unread message'

ok '<a href="/inbox">view message{!s}</a>'.format(2)
, '<a href="/inbox">view messages</a>'

ok '<a href="/inbox">view message{!s}</a>'.format(1)
, '<a href="/inbox">view message</a>'

ok '<a href="/inbox">view message{length!s}</a>'.format(['foo', 'bar'])
, '<a href="/inbox">view messages</a>'

ok '<a href="/inbox">view message{length!s}</a>'.format(['baz'])
, '<a href="/inbox">view message</a>'


console.log "#{passes} of #{count} tests passed"

0 comments on commit ec4f5d7

Please sign in to comment.