1. Introduction
[P0645] has proposed a text formatting facility that provides a safe
and extensible alternative to the
family of functions. This
paper explores how to format complex numbers represented by
.
2. Examples
Default format:
std :: string s = std :: format ( "{}" , 1.0 + 2 i ); // s == "(1+2i)"
iostreams-compatible format (optional):
std :: string s = std :: format ( "{:p}" , 1.0 + 2 i ); // s == "(1,2)"
Format specifiers:
std :: string s = std :: format ( "{:.2f}" , 1.0 + 2 i ); // s == "1.00+2.00i"
3. Motivation
This is a proposal defining formatting of complex numbers
represented by the library type
. The default notation (3+4i)
is proposed, as it is common in
mathematics, the physical sciences, and many other popular mathematical
software environments. This form is also more consistent with the standard
library literals for
[N3660]. In addition to defining the
new format and discussing design choices, this proposal attempts to address
questions around introducing a format which differs from the existing
iostreams format, and why the aforementioned advantages outweigh the
disadvantages of introducing a potentially incompatible format.
An option to produce iostreams-compatible output is also provided.
The formatting of
should be simple, consistent with
existing conventions of
, and conveniently support the most
common use cases of
. As the first nested format specified for
, it can also serve as an example for how format nesting can be done.
Mathematics generally follows the convention that complex numbers consist of a real part and an orthogonal imaginary part which is identified by multiplication of the imaginary unit vector \( i \). Extending the set of unit vectors in this way furthermore implies straightforward extensions to other useful algebras such as quaternions \( i, j, k \), dual numbers \( \epsilon \), etc.
For the types
, C++14
introduced string literals to the standard library in the namespace
. These string literals acknowledge
the common use cases of these types and provide a convenient way to
write complex numbers in code, for example the number \( 1 + 1i \)
can be written in code as as 1.0f + 1if
, 1.0 + 1i
, or 1.0l + 1il
,
depending on the desired underlying type.
Sometimes it is possible to omit one part in a symbolic representation
yet retain bijectivity in the machine representation to symbolic
mapping. For example, the complex number \( 0 + 0i \) can be
unambiguously written as either
or
. The convention of
mathematics is the former, although the latter has the advantage of
implying the underlying field.
As specified in [N4849], the existing iostreams formatting of a complex
number
is essentially
wheres << '(' << x . real () << "," << x . imag () << ')' ;
s
is a stream object.
This embedded comma can cause silent unexpected generation of ambiguous output, which can happen e.g. when the locale’s decimal separator is set to comma. This ambiguity does not exist in the imaginary unit notation, even when an unusual locale is used.
4. Design Considerations
With an eye to providing a replacement for all the functionality of iostreams, the following considerations are made.
4.1. Numeric form
The question of how to represent the numeric type
of
is simply delegated to the
for that
type. Special alignment, fill, and sign rules may apply when
is
,
or
, but other custom value
types are accomodated. This is done by optionally forwarding a
designated portion of the
format spec to
.
Although the standard does not specify behavior of
for types other than
,
,
, it is not
uncommon to use a type for
which provides functionality such as
extended precision or automatic differentiation. The formatting
specification should therefore be recursive, so that arbitrary
numerical types for
are properly formatted.
4.2. Imaginary unit
As previously mentioned, mathematics notation typically uses \( i \) as
the complex unit vector, however it is very common in electrical
engineering to use \( j \) instead. Mathematica uses the Unicode
character ⅈ for the imaginary unit. Another common written form of
complex numbers puts the imaginary unit in front of the imaginary part
rather than after it. Julia uses the dual-character symbol
, and
it it easy to imagine wanting to explicitly specify the
usually-omitted implied real unit-vector, result in a format like
. Supporting these use cases would be nice, but not with
significant implementation difficulty.
4.3. Omission of a part
Because the complex number is always a pair of real part and imaginary part, it is not necessary to print both parts if one of the parts is identical to a known quantity: typically (nonnegative) zero; in this case omission implies the value uniquely. Either the real or the imaginary part can be omitted when this condition is satisfied, although clearly not both.
Should a part be dropped?
The benefits of part dropping include: shorter conversions in the special but common cases of purely real or imaginary numbers, adherence to common notation. There is also a tie-in with the design consideration discussed below of whether surrounding parenthesis are necessary: a single numeric value does not need to be surrounded by parenthesis in order to recognize it as the value for an entire complex number.
What are the conditions under which a part can be dropped?
A simple comparison with zero is usually insufficient to decide
whether a part can be omitted. While C++ does not specify the
underlying floating-point format, for correct round-trip conversions,
the omitted part must be binary equivalent to
. The function
is used to distinguish between
and
, so the
type
must have both a defined
and
to distinguish the two cases.
This nuance is demonstrated by the result of
vs
.
Which part should be dropped?
Either part of an imaginary number could be dropped if it is binary
equal to
, but in the special case of \( 0 + 0i \) dropping both
parts would lead to the absurdity of an empty string. This is an open
question, but it is the opinion of the author that the real part
should be dropped, so that the remaining symbolic representation
retains the imaginary unit vector, indicating use of the complex field
\( \mathbb{C} \).
5. Parentheses
Should parentheses be mandatory?
Are parentheses always neccesary to unambiguously specify a complex number?
Do mandatory parentheses significantly improve ease or speed of complex number parsing?
If parentheses are not mandatory, when should they be omitted?
6. Backwards Compatibility
To maintain backward compatibility we propose an easy-to-use format specifier that exactly reproduces the legacy iostreams output format.
The
specifiers that affect complex number output are
and
, these can not be easily guessed, but can be specified
manually in the nested format specifier. Otherwise the compatibilty
format the output will produce roughly the same output (modulo locale
and default format for
) that iostreams produces.
7. Parsing
This paper does not address parsing (scan’ing) for the type
but does aim to produce formatted output that can
unambiguously round trip formatted and parsed.
8. Survey of other languages
The following programming languages/environments similarly use the imaginary-unit notation as their default: Python, Julia, R, MATLAB, Mathematica, Go. If you know the type of the data, these languages offer round-trip conversion from complex -> text -> complex, but because some of them drop the complex part in their textual output when the complex part is zero (or even negative zero!) some arguably pertinent information can be lost during formatting.
Language | Basic Format | Result of
| Result of
|
C++ iostreams |
|
|
|
NumPy |
|
|
|
Julia |
|
|
|
Octave |
|
|
|
Mathematica* |
| ⅈ
|
|
R |
|
|
|
C++14 literals |
|
|
|
Go |
|
|
|
- checked via wolframalpha
Haskell provides
notation - this choice does not need much commentary,
this much is offered: it is quite unique.
C# does not provide this functionality, but the doc page for complex includes an example code for creating an appropriate formatter.
9. Wish List
Feature wish list:
-
nested specification of real and imaginary parts via
formatter < T > -
easy substitution of "old style" iostreams format with simply
{ : p } -
defineable symbol for imaginary unit (
,j
)im -
option to prefix the imaginary part with the imaginary unit
-
control over which (real/imag) part omission (
or0
)0 j -
default to minimalist unique parseable format:
,1
,1 i
,0 j ( 1 + 1 i ) -
toggle to turn off surrounding parens:
1 + 1 i -
toggle to turn off outputting
on negative zero - addressed by P2021- -
center alignment
aligns output around the connecting^ +/- -
option for polar formatted output, ie
( 1.41421 * exp ( i * 3.14159 ))
10. Proposed Wording
Modify [complex.syn] as follows:
template < class T , class charT , class traits > basic_ostream < charT , traits >& operator << ( basic_ostream < charT , traits >& , const complex < T >& ); // 26.4.?, formatting template < class charT > struct formatter < complex < float > , charT > ; template < class charT > struct formatter < complex < double > , charT > ; template < class charT > struct formatter < complex < long double > , charT > ;
Add a new section 26.4.? Formatting [complex.format]:
Eachformatter < complex < T > , charT >
(format.formatter) specialization in this section meets
the Formatter requirements (formatter.requirements). The parse
member functions of
these formatters interpret the format specification as std-format-spec (format.string.std) except that the 0
option is invalid.
Lettemplate < class charT > struct formatter < complex < T > , charT > { typename basic_format_parse_context < charT >:: iterator parse ( basic_format_parse_context < charT >& ctx ); template < class FormatContext > typename FormatContext :: iterator format ( const complex < T >& c , FormatContext & ctx ); }; template < class FormatContext > typename FormatContext :: iterator format ( const complex < T >& c , FormatContext & ctx );
real = format ( ctx . locale (), "{:<format-specs>}" , c . real ())
and imag = format ( ctx . locale (), "{:<format-specs>}" , c . imag ())
,
where < format - specs >
is std-format-spec with fill-and-align and width removed.
Effects: Equivalent to:
whereformat_to ( ctx . out (), "{:<fill-align-width>}" , format ( c . real () != 0 ? "({0}+{1}i)" : "{1}i" , real , imag ))
< fill - align - width >
is the fill-and-align and width part of std-format-spec. If alignment is not specified >
is used.
11. Questions
Q1: Do we want any of this?
Q2: The strategy of this paper is to include a laundry list of possibilities, which parts do we want?