Commits

David Chambers committed ae90644

initial commit

Comments (0)

Files changed (4)

+# 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:
+
+    "{0}, you have {1} unread message{2}".format("Holly", 2, "s")
+    # "Holly, you have 2 unread messages"
+
+Unmatched placeholders produce no output:
+
+    "{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:
+
+    "{0} x {0} x {0} = {1}".format(3, 3*3*3)
+    # "3 x 3 x 3 = 27"
+
+Positional arguments may be referenced implicitly:
+
+    "{}, you have {} unread message{}".format("Steve", 1)
+    # "Steve, you have 1 unread message"
+
+A format string must not contain both implicit and explicit references:
+
+    "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 `}`:
+
+    "{{}} creates an empty {} in {}".format("dictionary", "Python")
+    # "{} creates an empty dictionary in Python"
+
+Dot notation may be used to reference object properties:
+
+    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:
+
+    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`:
+
+    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:
+
+    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:
+
+    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:
+
+    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:
+
+    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

string-format.coffee

+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 = {}
+(function() {
+  var format, lookup,
+    __slice = Array.prototype.slice;
+
+  format = String.prototype.format = function() {
+    var args, error, explicit, idx, implicit,
+      _this = this;
+    args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+    if (args.length === 0) {
+      return function() {
+        var args;
+        args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+        return _this.format.apply(_this, args);
+      };
+    }
+    idx = 0;
+    explicit = implicit = false;
+    error = 'cannot switch from {} to {} numbering'.format();
+    return this.replace(/([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, function(match, literal, key, transformer) {
+      var fn, value, _ref, _ref2, _ref3;
+      if (literal) return literal;
+      if (key.length) {
+        explicit = true;
+        if (implicit) throw error('implicit', 'explicit');
+        value = (_ref = lookup(args, key)) != null ? _ref : '';
+      } else {
+        implicit = true;
+        if (explicit) throw error('explicit', 'implicit');
+        value = (_ref2 = args[idx++]) != null ? _ref2 : '';
+      }
+      value = value.toString();
+      if (fn = format.transformers[transformer]) {
+        return (_ref3 = fn.call(value)) != null ? _ref3 : '';
+      } else {
+        return value;
+      }
+    });
+  };
+
+  lookup = function(object, key) {
+    var match;
+    if (!/^(\d+)([.]|$)/.test(key)) key = '0.' + key;
+    while (match = /(.+?)[.](.+)/.exec(key)) {
+      object = object[match[1]];
+      key = match[2];
+    }
+    return object[key];
+  };
+
+  format.transformers = {};
+
+}).call(this);
+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"