Bugfix for sprintf option "%-<n>s"
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#lang info
|
||||
|
||||
(define pkg-authors '(hnmdijkema))
|
||||
(define version "0.1.2")
|
||||
(define version "0.1.3")
|
||||
(define license 'MIT)
|
||||
(define collection "racket-sprintf")
|
||||
(define pkg-desc "racket-sprintf - simple sprintf implementation")
|
||||
@@ -20,6 +20,7 @@
|
||||
(define build-deps
|
||||
'("racket-doc"
|
||||
"scribble-lib"
|
||||
"rackunit-lib"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+76
-145
@@ -1,174 +1,105 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@(require scribble/manual
|
||||
;scribble/class
|
||||
scribble/eval
|
||||
(for-label racket/base
|
||||
))
|
||||
@(require (for-label racket/base
|
||||
racket/contract/base
|
||||
racket/format
|
||||
racket-sprintf))
|
||||
|
||||
@(define the-eval
|
||||
(make-base-eval))
|
||||
@title{racket-sprintf}
|
||||
@author["Hans Dijkema / hans@dijkewijk.nl"]
|
||||
|
||||
@defmodule[racket-sprintf]
|
||||
|
||||
@title{sprintf functions}
|
||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||
The @tt{racket-sprintf} package provides @racket[sprintf] and
|
||||
@racket[sprintf*], two small C-style string formatting procedures. The
|
||||
format language deliberately supports only a useful subset of C
|
||||
@tt{printf}. The result is always a string; output is not written to a port.
|
||||
|
||||
@defmodule[sprintf]
|
||||
@section{Procedures}
|
||||
|
||||
This module provides a compact @tt{printf}-style string
|
||||
formatting facility for Racket.
|
||||
It is inspired by C-style format strings, but implemented
|
||||
on top of @racket[racket/format].
|
||||
|
||||
The core functionality is provided by @racket[sprintf]
|
||||
and @racket[sprintf*].
|
||||
|
||||
@section{Overview}
|
||||
|
||||
The module exports two formatting functions:
|
||||
|
||||
@itemlist[
|
||||
@item{@racket[sprintf] — formats using variadic arguments}
|
||||
@item{@racket[sprintf*] — formats using an explicit argument list}
|
||||
]
|
||||
|
||||
Both functions use the same format-string syntax and semantics.
|
||||
|
||||
@section{Exports}
|
||||
|
||||
@defproc[(sprintf [format string?] [arg any/c] ...) string?]{
|
||||
Formats the string @racket[format] by substituting the given
|
||||
arguments according to printf-style conversion specifications.
|
||||
|
||||
Arguments are consumed sequentially from left to right.
|
||||
An error is raised if there are too many or too few arguments
|
||||
for the format string.
|
||||
@defproc[(sprintf [fmt string?] [arg any/c] ...) string?]{
|
||||
Formats the arguments according to @racket[fmt] and returns the resulting
|
||||
string. Arguments are consumed from left to right. An exception is raised
|
||||
when a conversion is missing an argument, when arguments remain after all
|
||||
conversions have been processed, or when an argument has the wrong type for
|
||||
its conversion.
|
||||
}
|
||||
|
||||
@defproc[(sprintf* [format string?] [args list?]) string?]{
|
||||
Variant of @racket[sprintf] where the arguments are supplied
|
||||
explicitly as a list.
|
||||
|
||||
This form is useful when arguments are constructed dynamically
|
||||
or passed through higher-order functions.
|
||||
@defproc[(sprintf* [fmt string?] [args list?]) string?]{
|
||||
Like @racket[sprintf], but takes the values to format in the list
|
||||
@racket[args]. This is useful when the arguments have already been collected
|
||||
in a list.
|
||||
}
|
||||
|
||||
@section{Format String Syntax}
|
||||
@section{Format syntax}
|
||||
|
||||
A format specification has the general form:
|
||||
A conversion has this form:
|
||||
|
||||
@verbatim{%[flag][width][.precision][length]conversion}
|
||||
|
||||
The supported conversion characters are @tt{s}, @tt{d}, @tt{f}, @tt{x}, and
|
||||
@tt{%}. @tt{%s} formats a string. @tt{%d} formats a decimal number.
|
||||
@tt{%f} formats a number using the requested precision. @tt{%x} formats a
|
||||
number in base 16. @tt{%%} represents a literal percent sign.
|
||||
|
||||
The optional @tt{-} flag left-aligns a value within its field. Without
|
||||
@tt{-}, a value with a field width is right-aligned. For numeric conversions,
|
||||
the optional @tt{0} flag pads on the left with zeroes.
|
||||
|
||||
A width specifies the minimum field width. For example:
|
||||
|
||||
@verbatim{
|
||||
%[flags][width][.precision][length]type
|
||||
(sprintf "%10s" "Abc") ; => " Abc"
|
||||
(sprintf "%-10s" "Abc") ; => "Abc "
|
||||
(sprintf "%05d" 42) ; => "00042"
|
||||
(sprintf "%-5d" 42) ; => "42 "
|
||||
}
|
||||
|
||||
Only a well-defined subset of the traditional @tt{printf}
|
||||
syntax is supported.
|
||||
|
||||
@section{Supported Conversion Types}
|
||||
|
||||
The following conversion specifiers are recognized:
|
||||
|
||||
@itemlist[
|
||||
@item{@tt{%d} — decimal integer}
|
||||
@item{@tt{%x} — hexadecimal integer}
|
||||
@item{@tt{%f} — floating-point number}
|
||||
@item{@tt{%s} — string}
|
||||
@item{@tt{%%} — literal percent sign}
|
||||
]
|
||||
|
||||
@section{Flags}
|
||||
|
||||
The following flags are supported:
|
||||
|
||||
@itemlist[
|
||||
@item{@tt{0} — pad numeric output with zeros instead of spaces}
|
||||
@item{@tt{-} — left-align the result within the field width}
|
||||
]
|
||||
|
||||
If no flag is specified, numeric values are right-aligned and
|
||||
padded with spaces.
|
||||
|
||||
@section{Field Width}
|
||||
|
||||
A minimum field width may be specified as a decimal number:
|
||||
For @tt{%s}, precision specifies the maximum number of characters taken from
|
||||
the string. Truncation happens before field-width padding. Consequently:
|
||||
|
||||
@verbatim{
|
||||
%8d
|
||||
%-10s
|
||||
(sprintf "%.5s" "abcdefgh") ; => "abcde"
|
||||
(sprintf "%10.5s" "abcdefgh") ; => " abcde"
|
||||
(sprintf "%-10.5s" "abcdefgh") ; => "abcde "
|
||||
}
|
||||
|
||||
If the formatted value is shorter than the field width, it is
|
||||
padded according to the alignment rules.
|
||||
For numeric conversions other than @tt{%d}, precision is passed to
|
||||
@racket[~r] as the number of digits after the decimal point. Decimal integer
|
||||
conversion @tt{%d} always uses integer-style precision zero.
|
||||
|
||||
@section{Dynamic Width and Precision}
|
||||
@section{Dynamic width and precision}
|
||||
|
||||
Both width and precision may be specified using @tt{*}.
|
||||
In that case, the value is taken from the argument list.
|
||||
|
||||
Example:
|
||||
A width or precision can be written as @tt{*}. Its value is then consumed
|
||||
from the argument list before the value being formatted. For example:
|
||||
|
||||
@verbatim{
|
||||
(sprintf "%*.*f" 8 3 1.23456)
|
||||
}
|
||||
|
||||
In this example:
|
||||
@itemlist[
|
||||
@item{the first argument specifies the field width}
|
||||
@item{the second argument specifies the precision}
|
||||
@item{the third argument is the value to be formatted}
|
||||
Here @tt{8} supplies the field width, @tt{3} supplies the precision, and
|
||||
@tt{1.23456} is the value. A dynamic width or precision must be numeric.
|
||||
|
||||
@section{Length modifier}
|
||||
|
||||
One or more @tt{l} characters are accepted before the conversion character
|
||||
for compatibility with existing format strings, but currently have no effect
|
||||
on the result.
|
||||
|
||||
@section{Type rules}
|
||||
|
||||
The @tt{%s} conversion requires a string. The numeric conversions require a
|
||||
number. The implementation intentionally reports a type mismatch instead of
|
||||
silently coercing the value.
|
||||
|
||||
@section{Examples}
|
||||
|
||||
@racketblock[
|
||||
(sprintf "%-12s %5d" "items" 42)
|
||||
(sprintf "%08x" 255)
|
||||
(sprintf "%-10.4s" "abcdefgh")
|
||||
(sprintf* "%s = %d" (list "answer" 42))
|
||||
]
|
||||
|
||||
The arguments supplying width or precision must be numbers;
|
||||
otherwise an error is raised.
|
||||
|
||||
@section{Precision}
|
||||
|
||||
Precision has different meanings depending on the conversion type:
|
||||
|
||||
@itemlist[
|
||||
@item{for numeric conversions, it controls the number of digits after the decimal point}
|
||||
@item{for strings, it specifies the maximum output width}
|
||||
]
|
||||
|
||||
If no precision is specified:
|
||||
@itemlist[
|
||||
@item{numeric values default to precision 0 for integers}
|
||||
@item{strings are not truncated}
|
||||
]
|
||||
|
||||
@section{Length Modifiers}
|
||||
|
||||
The length modifier field (e.g. @tt{l}) is parsed for syntactic
|
||||
compatibility but otherwise ignored.
|
||||
It has no semantic effect.
|
||||
|
||||
@section{Type Checking and Errors}
|
||||
|
||||
The formatter performs runtime type checking:
|
||||
|
||||
@itemlist[
|
||||
@item{numeric conversion specifiers require numeric arguments}
|
||||
@item{@tt{%s} requires a string argument}
|
||||
]
|
||||
|
||||
Errors are raised in the following situations:
|
||||
|
||||
@itemlist[
|
||||
@item{arguments remain but no format specifiers are left}
|
||||
@item{format specifiers remain but no arguments are left}
|
||||
@item{@tt{*} is used but no corresponding numeric argument is available}
|
||||
@item{argument types do not match the conversion specifier}
|
||||
]
|
||||
|
||||
Error messages are designed to be explicit and descriptive.
|
||||
|
||||
@section{Implementation Notes}
|
||||
|
||||
Internally, the formatter uses regular-expression driven parsing
|
||||
and delegates the actual formatting to @racket[~r] and @racket[~a]
|
||||
from @racket[racket/format].
|
||||
|
||||
The implementation is intentionally strict: mismatches between
|
||||
format strings and arguments are treated as programming errors
|
||||
and reported immediately.
|
||||
|
||||
The implementation uses @racket[~a] and @racket[~r] from
|
||||
@racketmodname[racket/format] for the final field formatting.
|
||||
|
||||
+51
-41
@@ -43,15 +43,18 @@
|
||||
(- (string-length r) (string-length r-trim))
|
||||
#\space)))
|
||||
r)))
|
||||
(let* ((pad-str (if (string=? zeros "") " " zeros))
|
||||
(min-width (if (eq? adjust-width #f) 0 adjust-width))
|
||||
(max-width (if (eq? precision #f) +inf.0 precision))
|
||||
(adjust (if (eq? zeros #f) 'left
|
||||
(if (string=? zeros "-") 'left 'right)))
|
||||
)
|
||||
(let* ((min-width (if (eq? adjust-width #f) 0 adjust-width))
|
||||
(adjust (if (string=? zeros "-") 'left 'right))
|
||||
(value (if (and (number? precision)
|
||||
(> (string-length arg) precision))
|
||||
(substring arg 0 precision)
|
||||
arg)))
|
||||
(unless (eq? kind 's)
|
||||
(error "argument is a string, but a number is expected"))
|
||||
(~a arg #:pad-string pad-str #:min-width min-width #:max-width max-width #:align adjust))
|
||||
(~a value
|
||||
#:pad-string " "
|
||||
#:min-width min-width
|
||||
#:align adjust))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -61,47 +64,54 @@
|
||||
(format a ...))))
|
||||
|
||||
(define (do-format format args)
|
||||
(if (null? args)
|
||||
(let ((m (regexp-match re-format format)))
|
||||
(unless (eq? m #f)
|
||||
(error (fmt "formatting left, but no arguments left: ~a" format)))
|
||||
format)
|
||||
(let ((m (regexp-match re-format format)))
|
||||
(when (eq? m #f)
|
||||
(error (fmt "arguments left, but no formatting left: ~a" format)))
|
||||
(let ((m (regexp-match re-format format)))
|
||||
(if (eq? m #f)
|
||||
(begin
|
||||
(unless (null? args)
|
||||
(error (fmt "arguments left, but no formatting left: ~a" format)))
|
||||
format)
|
||||
(let* ((matched-length (string-length (list-ref m 0)))
|
||||
(prefix (list-ref m 1))
|
||||
(zeros (list-ref m 2))
|
||||
(adjust-width (list-ref m 3))
|
||||
(precision (list-ref m 5))
|
||||
(long (list-ref m 6))
|
||||
(kind (string->symbol (list-ref m 7)))
|
||||
)
|
||||
(unless (eq? adjust-width #f)
|
||||
(set! adjust-width (if (string=? adjust-width "*")
|
||||
(let ((n (shift args)))
|
||||
(when (null? args)
|
||||
(error "* requires >= 2 arguments left"))
|
||||
(unless (number? n)
|
||||
(error "* requires a number?"))
|
||||
n)
|
||||
(string->number adjust-width))))
|
||||
(unless (eq? precision #f)
|
||||
(set! precision (if (string=? precision "*")
|
||||
(let ((n (shift args)))
|
||||
(when (null? args)
|
||||
(error "* requires >= 2 arguments left"))
|
||||
(unless (number? n)
|
||||
(error "* requires a number?"))
|
||||
n)
|
||||
(string->number precision))))
|
||||
(string-append prefix
|
||||
(if (eq? kind '%)
|
||||
(kind (string->symbol (list-ref m 7))))
|
||||
(if (eq? kind '%)
|
||||
(string-append prefix
|
||||
"%"
|
||||
(format-part zeros adjust-width precision kind (shift args)))
|
||||
(do-format (substring format matched-length) args))))
|
||||
)
|
||||
)
|
||||
(do-format (substring format matched-length) args))
|
||||
(begin
|
||||
(when (null? args)
|
||||
(error (fmt "formatting left, but no arguments left: ~a" format)))
|
||||
(unless (eq? adjust-width #f)
|
||||
(set! adjust-width
|
||||
(if (string=? adjust-width "*")
|
||||
(let ((n (shift args)))
|
||||
(when (null? args)
|
||||
(error "* requires >= 2 arguments left"))
|
||||
(unless (number? n)
|
||||
(error "* requires a number?"))
|
||||
n)
|
||||
(string->number adjust-width))))
|
||||
(unless (eq? precision #f)
|
||||
(set! precision
|
||||
(if (string=? precision "*")
|
||||
(let ((n (shift args)))
|
||||
(when (null? args)
|
||||
(error "* requires >= 2 arguments left"))
|
||||
(unless (number? n)
|
||||
(error "* requires a number?"))
|
||||
n)
|
||||
(string->number precision))))
|
||||
(string-append prefix
|
||||
(format-part zeros
|
||||
adjust-width
|
||||
precision
|
||||
kind
|
||||
(shift args))
|
||||
(do-format (substring format matched-length) args))))))))
|
||||
|
||||
|
||||
|
||||
(define (sprintf format . args)
|
||||
|
||||
Reference in New Issue
Block a user