diff --git a/info.rkt b/info.rkt index d3c9171..e5d8f16 100644 --- a/info.rkt +++ b/info.rkt @@ -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" ) ) diff --git a/scribblings/sprintf.scrbl b/scribblings/sprintf.scrbl index de0f9b4..7afd82b 100644 --- a/scribblings/sprintf.scrbl +++ b/scribblings/sprintf.scrbl @@ -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. diff --git a/sprintf.rkt b/sprintf.rkt index 87f89de..ead5e49 100644 --- a/sprintf.rkt +++ b/sprintf.rkt @@ -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)