-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ec4f5d7
Showing
4 changed files
with
310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |