diff --git a/.jscsrc b/.jscsrc index f6f0a1d..af78b8b 100644 --- a/.jscsrc +++ b/.jscsrc @@ -21,7 +21,7 @@ "requireCapitalizedConstructors": true, "requireCommaBeforeLineBreak": true, "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch", "finally"], - "requireDotNotation": true, + "requireDotNotation": false, "requireLineFeedAtFileEnd": true, "requireParenthesesAroundIIFE": true, "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "||", "&&", ",", ":"], diff --git a/.jshintrc b/.jshintrc index 64558a6..ef6b669 100644 --- a/.jshintrc +++ b/.jshintrc @@ -41,7 +41,7 @@ "proto": false, "scripturl": false, "shadow": false, - "sub": false, + "sub": true, "supernew": false, "validthis": false, "couch": false, diff --git a/Makefile b/Makefile index 70eeaab..7d901c6 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,6 @@ setup: .PHONY: test test: - $(ISTANBUL) cover node_modules/.bin/_mocha -- test/index.js + $(ISTANBUL) cover node_modules/.bin/_mocha -- --timeout 60000 test/index.js $(ISTANBUL) check-coverage --branches 100 node test/readme.js diff --git a/README.md b/README.md index 42a34c6..dc672ab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # string-format -string-format is a small JavaScript library for formatting strings, based on -Python's [`str.format()`][1]. For example: +string-format is a JavaScript library for formatting strings. It implements +Python's [format string syntax][1] -- including the [format specification +mini-language][2] -- while acknowledging differences between the two languages. + +Here's a straightforward example: ```javascript '"{firstName} {lastName}" <{email}>'.format(user) @@ -15,6 +18,9 @@ The equivalent concatenation: // => '"Jane Smith" ' ``` +This remainder of this document is largely derived from then aforementioned +Python documentation. + ### Installation #### Node @@ -41,7 +47,7 @@ string-format can be used in two modes: [function mode](#function-mode) and #### Function mode ```javascript -format('Hello, {}!', 'Alice') +format('Hello, {}!', ['Alice']) // => 'Hello, Alice!' ``` @@ -66,6 +72,316 @@ format.extend(String.prototype, {}) `format(template, $0, $1, …, $N)` and `template.format($0, $1, …, $N)` can then be used interchangeably. +```javascript +"The sum of 1 and 2 is {0}".format(1 + 2) +// => "The sum of 1 and 2 is 3" +``` + +Format strings contain “replacement fields” surrounded by curly braces: +`{` and `}`. Anything that is not contained in braces is considered literal +text, which is copied unchanged to the output. To include a brace character +in literal text, use `{{` or `}}`. + +The grammar for a replacement field is as follows: + +```ebnf +replacement field = "{" , [field name] , ["!" , transformer name] , [":" , format spec] , "}" ; +field name = property name , { "." , property name } ; +property name = ? a character other than ".", "!" or ":" ? ; +transformer name = ? a character other than ":" ? ; +format spec = ? described in the next section ? ; +``` + +In less formal terms, the replacement field can start with a __field_name__ +that specifies the object whose value is to be formatted and inserted into +the output instead of the replacement field. The __field_name__ is optionally +followed by a __transformer_name__ field, which is preceded by an exclamation +point `!`, and a __format_spec__, which is preceded by a colon `:`. These +specify a non-default format for the replacement value. + +See also the [Format specification mini-language][XXX1] section. + +The __field_name__ begins with a number corresponding to a positional +argument. If a format string's field\_names begin 0, 1, 2, ... in sequence, +they can all be omitted (not just some) and the numbers 0, 1, 2, ... will be +automatically inserted in that order. The __field_name__ can also contain any +number of property expressions: a dot `.` followed by a __property_name__. + +Some simple format string examples: + +```javascript +"First, thou shalt count to {0}" // References first positional argument +"Bring me a {}" // Implicitly references the first positional argument +"From {} to {}" // Same as "From {0} to {1}" +"Weight in tons: {0.weight}" // 'weight' property of first positional arg +"My quest is {name}" // 'name' property of first positional arg +"Units destroyed: {players.0}" // '0' property of 'players' property of first positional arg +``` + +The __transformer_name__ field ... TODO + +The __format_spec__ field contains a specification of how the value should be +presented: field width, alignment, padding, decimal precision, and so on. + +A __format_spec__ field can also include nested replacement fields within it. +These nested replacement fields can contain only a field name; transformers +and format specifications are not allowed. The replacement fields within the +format\_spec are substituted before the __format_spec__ string is interpreted. +This allows the formatting of a value to be dynamically specified. + +See the [Format examples][XXX2] section for some examples. + +### Format specification mini-language + +“Format specifications” are used within replacement fields contained within a +format string to define how individual values are presented. They can also be +passed to the `format()` function this module exports when “required”. + +Some of the formatting options are only supported on numbers. + +A general convention is that an empty format string (`""`) produces the same +result as if you had called `.toString()` on the value. A non-empty format +string typically modifies the result. + +The general form of a _standard format specifier_ is: + +```ebnf +format spec = [[fill] , align] , [sign] , ["#"] , ["0"] , [width] , [","] , ["." , precision] , [type] ; +fill = ? a character other than "{" or "}" ? ; +align = "<" | ">" | "=" | "^" ; +sign = "+" | "-" | " " ; +width = digit , { digit } ; +precision = digit , { digit } ; +type = "b" | "c" | "d" | "e" | "E" | "f" | "g" | "G" | "o" | "s" | "x" | "X" | "%" ; +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; +``` + +If a valid __align__ value is specified, it can be preceded by a __fill__ +character that can be any character and defaults to a space if omitted. + +The meaning of the various alignment options is as follows: + + Option Meaning + -------------------------------------------------------------------- + '<' Forces the field to be left-aligned within the available + space (this is the default for most types). + -------------------------------------------------------------------- + '>' Forces the field to be right-aligned within the available + space (this is the default for numbers). + -------------------------------------------------------------------- + '=' Forces the padding to be placed after the sign (if any) but + before the digits. This is useful for printing fields in the + form ‘+000000120’. This alignment option is only valid for + numbers. + -------------------------------------------------------------------- + '^' Forces the field to be centered within the available space. + +Note that unless a minimum field width is defined, the field width will always +be the same size as the data to fill it, so that the alignment option has no +meaning in this case. + +The __sign__ option is only valid for numbers, and can be one of the +following: + + Option Meaning + -------------------------------------------------------------------- + '+' Indicates that a sign should be used for both positive as + well as negative numbers. + -------------------------------------------------------------------- + '-' Indicates that a sign should be used only for negative + numbers (this is the default behavior). + -------------------------------------------------------------------- + ' ' Indicates that a leading space should be used on positive + numbers, and a minus sign on negative numbers. + +The `'#'` option causes the “alternate form” to be used for the conversion. +The alternate form is defined differently for different types. This option is +only valid for numbers. For integer values, when binary, octal, or +hexadecimal output is used, this option adds the prefix respective `'0b'`, +`'0o'`, or `'0x'` to the output value. For non-integer values, the alternate +form causes the result of the conversion to always contain a decimal-point +character, even if no digits follow it. Normally, a decimal-point character +appears in the result of these conversions only if a digit follows it. In +addition, for `'g'` and `'G'` conversions, trailing zeros are not removed from +the result. + +The `','` option signals the use of a comma for a thousands separator. + +__width__ is a decimal integer defining the minimum field width. If not +specified, then the field width will be determined by the content. + +Preceding the __width__ field by a zero (`'0'`) character enables sign-aware +zero-padding for numbers. This is equivalent to a __fill__ character of `'0'` +with an __alignment__ type of `'='`. + +The __precision__ is a decimal number indicating how many digits should be +displayed after the decimal point for a number formatted with `'f'`, or before +and after the decimal point for a number formatted with `'g'` or `'G'`. For +strings the field indicates the maximum field size -- in other words, how many +characters will be used from the field content. + +Finally, the __type__ determines how the data should be presented. + +The available string presentation types are: + + Type Meaning + -------------------------------------------------------------------- + 's' String format. This is the default type for strings and may + be omitted. + -------------------------------------------------------------------- + None The same as 's'. + +The available integer presentation types are: + + Type Meaning + -------------------------------------------------------------------- + 'c' Character. Converts the integer to the corresponding unicode + character before printing. + -------------------------------------------------------------------- + 'd' Decimal integer. Outputs the number in base 10. + -------------------------------------------------------------------- + 'b' Binary format. Outputs the number in base 2. + -------------------------------------------------------------------- + 'o' Octal format. Outputs the number in base 8. + -------------------------------------------------------------------- + 'x' Hex format. Outputs the number in base 16, using lower-case + letters for the digits above 9. + -------------------------------------------------------------------- + 'X' Hex format. Outputs the number in base 16, using upper-case + letters for the digits above 9. + +In addition to the above presentation types, non-integer numbers can be +formatted with the floating point presentation types listed below. + +The available presentation types for floating point and decimal values are: + + Type Meaning + -------------------------------------------------------------------- + 'e' Exponential notation. Prints the number in scientific + notation using the letter ‘e’ to indicate the exponent. + The default precision is 6. + -------------------------------------------------------------------- + 'E' Exponential notation. Same as 'e' except it uses an + upper-case ‘E’ as the separator character. + -------------------------------------------------------------------- + 'f' Fixed point. Displays the number as a fixed-point number. + The default precision is 6. + -------------------------------------------------------------------- + 'g' General format. For a given precision p >= 1, this rounds + the number of significant digits and then formats the result + in either fixed-point format or in scientific notation, + depending on its magnitude. + + The precise rules are as follows: suppose that the result + formatted with presentation type 'e' and precision p-1 would + have exponent exp. Then if -4 <= exp < p, the number is + formatted with precision type 'f' and precision p-1-exp. + Otherwise, the number is formatted with presentation type + 'e' and precision p-1. In both cases insignificant trailing + zeros are removed from the significand, and the decimal + point is also removed if there are no remaining digits + following it. + + A precision of 0 is treated as equivalent to a precision + of 1. The default precision is 6. + + This is the default type for numbers and may be omitted. + -------------------------------------------------------------------- + 'G' Same as 'g' except switches to 'E' if the number gets too + large. + -------------------------------------------------------------------- + None If precision is specified, same as 'g'. Otherwise, same as + .toString(), except the other format modifiers can be used. + +### Format examples + +Accessing arguments by position: + +```javascript +"{0}, {1}, {2}".format("a", "b", "c") +// => "a, b, c" +"{}, {}, {}".format("a", "b", "c") // arguments' indices can be omitted +// => "a, b, c" +"{2}, {1}, {0}".format("a", "b", "c") +// => "c, b, a" +"{0}{1}{0}".format("abra", "cad") // arguments' indices can be repeated +// => "abracadabra" +``` + +TODO: Discuss accessing arguments by name (via implicit "0.") + +Accessing arguments' items: + +```javascript +"X: {0.0}; Y: {0.1}".format([3, 5]) +// => "X: 3; Y: 5" +``` + +Aligning the text and specifying a width: + +```javascript +"{:<30}".format("left aligned") +// => "left aligned " +"{:>30}".format("right aligned") +// => " right aligned" +"{:^30}".format("centered") +// => " centered " +"{:*^30}".format("centered") // use "*" as a fill char +// => "***********centered***********" +``` + +Specifying a sign: + +```javascript +"{:+f}; {:+f}".format(3.14, -3.14) // show it always +// => "+3.140000; -3.140000" +"{: f}; {: f}".format(3.14, -3.14) // show a space for positive numbers +// => " 3.140000; -3.140000" +"{:-f}; {:-f}".format(3.14, -3.14) // show only the minus -- same as "{:f}; {:f}" +// => "3.140000; -3.140000" +``` + +Converting the value to different bases: + +```javascript +"int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}".format(42) +// => "int: 42; hex: 2a; oct: 52; bin: 101010" +"int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) +// => "int: 42; hex: 0x2a; oct: 0o52; bin: 0b101010" +``` + +Using the comma as thousands separator: + +```javascript +"{:,}".format(1234567890) +// => "1,234,567,890" +``` + +Expressing a percentage: + +```javascript +"Correct answers: {:.2%}".format(19 / 22) +// => "Correct answers: 86.36%" +``` + +--- + +### string.format(values...) + +A KeyError is thrown if there are unmatched placeholders: + +```javascript +'{0} {1} {2}'.format('x', 'y') +// => KeyError: "2" +``` + +A format string must not contain both implicit and explicit references: + +```javascript +'My name is {} {}. Do you like the name {0}?'.format('Lemony', 'Snicket') +// => ValueError: cannot switch from implicit to explicit numbering +``` + ### `format(template, $0, $1, …, $N)` Returns the result of replacing each `{…}` placeholder in the template string @@ -78,41 +394,318 @@ Placeholders may contain numbers which refer to positional arguments: // => 'Holly, you have 2 unread messages' ``` -Unmatched placeholders produce no output: +Format strings contain “replacement fields” surrounded by curly braces: +`{` and `}`. Anything that is not contained in braces is considered literal +text, which is copied unchanged to the output. To include a brace character +in literal text, use `{{` or `}}`. + +The grammar for a replacement field is as follows: + +```ebnf +replacement field = "{" , [field name] , ["!" , transformer name] , [":" , format spec] , "}" ; +field name = property name , { "." , property name } ; +property name = ? a character other than ".", "!" or ":" ? ; +transformer name = ? a character other than ":" ? ; +format spec = ? described in the next section ? ; +``` + +In less formal terms, the replacement field can start with a __field_name__ +that specifies the object whose value is to be formatted and inserted into +the output instead of the replacement field. The __field_name__ is optionally +followed by a __transformer_name__ field, which is preceded by an exclamation +point `!`, and a __format_spec__, which is preceded by a colon `:`. These +specify a non-default format for the replacement value. + +See also the [Format specification mini-language][XXX1] section. + +The __field_name__ begins with a number corresponding to a positional +argument. If a format string's field\_names begin 0, 1, 2, ... in sequence, +they can all be omitted (not just some) and the numbers 0, 1, 2, ... will be +automatically inserted in that order. The __field_name__ can also contain any +number of property expressions: a dot `.` followed by a __property_name__. + +Some simple format string examples: ```javascript -'{0}, you have {1} unread message{2}'.format('Steve', 1) -// => 'Steve, you have 1 unread message' +"First, thou shalt count to {0}" // References first positional argument +"Bring me a {}" // Implicitly references the first positional argument +"From {} to {}" // Same as "From {0} to {1}" +"Weight in tons: {0.weight}" // 'weight' property of first positional arg +"My quest is {name}" // 'name' property of first positional arg +"Units destroyed: {players.0}" // '0' property of 'players' property of first positional arg ``` -A format string may reference a positional argument multiple times: +The __transformer_name__ field ... TODO + +The __format_spec__ field contains a specification of how the value should be +presented: field width, alignment, padding, decimal precision, and so on. + +A __format_spec__ field can also include nested replacement fields within it. +These nested replacement fields can contain only a field name; transformers +and format specifications are not allowed. The replacement fields within the +format\_spec are substituted before the __format_spec__ string is interpreted. +This allows the formatting of a value to be dynamically specified. + +See the [Format examples][XXX2] section for some examples. + +### Format specification mini-language + +“Format specifications” are used within replacement fields contained within a +format string to define how individual values are presented. They can also be +passed to the `format()` function this module exports when “required”. + +Some of the formatting options are only supported on numbers. + +A general convention is that an empty format string (`""`) produces the same +result as if you had called `.toString()` on the value. A non-empty format +string typically modifies the result. + +The general form of a _standard format specifier_ is: + +```ebnf +format spec = [[fill] , align] , [sign] , ["#"] , ["0"] , [width] , [","] , ["." , precision] , [type] ; +fill = ? a character other than "{" or "}" ? ; +align = "<" | ">" | "=" | "^" ; +sign = "+" | "-" | " " ; +width = digit , { digit } ; +precision = digit , { digit } ; +type = "b" | "c" | "d" | "e" | "E" | "f" | "g" | "G" | "o" | "s" | "x" | "X" | "%" ; +digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; +``` + +If a valid __align__ value is specified, it can be preceded by a __fill__ +character that can be any character and defaults to a space if omitted. + +The meaning of the various alignment options is as follows: + + Option Meaning + -------------------------------------------------------------------- + '<' Forces the field to be left-aligned within the available + space (this is the default for most types). + -------------------------------------------------------------------- + '>' Forces the field to be right-aligned within the available + space (this is the default for numbers). + -------------------------------------------------------------------- + '=' Forces the padding to be placed after the sign (if any) but + before the digits. This is useful for printing fields in the + form ‘+000000120’. This alignment option is only valid for + numbers. + -------------------------------------------------------------------- + '^' Forces the field to be centered within the available space. + +Note that unless a minimum field width is defined, the field width will always +be the same size as the data to fill it, so that the alignment option has no +meaning in this case. + +The __sign__ option is only valid for numbers, and can be one of the +following: + + Option Meaning + -------------------------------------------------------------------- + '+' Indicates that a sign should be used for both positive as + well as negative numbers. + -------------------------------------------------------------------- + '-' Indicates that a sign should be used only for negative + numbers (this is the default behavior). + -------------------------------------------------------------------- + ' ' Indicates that a leading space should be used on positive + numbers, and a minus sign on negative numbers. + +The `'#'` option causes the “alternate form” to be used for the conversion. +The alternate form is defined differently for different types. This option is +only valid for numbers. For integer values, when binary, octal, or +hexadecimal output is used, this option adds the prefix respective `'0b'`, +`'0o'`, or `'0x'` to the output value. For non-integer values, the alternate +form causes the result of the conversion to always contain a decimal-point +character, even if no digits follow it. Normally, a decimal-point character +appears in the result of these conversions only if a digit follows it. In +addition, for `'g'` and `'G'` conversions, trailing zeros are not removed from +the result. + +The `','` option signals the use of a comma for a thousands separator. + +__width__ is a decimal integer defining the minimum field width. If not +specified, then the field width will be determined by the content. + +Preceding the __width__ field by a zero (`'0'`) character enables sign-aware +zero-padding for numbers. This is equivalent to a __fill__ character of `'0'` +with an __alignment__ type of `'='`. + +The __precision__ is a decimal number indicating how many digits should be +displayed after the decimal point for a number formatted with `'f'`, or before +and after the decimal point for a number formatted with `'g'` or `'G'`. For +strings the field indicates the maximum field size -- in other words, how many +characters will be used from the field content. + +Finally, the __type__ determines how the data should be presented. + +The available string presentation types are: + + Type Meaning + -------------------------------------------------------------------- + 's' String format. This is the default type for strings and may + be omitted. + -------------------------------------------------------------------- + None The same as 's'. + +The available integer presentation types are: + + Type Meaning + -------------------------------------------------------------------- + 'c' Character. Converts the integer to the corresponding unicode + character before printing. + -------------------------------------------------------------------- + 'd' Decimal integer. Outputs the number in base 10. + -------------------------------------------------------------------- + 'b' Binary format. Outputs the number in base 2. + -------------------------------------------------------------------- + 'o' Octal format. Outputs the number in base 8. + -------------------------------------------------------------------- + 'x' Hex format. Outputs the number in base 16, using lower-case + letters for the digits above 9. + -------------------------------------------------------------------- + 'X' Hex format. Outputs the number in base 16, using upper-case + letters for the digits above 9. + +In addition to the above presentation types, non-integer numbers can be +formatted with the floating point presentation types listed below. + +The available presentation types for floating point and decimal values are: + + Type Meaning + -------------------------------------------------------------------- + 'e' Exponential notation. Prints the number in scientific + notation using the letter ‘e’ to indicate the exponent. + The default precision is 6. + -------------------------------------------------------------------- + 'E' Exponential notation. Same as 'e' except it uses an + upper-case ‘E’ as the separator character. + -------------------------------------------------------------------- + 'f' Fixed point. Displays the number as a fixed-point number. + The default precision is 6. + -------------------------------------------------------------------- + 'g' General format. For a given precision p >= 1, this rounds + the number of significant digits and then formats the result + in either fixed-point format or in scientific notation, + depending on its magnitude. + + The precise rules are as follows: suppose that the result + formatted with presentation type 'e' and precision p-1 would + have exponent exp. Then if -4 <= exp < p, the number is + formatted with precision type 'f' and precision p-1-exp. + Otherwise, the number is formatted with presentation type + 'e' and precision p-1. In both cases insignificant trailing + zeros are removed from the significand, and the decimal + point is also removed if there are no remaining digits + following it. + + A precision of 0 is treated as equivalent to a precision + of 1. The default precision is 6. + + This is the default type for numbers and may be omitted. + -------------------------------------------------------------------- + 'G' Same as 'g' except switches to 'E' if the number gets too + large. + -------------------------------------------------------------------- + None If precision is specified, same as 'g'. Otherwise, same as + .toString(), except the other format modifiers can be used. + +### Format examples + +Accessing arguments by position: + +```javascript +"{0}, {1}, {2}".format("a", "b", "c") +// => "a, b, c" +"{}, {}, {}".format("a", "b", "c") // arguments' indices can be omitted +// => "a, b, c" +"{2}, {1}, {0}".format("a", "b", "c") +// => "c, b, a" +"{0}{1}{0}".format("abra", "cad") // arguments' indices can be repeated +// => "abracadabra" +``` ```javascript "The name's {1}. {0} {1}.".format('James', 'Bond') // => "The name's Bond. James Bond." ``` -Positional arguments may be referenced implicitly: +TODO: Discuss accessing arguments by name (via implicit "0.") + +Accessing arguments' items: ```javascript -'{}, you have {} unread message{}'.format('Steve', 1) -// => 'Steve, you have 1 unread message' +"X: {0.0}; Y: {0.1}".format([3, 5]) +// => "X: 3; Y: 5" ``` -A format string must not contain both implicit and explicit references: +Aligning the text and specifying a width: ```javascript -'My name is {} {}. Do you like the name {0}?'.format('Lemony', 'Snicket') -// => ValueError: cannot switch from implicit to explicit numbering +"{:<30}".format("left aligned") +// => "left aligned " +"{:>30}".format("right aligned") +// => " right aligned" +"{:^30}".format("centered") +// => " centered " +"{:*^30}".format("centered") // use "*" as a fill char +// => "***********centered***********" +``` + +Specifying a sign: + +```javascript +"{:+f}; {:+f}".format(3.14, -3.14) // show it always +// => "+3.140000; -3.140000" +"{: f}; {: f}".format(3.14, -3.14) // show a space for positive numbers +// => " 3.140000; -3.140000" +"{:-f}; {:-f}".format(3.14, -3.14) // show only the minus -- same as "{:f}; {:f}" +// => "3.140000; -3.140000" +``` + +Converting the value to different bases: + +```javascript +"int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}".format(42) +// => "int: 42; hex: 2a; oct: 52; bin: 101010" +"int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) +// => "int: 42; hex: 0x2a; oct: 0o52; bin: 0b101010" +``` + +Using the comma as thousands separator: + +```javascript +"{:,}".format(1234567890) +// => "1,234,567,890" +``` + +Expressing a percentage: + +```javascript +"Correct answers: {:.2%}".format(19 / 22) +// => "Correct answers: 86.36%" ``` -`{{` and `}}` in format strings produce `{` and `}`: +--- + +### string.format(values...) + +A KeyError is thrown if there are unmatched placeholders: ```javascript -'{{}} creates an empty {} in {}'.format('dictionary', 'Python') -// => '{} creates an empty dictionary in Python' +'{0} {1} {2}'.format('x', 'y') +// => KeyError: "2" ``` +A format string must not contain both implicit and explicit references: + +```javascript +'My name is {} {}. Do you like the name {0}?'.format('Lemony', 'Snicket') +// => ValueError: cannot switch from implicit to explicit numbering +``` + +### format(template, values...) + Dot notation may be used to reference object properties: ```javascript @@ -128,7 +721,7 @@ var garry = {firstName: 'Garry', lastName: 'Kasparov'} ```javascript var repo = {owner: 'davidchambers', slug: 'string-format'} -'https://github.com/{owner}/{slug}'.format(repo) +'https://github.com/{owner}/{slug}'.format({owner: 'davidchambers', slug: 'string-format'}) // => 'https://github.com/davidchambers/string-format' ``` @@ -188,5 +781,8 @@ $ npm test ``` -[1]: http://docs.python.org/library/stdtypes.html#str.format -[2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind +[XXX1]: #format-specification-mini-language +[XXX2]: #format-examples +[1]: http://docs.python.org/3/library/string.html#formatstrings +[2]: http://docs.python.org/3/library/string.html#formatspec +[3]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind diff --git a/index.js b/index.js index 5bbc968..0cf096a 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,16 @@ -/* global define, module */ +/* global module, require */ ;(function(global) { 'use strict'; + // KeyError :: String -> Error + var KeyError = function(message) { + var err = new Error(message); + err.name = 'KeyError'; + return err; + }; + // ValueError :: String -> Error var ValueError = function(message) { var err = new Error(message); @@ -11,86 +18,429 @@ return err; }; - // defaultTo :: a,a? -> a - var defaultTo = function(x, y) { - return y == null ? x : y; + // toBig :: Number -> BigNumber + var toBig = function(n) { + return new BigNumber(String(n)); }; - // create :: Object -> String,*... -> String - var create = function(transformers) { - return function(template) { - var args = Array.prototype.slice.call(arguments, 1); - var idx = 0; - var state = 'UNDEFINED'; - - return template.replace( - /([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, - function(match, literal, key, xf) { - if (literal != null) { - return literal; - } - if (key.length > 0) { - if (state === 'IMPLICIT') { - throw ValueError('cannot switch from ' + - 'implicit to explicit numbering'); - } - state = 'EXPLICIT'; - } else { - if (state === 'EXPLICIT') { - throw ValueError('cannot switch from ' + - 'explicit to implicit numbering'); - } - state = 'IMPLICIT'; - key = String(idx); - idx += 1; - } - var value = defaultTo('', lookup(args, key.split('.'))); - - if (xf == null) { - return value; - } else if (Object.prototype.hasOwnProperty.call(transformers, xf)) { - return transformers[xf](value); - } else { - throw ValueError('no transformer named "' + xf + '"'); + var pad = function(string, fill, align, width) { + var padding = Array(Math.max(0, width - string.length + 1)).join(fill); + switch (align) { + case '<': + return string + padding; + case '>': + return padding + string; + case '^': + var idx = Math.floor(padding.length / 2); + return padding.slice(0, idx) + string + padding.slice(idx); + } + }; + + var toString = Object.prototype.toString; + + var tokenize_format_spec = function(spec) { + var tokens = { + 'fill': '', + 'align': '', + 'sign': '', + '#': false, + '0': false, + 'width': '', + ',': false, + 'precision': '', + 'type': '' + }; + + var idx = 0; + + // The presence of a fill character is signalled by the character + // following it, which must be one of the alignment options. Unless + // the second character of spec is a valid alignment option, the fill + // character is assumed to be are absent. + var match = /^(.)?([<>=^])/.exec(spec); + if (match != null) { + if (match[1] != null) { + tokens['fill'] = match[1]; + } + tokens['align'] = match[2]; + idx += match[0].length; + } + + match = /^[ +-]/.exec(spec.slice(idx)); + if (match != null) { + tokens['sign'] = match[0]; + idx += match[0].length; + } + + if (spec.charAt(idx) === '#') { + tokens['#'] = true; + idx += 1; + } + + if (spec.charAt(idx) === '0') { + tokens['0'] = true; + idx += 1; + } + + match = /^\d*/.exec(spec.slice(idx)); + tokens['width'] = match[0]; + idx += match[0].length; + + if (spec.charAt(idx) === ',') { + tokens[','] = true; + idx += 1; + } + + if (spec.charAt(idx) === '.') { + idx += 1; + match = /^\d+/.exec(spec.slice(idx)); + if (match == null) { + throw ValueError('Format specifier missing precision'); + } + tokens['precision'] = match[0]; + idx += match[0].length; + } + + if (idx < spec.length) { + tokens['type'] = spec.charAt(idx); + idx += 1; + } + + if (idx < spec.length) { + throw ValueError('Invalid conversion specification'); + } + + if (tokens[','] && tokens['type'] === 's') { + throw ValueError("Cannot specify ',' with 's'"); + } + + return tokens; + }; + + var format_value = function(value, tokens) { + switch (toString.call(value)) { + case '[object Number]': return formatNumber(value, tokens); + case '[object String]': return formatString(value, tokens); + default: + for (var prop in tokens) { + if (tokens[prop]) { + throw ValueError('non-empty format string for ' + + toString.call(value).split(/\W/)[2] + ' object'); } } + return String(value); + } + }; + + var formatString = function(value, tokens) { + var fill = tokens['fill'] || (tokens['0'] ? '0' : ' '); + var align = tokens['align'] || (tokens['0'] ? '=' : '<'); + var precision = Number(tokens['precision'] || value.length); + + if (tokens['type'] !== '' && tokens['type'] !== 's') { + throw ValueError('unknown format code "' + tokens['type'] + '" ' + + 'for String object'); + } + if (tokens[',']) { + throw ValueError("Cannot specify ',' with 's'"); + } + if (tokens['sign']) { + throw ValueError('Sign not allowed in string format specifier'); + } + if (tokens['#']) { + throw ValueError('Alternate form (#) not allowed ' + + 'in string format specifier'); + } + if (align === '=') { + throw ValueError('"=" alignment not allowed in string format specifier'); + } + return pad(value.slice(0, precision), + fill, + align, + Number(tokens['width'])); + }; + + // _formatNumber :: BigNumber,Number,String -> String + var _formatNumber = function _formatNumber(n, precision, type) { + if (/[A-Z]/.test(type)) { + return _formatNumber(n, precision, type.toLowerCase()).toUpperCase(); + } else if (type === 'c') { + return String.fromCharCode(n); + } else if (type === 'd') { + return n.abs().toString(10); + } else if (type === 'b') { + return n.abs().toString(2); + } else if (type === 'o') { + return n.abs().toString(8); + } else if (type === 'x') { + return n.abs().toString(16); + } else if (type === 'e') { + return n.abs().toExponential(precision).replace(/e[+](?=\d$)/, '$&0'); + } else if (type === 'f') { + return n.abs().toFixed(precision); + } else if (type === 'g') { + // A precision of 0 is treated as equivalent to a precision of 1. + var p = precision || 1; + if (n.e >= -4 && n.e < p) { + var x = toBig(10).pow(p - 1 - Math.abs(n.e)); + return n.abs().times(x).round().div(x).toString(); + } else { + return _formatNumber(n, Math.max(0, p - 1), 'e'); + } + } else if (type === '%') { + return _formatNumber(n.times(100), precision, 'f') + '%'; + } else if (type === '') { + return _formatNumber(n, precision, 'd'); + } else { + throw ValueError('unknown format code "' + type + '" for Number object'); + } + }; + + var formatNumber = function(value, tokens) { + var fill = tokens['fill'] || (tokens['0'] ? '0' : ' '); + var align = tokens['align'] || (tokens['0'] ? '=' : '>'); + var width = tokens['width']; + var type = tokens['type'] || (tokens['precision'] ? 'g' : ''); + + if (type === 'c' || type === 'd' || + type === 'b' || type === 'o' || + type === 'x' || type === 'X') { + if (value % 1 < 0 || value % 1 > 0) { + throw ValueError('cannot format non-integer ' + + 'with format specifier "' + type + '"'); + } + if (tokens['sign'] !== '' && type === 'c') { + throw ValueError("Sign not allowed with integer format specifier 'c'"); + } + if (tokens[','] && type !== 'd') { + throw ValueError("Cannot specify ',' with '" + type + "'"); + } + if (tokens['precision'] !== '') { + throw ValueError('Precision not allowed in integer format specifier'); + } + } else if (type === 'e' || type === 'E' || + type === 'f' || type === 'F' || + type === 'g' || type === 'G' || + type === '%') { + if (tokens['#']) { + throw ValueError('Alternate form (#) not allowed ' + + 'in float format specifier'); + } + } + + var s = _formatNumber(toBig(value), + Number(tokens['precision'] || '6'), + type); + + var sign = value < 0 || 1 / value < 0 ? '-' : + tokens['sign'] === '-' ? '' : tokens['sign']; + + var prefix = tokens['#'] && + (type === 'b' || type === 'o' || + type === 'x' || type === 'X') ? '0' + type : ''; + + if (tokens[',']) { + var match = /^(\d*)(.*)$/.exec(s); + var whole = match[1]; + var fract = match[2]; + var separated = whole.replace(/.(?=(...)+$)/g, '$&,'); + + if (align !== '=') { + return pad(sign + separated + fract, fill, align, width); + } else if (fill === '0') { + var shortfall = + Math.max(0, width - sign.length - separated.length - fract.length); + var digits = /^\d*/.exec(separated)[0].length; + var padding = ''; + for (var n = 0; n < shortfall; n += 1) { + padding = ((digits + n) % 4 === 3 ? ',' : '0') + padding; + } + return sign + (/^,/.test(padding) ? '0' : '') + + padding + separated + fract; + } else { + return sign + pad(separated + fract, fill, '>', width - sign.length); + } + } else if (width === '') { + return sign + prefix + s; + } else if (align === '=') { + return sign + prefix + + pad(s, fill, '>', width - sign.length - prefix.length); + } else { + return pad(sign + prefix + s, fill, align, width); + } + }; + + var quote = function(s) { + return '"' + s.replace(/"/g, '\\"') + '"'; + }; + + var descend = function(x, path) { + if (path.length === 0) { + return x; + } else if (path[0] in Object(x)) { + return descend( + typeof x[path[0]] === 'function' ? x[path[0]]() : x[path[0]], + path.slice(1) ); - }; + } else { + throw KeyError(quote(path[0])); + } }; - var lookup = function(obj, path) { - if (!/^\d+$/.test(path[0])) { - path = ['0'].concat(path); + var evaluate = function(transformers, expr, ctx) { + var val = descend(ctx, expr.field_name.split('.')); + if (expr.transformer.length === 0) { + return val; } - for (var idx = 0; idx < path.length; idx += 1) { - var key = path[idx]; - obj = typeof obj[key] === 'function' ? obj[key]() : obj[key]; + var xf = expr.transformer[0]; + if (Object.prototype.hasOwnProperty.call(transformers, xf)) { + return transformers[xf](val); } - return obj; + throw ValueError('no transformer named ' + quote(xf)); + }; + + var create = function(transformers) { + return function(template, args) { + var mode = 'UNDEFINED'; + var _idx = -1; + var normalize = function(name) { + if (name === '') { + if (mode === 'EXPLICIT') { + throw ValueError('cannot switch from ' + + 'explicit to implicit numbering'); + } + mode = 'IMPLICIT'; + return String(_idx += 1); + } else { + if (mode === 'IMPLICIT') { + throw ValueError('cannot switch from ' + + 'implicit to explicit numbering'); + } + mode = 'EXPLICIT'; + return /^\d+(?:[.]|$)/.test(name) ? name : '0.' + name; + } + }; + + var state = 'literal'; + var field_name = ''; + var transformer = []; + var spec = ''; + var output = ''; + + var _evaluate = function() { + state = 'literal'; + var token = { + field_name: field_name, + transformer: transformer, + spec: spec + }; + token.field_name = normalize(token.field_name); + var value = evaluate(transformers, token, args); + var tokens = tokenize_format_spec( + token.spec.replace(/\{(.*?)\}/g, function(match, field_name) { + return descend(args, normalize(field_name).split('.')); + }) + ); + output += format_value(value, tokens); + field_name = spec = ''; + transformer = []; + }; + + for (var idx = 0; idx < template.length; idx += 1) { + var c = template.charAt(idx); + + if (state === 'literal' && c === '{') { + state = '{'; + } else if (state === 'literal' && c === '}') { + state = '}'; + } else if (state === 'literal') { + output += c; + } else if (state === '{' && c === '{') { + state = 'literal'; + output += '{'; + } else if (state === '{' && c === '!') { + state = '!'; + transformer = ['']; + } else if (state === '{' && c === ':') { + state = ':'; + } else if (state === '{' && c === '}') { + _evaluate(); + } else if (state === '{') { + state = 'field_name'; + field_name += c; + } else if (state === 'field_name' && c === '!') { + state = '!'; + transformer = ['']; + } else if (state === 'field_name' && c === ':') { + state = ':'; + } else if (state === 'field_name' && c === '}') { + _evaluate(); + } else if (state === 'field_name') { + field_name += c; + } else if (state === '!' && c === ':') { + state = c; + } else if (state === '!' && c === '}') { + if (transformer[0] === '') { + throw ValueError('end of format ' + + 'while looking for conversion specifier'); + } + _evaluate(); + } else if (state === '!') { + transformer[0] += c; + } else if (state === ':' && c === '{') { + spec += '{'; + state = ':{'; + } else if (state === ':' && c === '}') { + _evaluate(); + } else if (state === ':') { + spec += c; + } else if (state === ':{' && c === '{') { + throw ValueError('Max string recursion exceeded'); + } else if (state === ':{' && c === '}') { + spec += '}'; + state = ':'; + } else if (state === ':{') { + spec += c; + } else if (state === '}' && c === '}') { + state = 'literal'; + output += '}'; + } + } + + switch (state) { + case '{': + throw ValueError("Single '{' encountered in format string"); + case '}': + throw ValueError("Single '}' encountered in format string"); + case 'field_name': + case '!': + case ':': + throw ValueError("unmatched '{' in format"); + default: + return output; + } + }; }; // format :: String,*... -> String var format = create({}); - // format.create :: Object -> String,*... -> String + // format.create :: Object? -> String,*... -> String format.create = create; - // format.extend :: Object,Object -> () + // format.extend :: Object,Object? -> () format.extend = function(prototype, transformers) { var $format = create(transformers); - prototype.format = function() { - var args = Array.prototype.slice.call(arguments); - args.unshift(this); - return $format.apply(global, args); - }; + prototype.format = function() { return $format(this, arguments); }; }; + var BigNumber; /* istanbul ignore else */ if (typeof module !== 'undefined') { + BigNumber = require('bignumber.js'); module.exports = format; - } else if (typeof define === 'function' && define.amd) { - define(format); } else { + BigNumber = global.Big; global.format = format; } diff --git a/package.json b/package.json index 2a8755e..bff9812 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,15 @@ "index.js", "package.json" ], + "dependencies": { + "bignumber.js": "2.0.x" + }, "devDependencies": { "istanbul": "0.3.x", "jscs": "2.1.x", "jshint": "2.8.x", + "lodash.random": "3.x.x", + "lodash.sample": "3.x.x", "mocha": "2.x.x", "ramda": "0.8.x", "xyz": "0.5.x" diff --git a/test/index.js b/test/index.js index 0b737d6..2f15733 100644 --- a/test/index.js +++ b/test/index.js @@ -4,7 +4,10 @@ /* jshint maxlen: 999, node: true */ var assert = require('assert'); +var exec = require('child_process').exec; +var random = require('lodash.random'); +var sample = require('lodash.sample'); var R = require('ramda'); var format = require('..'); @@ -14,6 +17,22 @@ var eq = assert.strictEqual; var s = function(num) { return num === 1 ? '' : 's'; }; +var throws = function(s, block) { + var sep = ': '; + var idx = s.indexOf(sep); + if (idx >= 0) { + assert.throws(block, function(err) { + return err instanceof Error && + err.name === s.slice(0, idx) && + err.message === s.slice(idx + sep.length); + }); + } else { + assert.throws(block, function(err) { + return err instanceof Error && err.name === s; + }); + } +}; + describe('format', function() { @@ -24,92 +43,129 @@ describe('format', function() { }); it('interpolates positional arguments', function() { - eq(format('{0}, you have {1} unread message{2}', 'Holly', 2, 's'), + eq(format('{0}, you have {1} unread message{2}', ['Holly', 2, 's']), 'Holly, you have 2 unread messages'); }); - it('strips unmatched placeholders', function() { - eq(format('{0}, you have {1} unread message{2}', 'Steve', 1), - 'Steve, you have 1 unread message'); + it('throws a KeyError if there are unmatched placeholders', function() { + throws('KeyError: "2"', function() { format('{0} {1} {2}', ['x', 'y']); }); }); it('allows indexes to be omitted if they are entirely sequential', function() { - eq(format('{}, you have {} unread message{}', 'Steve', 1), + eq(format('{}, you have {} unread message{}', ['Steve', 1, '']), 'Steve, you have 1 unread message'); }); it('replaces all occurrences of a placeholder', function() { - eq(format('the meaning of life is {0} ({1} x {2} is also {0})', 42, 6, 7), + eq(format('the meaning of life is {0} ({1} x {2} is also {0})', [42, 6, 7]), 'the meaning of life is 42 (6 x 7 is also 42)'); }); it('does not allow explicit and implicit numbering to be intermingled', function() { - assert.throws( - function() { format('{} {0}', 'foo', 'bar'); }, - function(err) { - return err instanceof Error && - err.name === 'ValueError' && - err.message === 'cannot switch from ' + - 'implicit to explicit numbering'; - } - ); - assert.throws( - function() { format('{1} {}', 'foo', 'bar'); }, - function(err) { - return err instanceof Error && - err.name === 'ValueError' && - err.message === 'cannot switch from ' + - 'explicit to implicit numbering'; - } - ); + throws('ValueError: cannot switch from implicit to explicit numbering', + function() { format('{} {0}', ['foo', 'bar']); }); + + throws('ValueError: cannot switch from explicit to implicit numbering', + function() { format('{1} {}', ['foo', 'bar']); }); }); it('treats "{{" and "}}" as "{" and "}"', function() { - eq(format('{{ {}: "{}" }}', 'foo', 'bar'), '{ foo: "bar" }'); + eq(format('{{ {}: "{}" }}', ['foo', 'bar']), '{ foo: "bar" }'); }); it('supports property access via dot notation', function() { var bobby = {first: 'Bobby', last: 'Fischer'}; var garry = {first: 'Garry', last: 'Kasparov'}; - eq(format('{0.first} {0.last} vs. {1.first} {1.last}', bobby, garry), + eq(format('{0.first} {0.last} vs. {1.first} {1.last}', [bobby, garry]), 'Bobby Fischer vs. Garry Kasparov'); }); it('accepts a shorthand for properties of the first positional argument', function() { var bobby = {first: 'Bobby', last: 'Fischer'}; - eq(format('{first} {last}', bobby), 'Bobby Fischer'); + eq(format('{first} {last}', [bobby]), 'Bobby Fischer'); }); it('invokes methods', function() { - eq(format('{0.toLowerCase}', 'III'), 'iii'); - eq(format('{0.toUpperCase}', 'iii'), 'III'); - eq(format('{0.getFullYear}', new Date('26 Apr 1984')), '1984'); - eq(format('{pop}{pop}{pop}', ['one', 'two', 'three']), 'threetwoone'); - eq(format('{quip.toUpperCase}', {quip: R.always('Bazinga!')}), 'BAZINGA!'); + eq(format('{0.toLowerCase}', ['III']), 'iii'); + eq(format('{0.toUpperCase}', ['iii']), 'III'); + eq(format('{0.getFullYear}', [new Date('26 Apr 1984')]), '1984'); + eq(format('{pop}{pop}{pop}', [['one', 'two', 'three']]), 'threetwoone'); + eq(format('{quip.toUpperCase}', [{quip: R.always('Bazinga!')}]), 'BAZINGA!'); }); it("passes applicable tests from Python's test suite", function() { - eq(format(''), ''); - eq(format('abc'), 'abc'); - eq(format('{0}', 'abc'), 'abc'); - eq(format('X{0}', 'abc'), 'Xabc'); - eq(format('{0}X', 'abc'), 'abcX'); - eq(format('X{0}Y', 'abc'), 'XabcY'); - eq(format('{1}', 1, 'abc'), 'abc'); - eq(format('X{1}', 1, 'abc'), 'Xabc'); - eq(format('{1}X', 1, 'abc'), 'abcX'); - eq(format('X{1}Y', 1, 'abc'), 'XabcY'); - eq(format('{0}', -15), '-15'); - eq(format('{0}{1}', -15, 'abc'), '-15abc'); - eq(format('{0}X{1}', -15, 'abc'), '-15Xabc'); - eq(format('{{'), '{'); - eq(format('}}'), '}'); - eq(format('{{}}'), '{}'); - eq(format('{{x}}'), '{x}'); - eq(format('{{{0}}}', 123), '{123}'); - eq(format('{{{{0}}}}'), '{{0}}'); - eq(format('}}{{'), '}{'); - eq(format('}}x{{'), '}x{'); + eq(format('', []), ''); + eq(format('abc', []), 'abc'); + eq(format('{0}', ['abc']), 'abc'); + eq(format('X{0}', ['abc']), 'Xabc'); + eq(format('{0}X', ['abc']), 'abcX'); + eq(format('X{0}Y', ['abc']), 'XabcY'); + eq(format('{1}', [1, 'abc']), 'abc'); + eq(format('X{1}', [1, 'abc']), 'Xabc'); + eq(format('{1}X', [1, 'abc']), 'abcX'); + eq(format('X{1}Y', [1, 'abc']), 'XabcY'); + eq(format('{0}', [-15]), '-15'); + eq(format('{0}{1}', [-15, 'abc']), '-15abc'); + eq(format('{0}X{1}', [-15, 'abc']), '-15Xabc'); + eq(format('{{', []), '{'); + eq(format('}}', []), '}'); + eq(format('{{}}', []), '{}'); + eq(format('{{x}}', []), '{x}'); + eq(format('{{{0}}}', [123]), '{123}'); + eq(format('{{{{0}}}}', []), '{{0}}'); + eq(format('}}{{', []), '}{'); + eq(format('}}x{{', []), '}x{'); + + // computed format specifiers + eq(format('{0:.{1}}', ['hello world', 5]), 'hello'); + eq(format('{0:.{1}s}', ['hello world', 5]), 'hello'); + eq(format('{1:.{precision}s}', [{precision: 5}, 'hello world']), 'hello'); + eq(format('{1:{width}.{precision}s}', [{width: 10, precision: 5}, 'hello world']), 'hello '); + eq(format('{1:{width}.{precision}s}', [{width: '10', precision: '5'}, 'hello world']), 'hello '); + + // test various errors + throws('ValueError', function() { format('{', []); }); + throws('ValueError', function() { format('}', []); }); + throws('ValueError', function() { format('a{', []); }); + throws('ValueError', function() { format('a}', []); }); + throws('ValueError', function() { format('{a', []); }); + throws('ValueError', function() { format('}a', []); }); + throws('KeyError', function() { format('{0}', []); }); + throws('KeyError', function() { format('{1}', ['abc']); }); + throws('KeyError', function() { format('{x}', []); }); + throws('ValueError', function() { format('}{', []); }); + throws('ValueError', function() { format('abc{0:{}', []); }); + throws('ValueError', function() { format('{0', []); }); + throws('KeyError', function() { format('{0.}', []); }); + throws('KeyError', function() { format('{0.}', [0]); }); + throws('KeyError', function() { format('{0[}', []); }); + throws('KeyError', function() { format('{0[}', [[]]); }); + throws('KeyError', function() { format('{0]}', []); }); + throws('KeyError', function() { format('{0.[]}', [0]); }); + throws('KeyError', function() { format('{0..foo}', [0]); }); + throws('KeyError', function() { format('{0[0}', [0]); }); + throws('KeyError', function() { format('{0[0:foo}', [0]); }); + throws('KeyError', function() { format('{c]}', []); }); + throws('ValueError', function() { format('{{ {{{0}}', [0]); }); + throws('ValueError', function() { format('{0}}', [0]); }); + throws('KeyError', function() { format('{foo}', [{bar: 3}]); }); + throws('ValueError', function() { format('{0!x}', [3]); }); + throws('ValueError', function() { format('{0!}', [0]); }); + throws('ValueError', function() { format('{!}', []); }); + throws('KeyError', function() { format('{:}', []); }); + throws('KeyError', function() { format('{:s}', []); }); + throws('KeyError', function() { format('{}', []); }); + + // exceed maximum recursion depth + throws('ValueError: Max string recursion exceeded', + function() { format('{0:{1:{2}}}', ['abc', 's', '']); }); + throws('ValueError: Max string recursion exceeded', + function() { format('{0:{1:{2:{3:{4:{5:{6}}}}}}}', + [0, 1, 2, 3, 4, 5, 6, 7]); }); + + // string format spec errors + throws('ValueError', function() { format('{0:-s}', ['']); }); + throws('ValueError', function() { format('{0:=s}', ['']); }); }); describe('format.create', function() { @@ -119,8 +175,8 @@ describe('format', function() { var formatA = format.create({x: append(' (formatA)')}); var formatB = format.create({x: append(' (formatB)')}); - eq(formatA('{!x}', 'abc'), 'abc (formatA)'); - eq(formatB('{!x}', 'abc'), 'abc (formatB)'); + eq(formatA('{!x}', ['abc']), 'abc (formatA)'); + eq(formatB('{!x}', ['abc']), 'abc (formatB)'); }); }); @@ -128,39 +184,39 @@ describe('format', function() { it('applies transformers to explicit positional arguments', function() { var $format = format.create({s: s}); var text = '{0}, you have {1} unread message{1!s}'; - eq($format(text, 'Steve', 1), 'Steve, you have 1 unread message'); - eq($format(text, 'Holly', 2), 'Holly, you have 2 unread messages'); + eq($format(text, ['Steve', 1]), 'Steve, you have 1 unread message'); + eq($format(text, ['Holly', 2]), 'Holly, you have 2 unread messages'); }); it('applies transformers to implicit positional arguments', function() { var $format = format.create({s: s}); var text = 'The Cure{!s}, The Door{!s}, The Smith{!s}'; - eq($format(text, 1, 2, 3), 'The Cure, The Doors, The Smiths'); + eq($format(text, [1, 2, 3]), 'The Cure, The Doors, The Smiths'); }); it('applies transformers to properties of explicit positional arguments', function() { var $format = format.create({s: s}); var text = 'view message{0.length!s}'; - eq($format(text, new Array(1)), 'view message'); - eq($format(text, new Array(2)), 'view messages'); + eq($format(text, [new Array(1)]), 'view message'); + eq($format(text, [new Array(2)]), 'view messages'); }); it('applies transformers to properties of implicit positional arguments', function() { var $format = format.create({s: s}); var text = 'view message{length!s}'; - eq($format(text, new Array(1)), 'view message'); - eq($format(text, new Array(2)), 'view messages'); + eq($format(text, [new Array(1)]), 'view message'); + eq($format(text, [new Array(2)]), 'view messages'); }); - it('throws if no such transformer is defined', function() { - assert.throws( - function() { format('foo-{!toString}-baz', 'bar'); }, - function(err) { - return err instanceof Error && - err.name === 'ValueError' && - err.message === 'no transformer named "toString"'; - } - ); + it('throws a ValueError if no such transformer is defined', function() { + throws('ValueError: no transformer named "toString"', + function() { format('foo-{!toString:}-baz', ['bar']); }); + throws('ValueError: no transformer named "toString"', + function() { format('foo-{!toString}-baz', ['bar']); }); + throws('ValueError: no transformer named ""', + function() { format('foo-{!:}-baz', ['bar']); }); + throws('ValueError: end of format while looking for conversion specifier', + function() { format('foo-{!}-baz', ['bar']); }); }); describe('format.extend', function() { @@ -182,4 +238,990 @@ describe('format', function() { }); + it('throws a ValueError if format specifier contains unused characters', function() { + throws('ValueError: Invalid conversion specification', + function() { format('{:ff}', [42]); }); + }); + + it('tests:type', function() { + eq(format('{:}', ['abc']), 'abc'); + eq(format('{:s}', ['abc']), 'abc'); + + eq(format('{:}', [42]), '42'); + eq(format('{:c}', [42]), '*'); + eq(format('{:d}', [42]), '42'); + eq(format('{:b}', [42]), '101010'); + eq(format('{:o}', [42]), '52'); + eq(format('{:x}', [42]), '2a'); + eq(format('{:X}', [42]), '2A'); + eq(format('{:e}', [42]), '4.200000e+01'); + eq(format('{:E}', [42]), '4.200000E+01'); + eq(format('{:f}', [42]), '42.000000'); + eq(format('{:g}', [42]), '42'); + eq(format('{:G}', [42]), '42'); + eq(format('{:%}', [42]), '4200.000000%'); + + eq(format('{:}', [3.14]), '3.14'); + eq(format('{:e}', [3.14]), '3.140000e+00'); + eq(format('{:E}', [3.14]), '3.140000E+00'); + eq(format('{:f}', [3.14]), '3.140000'); + eq(format('{:g}', [3.14]), '3.14'); + eq(format('{:G}', [3.14]), '3.14'); + eq(format('{:%}', [3.14]), '314.000000%'); + + throws('ValueError: cannot format non-integer with format specifier "c"', + function() { format('{:c}', [3.14]); }); + + throws('ValueError: cannot format non-integer with format specifier "d"', + function() { format('{:d}', [3.14]); }); + + throws('ValueError: cannot format non-integer with format specifier "b"', + function() { format('{:b}', [3.14]); }); + + throws('ValueError: cannot format non-integer with format specifier "o"', + function() { format('{:o}', [3.14]); }); + + throws('ValueError: cannot format non-integer with format specifier "x"', + function() { format('{:x}', [3.14]); }); + + throws('ValueError: cannot format non-integer with format specifier "X"', + function() { format('{:X}', [3.14]); }); + + eq(format('{:}', [[1, 2, 3]]), '1,2,3'); + }); + + it('tests:align', function() { + eq(format('{:<1s}', ['abc']), 'abc'); + eq(format('{:<8s}', ['abc']), 'abc '); + eq(format('{:<15s}', ['abc']), 'abc '); + + eq(format('{:^1s}', ['abc']), 'abc'); + eq(format('{:^8s}', ['abc']), ' abc '); + eq(format('{:^15s}', ['abc']), ' abc '); + + eq(format('{:>1s}', ['abc']), 'abc'); + eq(format('{:>8s}', ['abc']), ' abc'); + eq(format('{:>15s}', ['abc']), ' abc'); + + throws('ValueError: "=" alignment not allowed in string format specifier', + function() { format('{:=}', ['abc']); }); + throws('ValueError: "=" alignment not allowed in string format specifier', + function() { format('{:=s}', ['abc']); }); + + eq(format('{:<1c}', [42]), '*'); + eq(format('{:<8c}', [42]), '* '); + eq(format('{:<15c}', [42]), '* '); + + eq(format('{:^1c}', [42]), '*'); + eq(format('{:^8c}', [42]), ' * '); + eq(format('{:^15c}', [42]), ' * '); + + eq(format('{:>1c}', [42]), '*'); + eq(format('{:>8c}', [42]), ' *'); + eq(format('{:>15c}', [42]), ' *'); + + eq(format('{:=1c}', [42]), '*'); + eq(format('{:=8c}', [42]), ' *'); + eq(format('{:=15c}', [42]), ' *'); + + eq(format('{:<1d}', [-42]), '-42'); + eq(format('{:<1b}', [-42]), '-101010'); + eq(format('{:<1o}', [-42]), '-52'); + eq(format('{:<1x}', [-42]), '-2a'); + + eq(format('{:^1d}', [-42]), '-42'); + eq(format('{:^1b}', [-42]), '-101010'); + eq(format('{:^1o}', [-42]), '-52'); + eq(format('{:^1x}', [-42]), '-2a'); + + eq(format('{:>1d}', [-42]), '-42'); + eq(format('{:>1b}', [-42]), '-101010'); + eq(format('{:>1o}', [-42]), '-52'); + eq(format('{:>1x}', [-42]), '-2a'); + + eq(format('{:=1d}', [-42]), '-42'); + eq(format('{:=1b}', [-42]), '-101010'); + eq(format('{:=1o}', [-42]), '-52'); + eq(format('{:=1x}', [-42]), '-2a'); + + eq(format('{:<8d}', [-42]), '-42 '); + eq(format('{:<8b}', [-42]), '-101010 '); + eq(format('{:<8o}', [-42]), '-52 '); + eq(format('{:<8x}', [-42]), '-2a '); + + eq(format('{:^8d}', [-42]), ' -42 '); + eq(format('{:^8b}', [-42]), '-101010 '); + eq(format('{:^8o}', [-42]), ' -52 '); + eq(format('{:^8x}', [-42]), ' -2a '); + + eq(format('{:>8d}', [-42]), ' -42'); + eq(format('{:>8b}', [-42]), ' -101010'); + eq(format('{:>8o}', [-42]), ' -52'); + eq(format('{:>8x}', [-42]), ' -2a'); + + eq(format('{:=8d}', [-42]), '- 42'); + eq(format('{:=8b}', [-42]), '- 101010'); + eq(format('{:=8o}', [-42]), '- 52'); + eq(format('{:=8x}', [-42]), '- 2a'); + + eq(format('{:<15d}', [-42]), '-42 '); + eq(format('{:<15b}', [-42]), '-101010 '); + eq(format('{:<15o}', [-42]), '-52 '); + eq(format('{:<15x}', [-42]), '-2a '); + + eq(format('{:^15d}', [-42]), ' -42 '); + eq(format('{:^15b}', [-42]), ' -101010 '); + eq(format('{:^15o}', [-42]), ' -52 '); + eq(format('{:^15x}', [-42]), ' -2a '); + + eq(format('{:>15d}', [-42]), ' -42'); + eq(format('{:>15b}', [-42]), ' -101010'); + eq(format('{:>15o}', [-42]), ' -52'); + eq(format('{:>15x}', [-42]), ' -2a'); + + eq(format('{:=15d}', [-42]), '- 42'); + eq(format('{:=15b}', [-42]), '- 101010'); + eq(format('{:=15o}', [-42]), '- 52'); + eq(format('{:=15x}', [-42]), '- 2a'); + + eq(format('{:<1}', [-42]), '-42'); + eq(format('{:<1e}', [-42]), '-4.200000e+01'); + eq(format('{:<1f}', [-42]), '-42.000000'); + eq(format('{:<1g}', [-42]), '-42'); + eq(format('{:<1%}', [-42]), '-4200.000000%'); + + eq(format('{:^1}', [-42]), '-42'); + eq(format('{:^1e}', [-42]), '-4.200000e+01'); + eq(format('{:^1f}', [-42]), '-42.000000'); + eq(format('{:^1g}', [-42]), '-42'); + eq(format('{:^1%}', [-42]), '-4200.000000%'); + + eq(format('{:>1}', [-42]), '-42'); + eq(format('{:>1e}', [-42]), '-4.200000e+01'); + eq(format('{:>1f}', [-42]), '-42.000000'); + eq(format('{:>1g}', [-42]), '-42'); + eq(format('{:>1%}', [-42]), '-4200.000000%'); + + eq(format('{:=1}', [-42]), '-42'); + eq(format('{:=1e}', [-42]), '-4.200000e+01'); + eq(format('{:=1f}', [-42]), '-42.000000'); + eq(format('{:=1g}', [-42]), '-42'); + eq(format('{:=1%}', [-42]), '-4200.000000%'); + + eq(format('{:<8}', [-42]), '-42 '); + eq(format('{:<8e}', [-42]), '-4.200000e+01'); + eq(format('{:<8f}', [-42]), '-42.000000'); + eq(format('{:<8g}', [-42]), '-42 '); + eq(format('{:<8%}', [-42]), '-4200.000000%'); + + eq(format('{:^8}', [-42]), ' -42 '); + eq(format('{:^8e}', [-42]), '-4.200000e+01'); + eq(format('{:^8f}', [-42]), '-42.000000'); + eq(format('{:^8g}', [-42]), ' -42 '); + eq(format('{:^8%}', [-42]), '-4200.000000%'); + + eq(format('{:>8}', [-42]), ' -42'); + eq(format('{:>8e}', [-42]), '-4.200000e+01'); + eq(format('{:>8f}', [-42]), '-42.000000'); + eq(format('{:>8g}', [-42]), ' -42'); + eq(format('{:>8%}', [-42]), '-4200.000000%'); + + eq(format('{:=8}', [-42]), '- 42'); + eq(format('{:=8e}', [-42]), '-4.200000e+01'); + eq(format('{:=8f}', [-42]), '-42.000000'); + eq(format('{:=8g}', [-42]), '- 42'); + eq(format('{:=8%}', [-42]), '-4200.000000%'); + + eq(format('{:<15}', [-42]), '-42 '); + eq(format('{:<15e}', [-42]), '-4.200000e+01 '); + eq(format('{:<15f}', [-42]), '-42.000000 '); + eq(format('{:<15g}', [-42]), '-42 '); + eq(format('{:<15%}', [-42]), '-4200.000000% '); + + eq(format('{:^15}', [-42]), ' -42 '); + eq(format('{:^15e}', [-42]), ' -4.200000e+01 '); + eq(format('{:^15f}', [-42]), ' -42.000000 '); + eq(format('{:^15g}', [-42]), ' -42 '); + eq(format('{:^15%}', [-42]), ' -4200.000000% '); + + eq(format('{:>15}', [-42]), ' -42'); + eq(format('{:>15e}', [-42]), ' -4.200000e+01'); + eq(format('{:>15f}', [-42]), ' -42.000000'); + eq(format('{:>15g}', [-42]), ' -42'); + eq(format('{:>15%}', [-42]), ' -4200.000000%'); + + eq(format('{:=15}', [-42]), '- 42'); + eq(format('{:=15e}', [-42]), '- 4.200000e+01'); + eq(format('{:=15f}', [-42]), '- 42.000000'); + eq(format('{:=15g}', [-42]), '- 42'); + eq(format('{:=15%}', [-42]), '- 4200.000000%'); + }); + + it('tests:fill+align', function() { + eq(format('{:*<1}', ['abc']), 'abc'); + eq(format('{:*<1s}', ['abc']), 'abc'); + eq(format('{:*<8}', ['abc']), 'abc*****'); + eq(format('{:*<8s}', ['abc']), 'abc*****'); + eq(format('{:*<15}', ['abc']), 'abc************'); + eq(format('{:*<15s}', ['abc']), 'abc************'); + + eq(format('{:*^1}', ['abc']), 'abc'); + eq(format('{:*^1s}', ['abc']), 'abc'); + eq(format('{:*^8}', ['abc']), '**abc***'); + eq(format('{:*^8s}', ['abc']), '**abc***'); + eq(format('{:*^15}', ['abc']), '******abc******'); + eq(format('{:*^15s}', ['abc']), '******abc******'); + + eq(format('{:*>1}', ['abc']), 'abc'); + eq(format('{:*>1s}', ['abc']), 'abc'); + eq(format('{:*>8}', ['abc']), '*****abc'); + eq(format('{:*>8s}', ['abc']), '*****abc'); + eq(format('{:*>15}', ['abc']), '************abc'); + eq(format('{:*>15s}', ['abc']), '************abc'); + + eq(format('{:-<15c}', [42]), '*--------------'); + eq(format('{:-^15c}', [42]), '-------*-------'); + eq(format('{:->15c}', [42]), '--------------*'); + eq(format('{:-=15c}', [42]), '--------------*'); + + eq(format('{:*<15d}', [-42]), '-42************'); + eq(format('{:*<15b}', [-42]), '-101010********'); + eq(format('{:*<15o}', [-42]), '-52************'); + eq(format('{:*<15x}', [-42]), '-2a************'); + + eq(format('{:*^15d}', [-42]), '******-42******'); + eq(format('{:*^15b}', [-42]), '****-101010****'); + eq(format('{:*^15o}', [-42]), '******-52******'); + eq(format('{:*^15x}', [-42]), '******-2a******'); + + eq(format('{:*>15d}', [-42]), '************-42'); + eq(format('{:*>15b}', [-42]), '********-101010'); + eq(format('{:*>15o}', [-42]), '************-52'); + eq(format('{:*>15x}', [-42]), '************-2a'); + + eq(format('{:0=15d}', [-42]), '-00000000000042'); + eq(format('{:0=15b}', [-42]), '-00000000101010'); + eq(format('{:0=15o}', [-42]), '-00000000000052'); + eq(format('{:0=15x}', [-42]), '-0000000000002a'); + + eq(format('{:*<15}', [-42]), '-42************'); + eq(format('{:*<15e}', [-42]), '-4.200000e+01**'); + eq(format('{:*<15E}', [-42]), '-4.200000E+01**'); + eq(format('{:*<15f}', [-42]), '-42.000000*****'); + eq(format('{:*<15g}', [-42]), '-42************'); + eq(format('{:*<15G}', [-42]), '-42************'); + eq(format('{:*<15%}', [-42]), '-4200.000000%**'); + + eq(format('{:*^15}', [-42]), '******-42******'); + eq(format('{:*^15e}', [-42]), '*-4.200000e+01*'); + eq(format('{:*^15E}', [-42]), '*-4.200000E+01*'); + eq(format('{:*^15f}', [-42]), '**-42.000000***'); + eq(format('{:*^15g}', [-42]), '******-42******'); + eq(format('{:*^15G}', [-42]), '******-42******'); + eq(format('{:*^15%}', [-42]), '*-4200.000000%*'); + + eq(format('{:*>15}', [-42]), '************-42'); + eq(format('{:*>15e}', [-42]), '**-4.200000e+01'); + eq(format('{:*>15E}', [-42]), '**-4.200000E+01'); + eq(format('{:*>15f}', [-42]), '*****-42.000000'); + eq(format('{:*>15g}', [-42]), '************-42'); + eq(format('{:*>15G}', [-42]), '************-42'); + eq(format('{:*>15%}', [-42]), '**-4200.000000%'); + + eq(format('{:0=15}', [-42]), '-00000000000042'); + eq(format('{:0=15e}', [-42]), '-004.200000e+01'); + eq(format('{:0=15E}', [-42]), '-004.200000E+01'); + eq(format('{:0=15f}', [-42]), '-0000042.000000'); + eq(format('{:0=15g}', [-42]), '-00000000000042'); + eq(format('{:0=15G}', [-42]), '-00000000000042'); + eq(format('{:0=15%}', [-42]), '-004200.000000%'); + }); + + it('tests:sign', function() { + throws('ValueError: Sign not allowed in string format specifier', + function() { format('{:+}', ['abc']); }); + throws('ValueError: Sign not allowed in string format specifier', + function() { format('{:+s}', ['abc']); }); + + throws("ValueError: Sign not allowed with integer format specifier 'c'", + function() { format('{:+c}', [42]); }); + + eq(format('{:-d}', [42]), '42'); + eq(format('{:-d}', [-42]), '-42'); + eq(format('{: d}', [42]), ' 42'); + eq(format('{: d}', [-42]), '-42'); + eq(format('{:+d}', [42]), '+42'); + eq(format('{:+d}', [-42]), '-42'); + + eq(format('{:-b}', [42]), '101010'); + eq(format('{:-b}', [-42]), '-101010'); + eq(format('{: b}', [42]), ' 101010'); + eq(format('{: b}', [-42]), '-101010'); + eq(format('{:+b}', [42]), '+101010'); + eq(format('{:+b}', [-42]), '-101010'); + + eq(format('{:-o}', [42]), '52'); + eq(format('{:-o}', [-42]), '-52'); + eq(format('{: o}', [42]), ' 52'); + eq(format('{: o}', [-42]), '-52'); + eq(format('{:+o}', [42]), '+52'); + eq(format('{:+o}', [-42]), '-52'); + + eq(format('{:-x}', [42]), '2a'); + eq(format('{:-x}', [-42]), '-2a'); + eq(format('{: x}', [42]), ' 2a'); + eq(format('{: x}', [-42]), '-2a'); + eq(format('{:+x}', [42]), '+2a'); + eq(format('{:+x}', [-42]), '-2a'); + + eq(format('{:-}', [42]), '42'); + eq(format('{:-}', [-42]), '-42'); + eq(format('{: }', [42]), ' 42'); + eq(format('{: }', [-42]), '-42'); + eq(format('{:+}', [42]), '+42'); + eq(format('{:+}', [-42]), '-42'); + + eq(format('{:-e}', [42]), '4.200000e+01'); + eq(format('{:-e}', [-42]), '-4.200000e+01'); + eq(format('{: e}', [42]), ' 4.200000e+01'); + eq(format('{: e}', [-42]), '-4.200000e+01'); + eq(format('{:+e}', [42]), '+4.200000e+01'); + eq(format('{:+e}', [-42]), '-4.200000e+01'); + + eq(format('{:-f}', [42]), '42.000000'); + eq(format('{:-f}', [-42]), '-42.000000'); + eq(format('{: f}', [42]), ' 42.000000'); + eq(format('{: f}', [-42]), '-42.000000'); + eq(format('{:+f}', [42]), '+42.000000'); + eq(format('{:+f}', [-42]), '-42.000000'); + + eq(format('{:-g}', [42]), '42'); + eq(format('{:-g}', [-42]), '-42'); + eq(format('{: g}', [42]), ' 42'); + eq(format('{: g}', [-42]), '-42'); + eq(format('{:+g}', [42]), '+42'); + eq(format('{:+g}', [-42]), '-42'); + + eq(format('{:-%}', [42]), '4200.000000%'); + eq(format('{:-%}', [-42]), '-4200.000000%'); + eq(format('{: %}', [42]), ' 4200.000000%'); + eq(format('{: %}', [-42]), '-4200.000000%'); + eq(format('{:+%}', [42]), '+4200.000000%'); + eq(format('{:+%}', [-42]), '-4200.000000%'); + }); + + it('tests:#', function() { + throws('ValueError: Alternate form (#) not allowed in string format specifier', + function() { format('{:#}', ['abc']); }); + throws('ValueError: Alternate form (#) not allowed in string format specifier', + function() { format('{:#s}', ['abc']); }); + + eq(format('{:#}', [42]), '42'); + eq(format('{:#c}', [42]), '*'); + eq(format('{:#d}', [42]), '42'); + eq(format('{:#b}', [42]), '0b101010'); + eq(format('{:#o}', [42]), '0o52'); + eq(format('{:#x}', [42]), '0x2a'); + eq(format('{:#X}', [42]), '0X2A'); + }); + + it('tests:0', function() { + throws('ValueError: "=" alignment not allowed in string format specifier', + function() { format('{:0}', ['abc']); }); + throws('ValueError: "=" alignment not allowed in string format specifier', + function() { format('{:0s}', ['abc']); }); + + eq(format('{:0c}', [42]), '*'); + eq(format('{:0d}', [-42]), '-42'); + eq(format('{:0b}', [-42]), '-101010'); + eq(format('{:0o}', [-42]), '-52'); + eq(format('{:0x}', [-42]), '-2a'); + eq(format('{:0X}', [-42]), '-2A'); + + eq(format('{:0}', [-42]), '-42'); + eq(format('{:0e}', [-42]), '-4.200000e+01'); + eq(format('{:0E}', [-42]), '-4.200000E+01'); + eq(format('{:0f}', [-42]), '-42.000000'); + eq(format('{:0g}', [-42]), '-42'); + eq(format('{:0G}', [-42]), '-42'); + eq(format('{:0%}', [-42]), '-4200.000000%'); + + eq(format('{:08c}', [42]), '0000000*'); + eq(format('{:08d}', [-42]), '-0000042'); + eq(format('{:08b}', [-42]), '-0101010'); + eq(format('{:08o}', [-42]), '-0000052'); + eq(format('{:08x}', [-42]), '-000002a'); + eq(format('{:08X}', [-42]), '-000002A'); + + eq(format('{:08}', [-42]), '-0000042'); + eq(format('{:08e}', [-42]), '-4.200000e+01'); + eq(format('{:08E}', [-42]), '-4.200000E+01'); + eq(format('{:08f}', [-42]), '-42.000000'); + eq(format('{:08g}', [-42]), '-0000042'); + eq(format('{:08G}', [-42]), '-0000042'); + eq(format('{:08%}', [-42]), '-4200.000000%'); + + eq(format('{:015c}', [42]), '00000000000000*'); + eq(format('{:015d}', [-42]), '-00000000000042'); + eq(format('{:015b}', [-42]), '-00000000101010'); + eq(format('{:015o}', [-42]), '-00000000000052'); + eq(format('{:015x}', [-42]), '-0000000000002a'); + eq(format('{:015X}', [-42]), '-0000000000002A'); + + eq(format('{:015}', [-42]), '-00000000000042'); + eq(format('{:015e}', [-42]), '-004.200000e+01'); + eq(format('{:015E}', [-42]), '-004.200000E+01'); + eq(format('{:015f}', [-42]), '-0000042.000000'); + eq(format('{:015g}', [-42]), '-00000000000042'); + eq(format('{:015G}', [-42]), '-00000000000042'); + eq(format('{:015%}', [-42]), '-004200.000000%'); + }); + + it('tests:width', function() { + eq(format('{:1}', ['abc']), 'abc'); + eq(format('{:1s}', ['abc']), 'abc'); + eq(format('{:8}', ['abc']), 'abc '); + eq(format('{:8s}', ['abc']), 'abc '); + eq(format('{:15}', ['abc']), 'abc '); + eq(format('{:15s}', ['abc']), 'abc '); + + eq(format('{:1c}', [42]), '*'); + eq(format('{:1d}', [42]), '42'); + eq(format('{:1b}', [42]), '101010'); + eq(format('{:1o}', [42]), '52'); + eq(format('{:1x}', [42]), '2a'); + eq(format('{:1X}', [42]), '2A'); + + eq(format('{:1}', [42]), '42'); + eq(format('{:1e}', [42]), '4.200000e+01'); + eq(format('{:1E}', [42]), '4.200000E+01'); + eq(format('{:1f}', [42]), '42.000000'); + eq(format('{:1g}', [42]), '42'); + eq(format('{:1G}', [42]), '42'); + eq(format('{:1%}', [42]), '4200.000000%'); + + eq(format('{:8c}', [42]), ' *'); + eq(format('{:8d}', [42]), ' 42'); + eq(format('{:8b}', [42]), ' 101010'); + eq(format('{:8o}', [42]), ' 52'); + eq(format('{:8x}', [42]), ' 2a'); + eq(format('{:8X}', [42]), ' 2A'); + + eq(format('{:8}', [42]), ' 42'); + eq(format('{:8e}', [42]), '4.200000e+01'); + eq(format('{:8E}', [42]), '4.200000E+01'); + eq(format('{:8f}', [42]), '42.000000'); + eq(format('{:8g}', [42]), ' 42'); + eq(format('{:8G}', [42]), ' 42'); + eq(format('{:8%}', [42]), '4200.000000%'); + + eq(format('{:15c}', [42]), ' *'); + eq(format('{:15d}', [42]), ' 42'); + eq(format('{:15b}', [42]), ' 101010'); + eq(format('{:15o}', [42]), ' 52'); + eq(format('{:15x}', [42]), ' 2a'); + eq(format('{:15X}', [42]), ' 2A'); + + eq(format('{:15}', [42]), ' 42'); + eq(format('{:15e}', [42]), ' 4.200000e+01'); + eq(format('{:15E}', [42]), ' 4.200000E+01'); + eq(format('{:15f}', [42]), ' 42.000000'); + eq(format('{:15g}', [42]), ' 42'); + eq(format('{:15G}', [42]), ' 42'); + eq(format('{:15%}', [42]), ' 4200.000000%'); + }); + + it('tests:,', function() { + throws("ValueError: Cannot specify ',' with 's'", + function() { format('{:,}', ['abc']); }); + throws("ValueError: Cannot specify ',' with 's'", + function() { format('{:,s}', ['abc']); }); + throws("ValueError: Cannot specify ',' with 'c'", + function() { format('{:,c}', [42]); }); + throws("ValueError: Cannot specify ',' with 'b'", + function() { format('{:,b}', [42]); }); + throws("ValueError: Cannot specify ',' with 'o'", + function() { format('{:,o}', [42]); }); + throws("ValueError: Cannot specify ',' with 'x'", + function() { format('{:,x}', [42]); }); + throws("ValueError: Cannot specify ',' with 'X'", + function() { format('{:,X}', [42]); }); + + eq(format('{:,}', [1234567.89]), '1,234,567.89'); + eq(format('{:,d}', [1234567]), '1,234,567'); + eq(format('{:,e}', [1234567.89]), '1.234568e+06'); + eq(format('{:,E}', [1234567.89]), '1.234568E+06'); + eq(format('{:,f}', [1234567.89]), '1,234,567.890000'); + eq(format('{:,g}', [1234567.89]), '1.23457e+06'); + eq(format('{:,G}', [1234567.89]), '1.23457E+06'); + eq(format('{:,%}', [1234567.89]), '123,456,789.000000%'); + }); + + it('tests:precision', function() { + eq(format('{:.0}', ['abc']), ''); + eq(format('{:.1}', ['abc']), 'a'); + eq(format('{:.2}', ['abc']), 'ab'); + eq(format('{:.3}', ['abc']), 'abc'); + eq(format('{:.4}', ['abc']), 'abc'); + + eq(format('{:.0s}', ['abc']), ''); + eq(format('{:.1s}', ['abc']), 'a'); + eq(format('{:.2s}', ['abc']), 'ab'); + eq(format('{:.3s}', ['abc']), 'abc'); + eq(format('{:.4s}', ['abc']), 'abc'); + + throws('ValueError: Precision not allowed in integer format specifier', + function() { format('{:.4c}', [42]); }); + + throws('ValueError: Precision not allowed in integer format specifier', + function() { format('{:.4d}', [42]); }); + + throws('ValueError: Precision not allowed in integer format specifier', + function() { format('{:.4b}', [42]); }); + + throws('ValueError: Precision not allowed in integer format specifier', + function() { format('{:.4o}', [42]); }); + + throws('ValueError: Precision not allowed in integer format specifier', + function() { format('{:.4x}', [42]); }); + + throws('ValueError: Precision not allowed in integer format specifier', + function() { format('{:.4X}', [42]); }); + + eq(format('{:.0}', [3.14]), '3'); + eq(format('{:.1}', [3.14]), '3'); + eq(format('{:.2}', [3.14]), '3.1'); + eq(format('{:.3}', [3.14]), '3.14'); + eq(format('{:.4}', [3.14]), '3.14'); + + eq(format('{:.0e}', [3.14]), '3e+00'); + eq(format('{:.1e}', [3.14]), '3.1e+00'); + eq(format('{:.2e}', [3.14]), '3.14e+00'); + eq(format('{:.3e}', [3.14]), '3.140e+00'); + eq(format('{:.4e}', [3.14]), '3.1400e+00'); + + eq(format('{:.0E}', [3.14]), '3E+00'); + eq(format('{:.1E}', [3.14]), '3.1E+00'); + eq(format('{:.2E}', [3.14]), '3.14E+00'); + eq(format('{:.3E}', [3.14]), '3.140E+00'); + eq(format('{:.4E}', [3.14]), '3.1400E+00'); + + eq(format('{:.0f}', [3.14]), '3'); + eq(format('{:.1f}', [3.14]), '3.1'); + eq(format('{:.2f}', [3.14]), '3.14'); + eq(format('{:.3f}', [3.14]), '3.140'); + eq(format('{:.4f}', [3.14]), '3.1400'); + + eq(format('{:.0g}', [3.14]), '3'); + eq(format('{:.1g}', [3.14]), '3'); + eq(format('{:.2g}', [3.14]), '3.1'); + eq(format('{:.3g}', [3.14]), '3.14'); + eq(format('{:.4g}', [3.14]), '3.14'); + + eq(format('{:.0G}', [3.14]), '3'); + eq(format('{:.1G}', [3.14]), '3'); + eq(format('{:.2G}', [3.14]), '3.1'); + eq(format('{:.3G}', [3.14]), '3.14'); + eq(format('{:.4G}', [3.14]), '3.14'); + + throws('ValueError: Format specifier missing precision', + function() { format('{:.f}', [3.14]); }); + }); + + it('tests:(fill AND) align AND sign AND #', function() { + eq(format('{:<+#o}', [42]), '+0o52'); + eq(format('{:*<+#o}', [42]), '+0o52'); + }); + + it('tests:(fill AND) align AND sign AND # AND width', function() { + eq(format('{:<+#8o}', [42]), '+0o52 '); + eq(format('{:>+#8o}', [42]), ' +0o52'); + eq(format('{:*<+#8o}', [42]), '+0o52***'); + eq(format('{:*>+#8o}', [42]), '***+0o52'); + }); + + it('tests:(fill AND) align AND sign AND 0', function() { + eq(format('{:<+0}', [42]), '+42'); + eq(format('{:*<+0}', [42]), '+42'); + }); + + it('tests:(fill AND) align AND sign AND 0 AND width', function() { + eq(format('{:<+08}', [42]), '+4200000'); + eq(format('{:>+08}', [42]), '00000+42'); + eq(format('{:*<+08}', [42]), '+42*****'); + eq(format('{:*>+08}', [42]), '*****+42'); + }); + + it('tests:(fill AND) align AND sign AND width', function() { + eq(format('{:<+8}', [42]), '+42 '); + eq(format('{:>+8}', [42]), ' +42'); + eq(format('{:*<+8}', [42]), '+42*****'); + eq(format('{:*>+8}', [42]), '*****+42'); + }); + + it('tests:(fill AND) align AND sign AND width AND precision', function() { + eq(format('{:<+8.2f}', [42]), '+42.00 '); + eq(format('{:>+8.2f}', [42]), ' +42.00'); + eq(format('{:*<+8.2f}', [42]), '+42.00**'); + eq(format('{:*>+8.2f}', [42]), '**+42.00'); + }); + + it('tests:(fill AND) align AND sign AND ,', function() { + eq(format('{:<+,}', [4200]), '+4,200'); + eq(format('{:*<+,}', [4200]), '+4,200'); + }); + + it('tests:(fill AND) align AND sign AND precision', function() { + eq(format('{:<+.2f}', [42]), '+42.00'); + eq(format('{:*<+.2f}', [42]), '+42.00'); + }); + + it('tests:(fill AND) align AND # AND 0 AND width', function() { + eq(format('{:<#08o}', [42]), '0o520000'); + eq(format('{:>#08o}', [42]), '00000o52'); + eq(format('{:*<#08o}', [42]), '0o52****'); + eq(format('{:*>#08o}', [42]), '****0o52'); + }); + + it('tests:(fill AND) align AND # AND width', function() { + eq(format('{:<#8o}', [42]), '0o52 '); + eq(format('{:>#8o}', [42]), ' 0o52'); + eq(format('{:*<#8o}', [42]), '0o52****'); + eq(format('{:*>#8o}', [42]), '****0o52'); + }); + + it('tests:(fill AND) align AND # AND width AND ,', function() { + eq(format('{:<#8,}', [4200]), '4,200 '); + eq(format('{:>#8,}', [4200]), ' 4,200'); + eq(format('{:*<#8,}', [4200]), '4,200***'); + eq(format('{:*>#8,}', [4200]), '***4,200'); + }); + + it('tests:(fill AND) align AND 0 AND width', function() { + eq(format('{:<08}', [-42]), '-4200000'); + eq(format('{:>08}', [-42]), '00000-42'); + eq(format('{:*<08}', [-42]), '-42*****'); + eq(format('{:*>08}', [-42]), '*****-42'); + }); + + it('tests:(fill AND) align AND 0 AND width AND ,', function() { + eq(format('{:<08,}', [4200]), '4,200000'); + eq(format('{:^08,}', [4200]), '04,20000'); + eq(format('{:>08,}', [4200]), '0004,200'); + eq(format('{:=08,}', [4200]), '0,004,200'); + eq(format('{:*<08,}', [4200]), '4,200***'); + eq(format('{:*^08,}', [4200]), '*4,200**'); + eq(format('{:*>08,}', [4200]), '***4,200'); + eq(format('{:*=08,}', [4200]), '***4,200'); + }); + + it('tests:(fill AND) align AND 0 AND width AND precision', function() { + eq(format('{:<08.2f}', [-42]), '-42.0000'); + eq(format('{:^08.2f}', [-42]), '0-42.000'); + eq(format('{:>08.2f}', [-42]), '00-42.00'); + eq(format('{:=08.2f}', [-42]), '-0042.00'); + eq(format('{:*<08.2f}', [-42]), '-42.00**'); + eq(format('{:*^08.2f}', [-42]), '*-42.00*'); + eq(format('{:*>08.2f}', [-42]), '**-42.00'); + eq(format('{:*=08.2f}', [-42]), '-**42.00'); + }); + + it('tests:(fill AND) align AND 0 AND ,', function() { + eq(format('{:<0,}', [4200]), '4,200'); + eq(format('{:*<0,}', [4200]), '4,200'); + }); + + it('tests:(fill AND) align AND 0 AND precision', function() { + eq(format('{:<0.2f}', [42]), '42.00'); + eq(format('{:*<0.2f}', [42]), '42.00'); + }); + + it('tests:(fill AND) align AND width AND ,', function() { + eq(format('{:<8,}', [-4200]), '-4,200 '); + eq(format('{:>8,}', [-4200]), ' -4,200'); + eq(format('{:*<8,}', [-4200]), '-4,200**'); + eq(format('{:*>8,}', [-4200]), '**-4,200'); + eq(format('{:0=8,}', [-4200]), '-004,200'); + eq(format('{:0=8,}', [4200]), '0,004,200'); + }); + + it('tests:(fill AND) align AND width AND , AND precision', function() { + eq(format('{:<15,.2f}', [-4200000]), '-4,200,000.00 '); + eq(format('{:^15,.2f}', [-4200000]), ' -4,200,000.00 '); + eq(format('{:>15,.2f}', [-4200000]), ' -4,200,000.00'); + eq(format('{:=15,.2f}', [-4200000]), '- 4,200,000.00'); + eq(format('{:*<15,.2f}', [-4200000]), '-4,200,000.00**'); + eq(format('{:*^15,.2f}', [-4200000]), '*-4,200,000.00*'); + eq(format('{:*>15,.2f}', [-4200000]), '**-4,200,000.00'); + eq(format('{:*=15,.2f}', [-4200000]), '-**4,200,000.00'); + }); + + it('tests:(fill AND) align AND width AND precision', function() { + eq(format('{:<8.2f}', [42]), '42.00 '); + eq(format('{:>8.2f}', [42]), ' 42.00'); + eq(format('{:*<8.2f}', [42]), '42.00***'); + eq(format('{:*>8.2f}', [42]), '***42.00'); + }); + + it('tests:(fill AND) align AND , AND precision', function() { + eq(format('{:<,.2f}', [4200]), '4,200.00'); + eq(format('{:*<,.2f}', [4200]), '4,200.00'); + }); + + it('tests:sign AND # AND 0', function() { + eq(format('{:+#0o}', [42]), '+0o52'); + }); + + it('tests:sign AND # AND width', function() { + eq(format('{:+#8o}', [42]), ' +0o52'); + }); + + it('tests:sign AND 0 AND width', function() { + eq(format('{:+08}', [42]), '+0000042'); + }); + + it('tests:sign AND 0 AND ,', function() { + eq(format('{:+08,}', [4200]), '+004,200'); + }); + + it('tests:sign AND 0 AND precision', function() { + eq(format('{:+08.2f}', [42]), '+0042.00'); + }); + + it('tests:sign AND width AND ,', function() { + eq(format('{:+8,}', [4200]), ' +4,200'); + }); + + it('tests:sign AND width AND precision', function() { + eq(format('{:+8.2f}', [42]), ' +42.00'); + }); + + it('tests:sign AND , AND precision', function() { + eq(format('{:+,.2f}', [4200]), '+4,200.00'); + }); + + it('tests:# AND 0 AND width', function() { + eq(format('{:#08o}', [42]), '0o000052'); + }); + + it('tests:# AND width AND ,', function() { + eq(format('{:#8,}', [4200]), ' 4,200'); + }); + + it('tests:0 AND width AND ,', function() { + eq(format('{:08,}', [4200]), '0,004,200'); + }); + + it('tests:0 AND width AND precision', function() { + eq(format('{:08.2f}', [42]), '00042.00'); + }); + + it('tests:0 AND , AND precision', function() { + eq(format('{:08,.2f}', [42]), '0,042.00'); + }); + + it('tests:width AND , AND precision', function() { + eq(format('{:15,.2f}', [4200000]), ' 4,200,000.00'); + }); + + it('asdf 1', function() { + eq(format('{0.@#$.%^&}', [{'@#$': {'%^&': 42}}]), '42'); + }); + + it('asdf 2', function() { + format.extend(String.prototype, {'@#$': R.always('xyz')}); + eq('{!@#$}'.format('abc'), 'xyz'); + delete String.prototype.format; + }); + + it('asdf 3', function() { + eq(format('{:d}', [0]), '0'); + eq(format('{:d}', [-0]), '-0'); + eq(format('{:d}', [Infinity]), 'Infinity'); + eq(format('{:d}', [-Infinity]), '-Infinity'); + eq(format('{:d}', [NaN]), 'NaN'); + + eq(format('{}', [0]), '0'); + eq(format('{}', [-0]), '-0'); + eq(format('{}', [Infinity]), 'Infinity'); + eq(format('{}', [-Infinity]), '-Infinity'); + eq(format('{}', [NaN]), 'NaN'); + + eq(format('{:f}', [0]), '0.000000'); + eq(format('{:f}', [-0]), '-0.000000'); + eq(format('{:f}', [Infinity]), 'Infinity'); + eq(format('{:f}', [-Infinity]), '-Infinity'); + eq(format('{:f}', [NaN]), 'NaN'); + }); + + it('allows "," to be used as a thousands separator', function() { + eq(format('{:,}', [42]), '42'); + eq(format('{:,}', [420]), '420'); + eq(format('{:,}', [4200]), '4,200'); + eq(format('{:,}', [42000]), '42,000'); + eq(format('{:,}', [420000]), '420,000'); + eq(format('{:,}', [4200000]), '4,200,000'); + + eq(format('{:00,}', [42]), '42'); + eq(format('{:01,}', [42]), '42'); + eq(format('{:02,}', [42]), '42'); + eq(format('{:03,}', [42]), '042'); + eq(format('{:04,}', [42]), '0,042'); + eq(format('{:05,}', [42]), '0,042'); + eq(format('{:06,}', [42]), '00,042'); + eq(format('{:07,}', [42]), '000,042'); + eq(format('{:08,}', [42]), '0,000,042'); + eq(format('{:09,}', [42]), '0,000,042'); + eq(format('{:010,}', [42]), '00,000,042'); + }); + + it('allows non-string, non-number arguments', function() { + throws('ValueError: non-empty format string for Array object', + function() { format('{:,}', [[1, 2, 3]]); }); + + throws('ValueError: non-empty format string for Array object', + function() { format('{:z}', [[1, 2, 3]]); }); + }); + + it('throws if a number is passed to a string formatter', function() { + throws('ValueError: unknown format code "s" for Number object', + function() { format('{:s}', [42]); }); + }); + + it('throws if a string is passed to a number formatter', function() { + throws('ValueError: unknown format code "c" for String object', + function() { format('{:c}', ['42']); }); + + throws('ValueError: unknown format code "d" for String object', + function() { format('{:d}', ['42']); }); + + throws('ValueError: unknown format code "b" for String object', + function() { format('{:b}', ['42']); }); + + throws('ValueError: unknown format code "o" for String object', + function() { format('{:o}', ['42']); }); + + throws('ValueError: unknown format code "x" for String object', + function() { format('{:x}', ['42']); }); + + throws('ValueError: unknown format code "X" for String object', + function() { format('{:X}', ['42']); }); + + throws('ValueError: unknown format code "f" for String object', + function() { format('{:f}', ['42']); }); + + throws('ValueError: unknown format code "e" for String object', + function() { format('{:e}', ['42']); }); + + throws('ValueError: unknown format code "E" for String object', + function() { format('{:E}', ['42']); }); + + throws('ValueError: unknown format code "g" for String object', + function() { format('{:g}', ['42']); }); + + throws('ValueError: unknown format code "G" for String object', + function() { format('{:G}', ['42']); }); + + throws('ValueError: unknown format code "%" for String object', + function() { format('{:%}', ['42']); }); + }); + + it('provides a format function when "required"', function() { + eq(format("The name's {1}. {0} {1}.", ['James', 'Bond']), + "The name's Bond. James Bond."); + }); + + it('asdf 4', function() { + throws('ValueError: ' + + 'Alternate form (#) not allowed in float format specifier', + function() { format('{:#%}', [42]); }); + }); + + var __f = function(s) { + return s.length === 5 || random(0, 1) === 0 ? s : + s === '0' ? __f(String(random(1, 9))) : + __f(s + String(random(0, 9))); + }; + + var random_spec = function(types) { + var align = sample(['', '<', '>', '=', '^']); + var fill = sample(['', String.fromCharCode(random(0x20, 0x7E))]); + var sign = sample(['', '+', '-', ' ']); + var hash = sample(['', '#']); + var zero = sample(['', '0']); + var width = sample(['', __f('0')]); + var comma = sample(['', ',']); + var precision = sample(['', '.' + __f('0')]); + var type = sample(types); + + return fill + align + sign + hash + zero + width + comma + precision + type; + }; + + // fromError :: Error -> String + var fromError = R.pipe( + R.prop('message'), + R.split(/^/m), + R.last, + R.replace(/\n$/, ''), + R.replace(/\.$/, '') + ); + + it.skip('matches the Python implementation (float)', function(done) { + var specs = R.map(function() { + return random_spec(['e', 'E', 'f', 'F', 'g', 'G', '%']); + }, R.range(0, 1000)); + + var recur = function recur() { + if (specs.length > 0) { + var spec = specs.pop(); + var thunk = function() { return format('{:' + spec + '}', [42]); }; + console.log(" format('{:" + spec.replace(/'/g, '\\$&') + "}', [42])"); + exec( + "python -c 'import sys; sys.stdout.write(\"{:" + spec.replace(/"/g, '\\$&').replace(/'/g, '$&"$&"$&') + "}\".format(42.0))'", + function(err, stdout) { + if (err == null) { + eq(thunk(), String(stdout), "format('{:" + spec + "}', [42]) === '" + stdout.replace(/'/g, '\\$&') + "'"); + } else { + throws(fromError(err), thunk, "format('{:" + spec + "}', [42])"); + } + recur(); + } + ); + } else { + done(); + } + }; + recur(); + }); + + it.skip('matches the Python implementation (int)', function(done) { + var specs = R.map(function() { + return random_spec(['b', 'c', 'd', 'o', 'x', 'X']); + }, R.range(0, 1000)); + + var recur = function recur() { + if (specs.length > 0) { + var spec = specs.pop(); + var thunk = function() { return format('{:' + spec + '}', [42]); }; + console.log(" format('{:" + spec.replace(/'/g, '\\$&') + "}', [42])"); + exec( + "python -c 'import sys; sys.stdout.write(\"{:" + spec.replace(/"/g, '\\$&').replace(/'/g, '$&"$&"$&') + "}\".format(42))'", + function(err, stdout) { + if (err == null) { + eq(thunk(), + String(stdout), + "format('{:" + spec + "}', [42]) === '" + stdout.replace(/'/g, '\\$&') + "'"); + } else { + throws(fromError(err), thunk, "format('{:" + spec + "}', [42])"); + } + recur(); + } + ); + } else { + done(); + } + }; + recur(); + }); + }); diff --git a/test/readme.js b/test/readme.js index ed11153..7790409 100644 --- a/test/readme.js +++ b/test/readme.js @@ -27,7 +27,8 @@ var extractChunks = R.pipe( accum.inExample && line !== '```\n' ? { inExample: true, - examples: R.append(R.last(accum.examples) + line, + examples: R.append(R.last(accum.examples) + + line.replace(/[ ]+[/][/].*/, ''), R.slice(0, -1, accum.examples)) } : line === '```javascript\n' ?