Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sprintf implementation / fmt package #565

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions fmt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
Printf for Deno
===============

This is very much a work-in-progress. I'm actively soliciting feedback.
What immediately follows are points for discussion.

If you are looking for the documentation proper, skip to:

"printf: prints formatted output"

below.


Discussion
----------

This is very much a work-in-progress. I'm actively soliciting feedback.


- What useful features are available in other languages apart from
Golang and C?

- behaviour of `%v` verb. In Golang, this is a shortcut verb to "print the
default format" of the argument. It is currently implemented to format
using `toString` in the default case and `inpect` if the `%#v`
alternative format flag is used in the format directive. Alternativly,
`%V` could be used to distinguish the two.

`inspect` output is not defined, however. This may be problematic if using
this code on other plattforms (and expecting interoperability). To my
knowledge, no suitable specification of object representation aside from JSON
and `toString` exist. ( Aside: see "[Common object formats][3]" in the
"Console Living Standard" which basically says "do whatever" )

- `%j` verb. This is an extension particular to this implementation. Currently
not very sophisticated, it just runs `JSON.stringify` on the argument.
Consider possible modifier flags, etc.

- `<` verb. This is an extension that assumes the argument is an array and will
format each element according to the format (surrounded by [] and seperated
by comma) (`<` Mnemonic: pull each element out of array)

- how to deal with more newfangled Javascript features ( generic Iterables,
Map and Set types, typed Arrays, ...)

- the implementation is fairly rough around the edges:

- currently contains little in the way of checking for
correctness. Conceivably, there will be a 'strict' form, e.g.
that ensures only Number-ish arguments are passed to %f flags

- assembles output using string concatenation instead of
utilizing buffers or other optimizations. It would be nice to
have printf / sprintf / fprintf (etc) all in one.

- float formatting is handled by toString() and to `toExponential`
along with a mess of Regexp. Would be nice to use fancy match

- some flags that are potentially applicable ( POSIX long and unsigned
modifiers are not likely useful) are missing, namely %q (print quoted), %U
(unicode format)


Author
------
Tim Becker (tim@presseverykey.com)

License
-------

MIT

At the current time this module contains code from:


https://github.com/shrpne/from-exponential


The implementation is inspired by POSIX and Golang (see above) but does
not port implementation code. A number of Golang test-cases based on:

https://golang.org/src/fmt/fmt_test.go
( BSD: Copyright (c) 2009 The Go Authors. All rights reserved. )


were used.




printf: prints formatted output
=======================


sprintf converts and formats a variable number of arguments as is
specified by a `format string`. In it's basic form, a format string
may just be a literal. In case arguments are meant to be formatted,
a `directive` is contained in the format string, preceded by a '%' character:

%<verb>

E.g. the verb `s` indicates the directive should be replaced by the
string representation of the argument in the corresponding position of
the argument list. E.g.:

Hello %s!

applied to the arguments "World" yields "Hello World!"

The meaning of the format string is modelled after [POSIX][1] format
strings as well as well as [Golang format strings][2]. Both contain
elements specific to the respective programming language that don't
apply to JavaScript, so they can not be fully supported. Furthermore we
implement some functionality that is specific to JS.


Verbs
-----

The following verbs are supported:

| Verb | Meaning |
|--------|----------------------------------------------------------------|
| `% ` | print a literal percent |
| `t ` | evaluate arg as boolean, print `true` or `false` |
| `b ` | eval as number, print binary |
| `c ` | eval as number, print character corresponding to the codePoint |
| `o ` | eval as number, print octal |
| `x X ` | print as hex (ff FF), treat string as list of bytes |
| `e E ` | print number in scientific/exponent format 1.123123e+01 |
| `f F ` | print number as float with decimal point and no exponent |
| `g G ` | use %e %E or %f %F depending on size of argument |
| `s ` | interpolate string |
| `T ` | type of arg, as returned by `typeof` |
| `v ` | value of argument in 'default' format (see below) |
| `j ` | argument as formatted by `JSON.stringify` |



Width and Precision
-------------------

Verbs may be modified by providing them with width and precision, either or
both may be omitted:

%9f width 9, default precision
%.9f default width, precision 9
%8.9f width 8, precision 9
%8.f width 9, precision 0

In general, 'width' describes the minimum length of the output, while 'precision'
limits the output.

| verb | precision |
| ----------|----------------------------------------------------------------|
| `t ` | n/a |
| `b c o ` | n/a |
| `x X ` | n/a for number, strings are truncated to p bytes(!) |
| `e E f F` | number of places after decimal, default 6 |
| `g G ` | set maximum number of digits |
| `s ` | truncate input |
| `T ` | truncate |
| `v ` | tuncate, or depth if used with # see "'default' format", below |
| `j ` | n/a |

Numerical values for width and precision can be substituted for the `*` char, in
which case the values are obtained from the next args, e.g.:

sprintf ("%*.*f", 9,8,456.0)

is equivalent to

sprintf ("%9.9f", 456.0)


Flags
-----

The effects of the verb may be further influenced by using flags to modify the
directive:

| Flag | Verb | Meaning |
|--------|-----------|----------------------------------------------------------------------------|
| `+ ` | numeric | always print sign |
| `- ` | all | pad to the right (left justify) |
| `# ` | | alternate format |
| `# ` | `b o x X` | prefix with `0b 0 0x` |
| `# ` | `g G` | don't remove trailing zeros |
| `# ` | `v` | ues output of `inspect` instead of `toString` |
| `' ' ` | | space character |
| `' ' ` | `x X` | leave spaces between bytes when printing string |
| `' ' ` | `d` | insert space for missing `+` sign character |
| `0 ` | all | pad with zero, `-` takes precedence, sign is appended in front of padding |
| `<` | all | format elements of the passed array according to the directive (extension) |


'default' format
----------------
The default format used by `%v` is the result of calling `toString()` on the
relevant argument. If the `#` flags is used, the result of calling `inspect()`
is interpolated. In this case, the precision, if set is passed to `inspect()` as
the 'depth' config parameter


Positional arguments
--------------------

Arguments do not need to be consumed in the order they are provded and may
be consumed more than once. E.g.:

sprintf("%[2]s %[1]s", "World", "Hello")

returns "Hello World". The precence of a positional indicator resets the arg counter
allowing args to be reused:

sprintf("dec[%d]=%d hex[%[1]d]=%x oct[%[1]d]=%#o %s", 1, 255, "Third")

returns `dec[1]=255 hex[1]=0xff oct[1]=0377 Third`

Width and precision my also use positionals:

"%[2]*.[1]*d", 1, 2

This follows the golang conventions and not POSIX.


Errors
------

The following errors are handled:

Incorrect verb:
S("%h", "") %!(BAD VERB 'h')

Too few arguments:
S("%d") %!(MISSING 'd')"




[1]: https://pubs.opengroup.org/onlinepubs/009695399/functions/fprintf.html
[2]: https://golang.org/pkg/fmt/
[3]: https://console.spec.whatwg.org/#object-formats
12 changes: 12 additions & 0 deletions fmt/TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

* "native" formatting, json, arrays, object/structs, functions ...
* %q %U
* Java has a %n flag to print the plattform native newline... in POSIX
that means "number of chars printed so far", though.
* use of Writer and Buffer internally in order to make FPrintf, Printf, etc.
easier and more elegant.
* see "Discussion" in README

*scanf , pack,unpack, annotated hex
* error handling, consistantly
* probably rewrite, now that I konw how it's done.
105 changes: 105 additions & 0 deletions fmt/fromExponential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// following code is from: https://github.com/shrpne/from-exponential
//
// Copyright (c) 2018, Respective Authors all rights reserved.
//
// The MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in// f the Software without restriction, including without limitation the rights
// to// f use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// co// fpies of the Software, and to permit persons to whom the Software is
// fu// frnished to do so, subject to the following conditions:
// // f
// Th// fe above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

/**
* Return two parts array of exponential number
* @param {number|string|Array} num
* @return {string[]}
*/
function getExponentialParts(num) {
return Array.isArray(num) ? num : String(num).split(/[eE]/);
}

/**
*
* @param {number|string|Array} num - number or array of its parts
*/
function isExponential(num) {
const eParts = getExponentialParts(num);
return !Number.isNaN(Number(eParts[1]));
}


/**
* Converts exponential notation to a human readable string
* @param {number|string|Array} num - number or array of its parts
* @return {string}
*/
export function fromExponential(num) {
const eParts = getExponentialParts(num);
if (!isExponential(eParts)) {
return eParts[0];
}

const sign = eParts[0][0] === '-' ? '-' : '';
const digits = eParts[0].replace(/^-/, '');
const digitsParts = digits.split('.');
const wholeDigits = digitsParts[0];
const fractionDigits = digitsParts[1] || '';
let e = Number(eParts[1]);

if (e === 0) {
return `${sign + wholeDigits}.${fractionDigits}`;
} else if (e < 0) {
// move dot to the left
const countWholeAfterTransform = wholeDigits.length + e;
if (countWholeAfterTransform > 0) {
// transform whole to fraction
const wholeDigitsAfterTransform = wholeDigits.substr(0, countWholeAfterTransform);
const wholeDigitsTransformedToFracton = wholeDigits.substr(countWholeAfterTransform);
return `${sign + wholeDigitsAfterTransform}.${wholeDigitsTransformedToFracton}${fractionDigits}`;
} else {
// not enough whole digits: prepend with fractional zeros

// first e goes to dotted zero
let zeros = '0.';
e += 1;
while (e) {
zeros += '0';
e += 1;
}
return sign + zeros + wholeDigits + fractionDigits;
}
} else {
// move dot to the right
const countFractionAfterTransform = fractionDigits.length - e;
if (countFractionAfterTransform > 0) {
// transform fraction to whole
// countTransformedFractionToWhole = e
const fractionDigitsAfterTransform = fractionDigits.substr(e);
const fractionDigitsTransformedToWhole = fractionDigits.substr(0, e);
return `${sign + wholeDigits + fractionDigitsTransformedToWhole}.${fractionDigitsAfterTransform}`;
} else {
// not enough fractions: append whole zeros
let zerosCount = -countFractionAfterTransform;
let zeros = '';
while (zerosCount) {
zeros += '0';
zerosCount -= 1;
}
return sign + wholeDigits + fractionDigits + zeros;
}
}
}
Loading