Looking at the docs for Strings, String | Apple Developer Documentation , i could not find the initializer that takes a double to convert. I wanted to know if the doc is missing an api, because I know there is one..
I’m looking to learn how the conversion is implemented. particularly, what happens if the input double is really long like 0.49999999... For example, in java it returns a string of a value with the fewest decimal places that can convert back to the given double, so possibly 0.45.
To understand how Swift converts types to String you should read up on the initializers under String.init(describing:)
This means that you should investigate each protocol conformance which has a supported initializer via this group.
xwu
(Xiaodi Wu)
November 20, 2025, 9:29pm
3
The relevant initializer is this one, unless I'm mistaken and there's another more specific:
The documentation doesn't have any information specific to floating-point values. However, conformance to LosslessStringConvertible (as all of the standard library floating-point types do) provides an important guarantee that you touch on: the string representation you get has enough information that it can be converted back to exactly the same floating-point value you start out with.
How exactly it's done is an implementation detail which can change at any time. However, since Swift 4.2, the standard library has used a variation of the Grisu2 algorithm with changes described in Errol3, which means that you'll get a string representation with the minimum number of digits required for lossless conversion and with good performance (but, again, the API makes no such guarantee ):
master ← tbkka:tbkka-floating-point-printing-C
opened 01:39AM - 24 Mar 18 UTC
This replaces the current implementation of `description` and
`debugDescription… ` for the standard floating-point types with a new
formatting routine that provides both improved performance and
better results.
Unlike the earlier code based on `sprintf` with a fixed number of
digits, this logic automatically chooses the optimal number of digits,
generating compact output where possible while ensuring round-trip
accuracy. As such, we can now use the exact same output for both `description` and
`debugDescription` (except of course that `debugDescription` provides
full detail for NaNs).
This resolves [SR-106](https://bugs.swift.org/browse/SR-106), [SR-454](https://bugs.swift.org/browse/SR-454), [SR-491](https://bugs.swift.org/browse/SR-491), [SR-3131](https://bugs.swift.org/browse/SR-3131) and other related issues that have complained
about the floating-point `description` and `debugDescription` properties being inexact
and/or inconsistent.
## Ergonomics
With the new code, the REPL generally prints the values you would expect.
```
(swift) 1.1
// r0 : Double = 1.1
(swift) (1.1).description
// r1 : String = "1.1"
(swift) 1e23
// r2 : Double = 1e+23
(swift) print("\(1e23.nextDown) \(1e23) \(1e23.nextUp)")
9.999999999999997e+22 1e+23 1.0000000000000001e+23
(swift) 1.100000000000001
// r3 : Double = 1.100000000000001
(swift) print("\(1.100000000000001)")
1.100000000000001
(swift) 1.0 / 10.0
// r4 : Double = 0.1
(swift) 1.0 / 10.0 + 1.0
// r5 : Double = 1.1
```
In comparison, the previous implementation routinely prints extraneous digits for `debugDescription` (used by the REPL) and omits significant digits in `description` (used by `print`):
```
Welcome to Apple Swift version 4.1 (swiftlang-902.0.38 clang-902.0.31). Type :help for assistance.
1> 1.1
$R0: Double = 1.1000000000000001
2> (1.1).description
$R1: String = "1.1"
3> 1e23
$R2: Double = 9.9999999999999991E+22
4> print("\(1e23.nextDown) \(1e23) \(1e23.nextUp)")
1e+23 1e+23 1e+23
5> 1.100000000000001
$R3: Double = 1.100000000000001
6> print("\(1.100000000000001)")
1.1
7> 1.0 / 10.0
$R4: Double = 0.10000000000000001
8> 1.0 / 10.0 + 1.0
$R5: Double = 1.1000000000000001
```
Of course, this only changes how the floating-point numbers are printed. The actual parsing, storage, and arithmetic operations are unaffected and are still subject to the same rounding issues common to all floating-point arithmetic.
## About the Algorithm
The `SwiftDtoa.c` file here implements a variation of Florian Loitsch' Grisu2
algorithm with changes suggested by Andrysco, Jhala, and Lerner's 2016
paper describing Errol3.
The implementation is:
* Fast. It uses only fixed-width integer arithmetic and has constant
memory and time requirements.
* Simple. It is only a little more complex than Loitsch' original
implementation of Grisu2. The digit decomposition logic for double is
less than 300 lines of standard C (half of which is common arithmetic
support routines).
* Always Accurate. Converting the decimal form back to binary using an
accurate algorithm (such as Clinger's algorithm) will always yield exactly the
original binary value. For the IEEE 754 formats, the round-trip will
produce exactly the same bit pattern in memory. This is an essential
requirement for debugging, logging, and JSON serialization.
* Always Short. This always selects an accurate result with the minimum
number of decimal digits. (So that `1.0 / 10.0` will always print `0.1`.)
* Always Close. Among all accurate, short results, this always chooses
the result that is closest to the exact floating-point value. (In case
of an exact tie, it rounds the last digit even.)
## Performance
The graph below compares the new code (in green) to the performance of three other
popular algorithms. Note that this graph benchmarks just the underlying C code; the Swift
`description` logic must spend additional effort to allocate a returned String.
* Dragon4 (in yellow) uses variable-length arithmetic in order to provide accurate results regardless of the number of digits requested. This causes it to become significantly slower when the input has a large positive or negative exponent. (The other algorithms here do not offer arbitrary numbers of digits, so can use much faster fixed-width arithmetic.) This is the algorithm commonly used by C standard library implementations of the `printf` family of functions.
* Grisu3 + Dragon4 (in red) uses fast fixed-width arithmetic for 99% of values and falls back to Dragon4 about 1% of the time. Note the upper whisker that goes to the same height as Dragon4.
* Errol4 (in blue) is a recent algorithm that uses multiple-precision floating-point arithmetic internally. It is the successor to Errol3 which partly inspired the new Swift implementation.
* The new `SwiftDtoa` implementation (in green) is similar to the Errol algorithms but uses fixed-width integer arithmetic throughout. This gives it uniform fast performance regardless of the input value.
<img width="619" alt="screen shot 2018-03-23 at 5 53 13 pm" src="https://user-images.githubusercontent.com/21696764/37858541-2b83677c-2ec3-11e8-9c5e-9a7dda50e84f.png">
The implementation was recently re-written in native Swift, partly to support Embedded use cases:
main ← tbkka:tbkka-swift-floatingpointtostring
opened 10:38PM - 02 Jul 25 UTC
This replaces the previous SwiftDtoa.cpp with the same algorithm reimplemented i… n Swift. It supports Float16, Float32, Float64, and Float80 (on Intel).
Performance is reasonable: In my testing (M1 and x86_64): Float16 and Float32 are a bit faster than the C version, Float64 is almost exactly the same, Float80 is a bit slower.
I think I've finally worked out the availability, though I'd appreciate someone who knows better taking a critical look.
3 Likes
tbkka
(Tim Kientzle)
April 3, 2026, 9:45pm
4
You can get a String representation for a Double using:
The .description property, or
The .debugDescription property
Since Swift 4.2, both of the above produce the shortest value that parses back to exactly the same value. The only difference is that NaNs that have payload values are printed in an extended form by .debugDescription.
let s = (0.4999999999).description
Most people are likely to use string interpolation or the String(describing:) initializer:
let s = "My value is \(value)"
let t = "My value is " + String(describing: value)
both of which internally call .description to generate the actual text for a floating-point value.
If you’re really interested in the inner details, I’m working on a new implementation that’s both simpler and faster:
main ← tbkka:tbkka-new-fp-description
opened 03:52PM - 03 Mar 26 UTC
This reimplements debugDescription for Float16, Float, Double, and Float80 using… a new algorithm based on Raffaello Guilietti's Schubfach paper, carrying over some ideas from the previous implementation which drew ideas from Grisu, Errol, and Ryū.
In practice, this is about twice as fast as the previous implementation, and the code is both shorter and simpler.
Extensive testing shows that the new implementations produce identical output to the previous implementations.
## Algorithm Sketch
The basic idea is:
* Start by expressing the floating-point value as `s * 2^e` where `s` and `e` are both integers.
* Estimate the power of 10 as `p = ceiling(e * log10(2))`
* Scale the upper bound of the rounding interval by `10^(-p)` -- the integer portion of the fixed-point result will give you between 0 and 16 digits for Double (0 to 8 for Float)
* About 40% of the time, those initial digits (after pruning any trailing zeros) are precisely the desired optimal form
* The remaining 60% of the time, we need exactly one additional digit.
* A slight adjustment allows us to directly compute one of only two possibilities for that final digit. A quick test discriminates between them.
The performance follows from the fact that we routinely get the full decimal significand with just a couple of high-precision operations. (Unlike Grisu, there is no per-digit looping; unlike Ryū, there is no division.) This allows a fast integer conversion to handle all of the digit formatting in a single operation without any per-digit overhead.
Note: If you're interested in more details, the Float32 implementation has extensive comments.
## Performance
On an M4 MacBook Air, the core `_Float32ToASCII` for single-precision takes about 6ns to produce the final text in a local buffer. Previously, this took 9ns. `Float.debugDescription` based on this takes a total of 9ns, including constructing the small string and other overhead.
The core double-precision implementation takes about 10ns to produce the text, compared to 19ns previously. The full `debugDescription` requires another 14ns to allocate a `String` on the heap. Float64 code + tables is well under 4k on Apple Silicon, about 700 bytes smaller than before.
I'm especially pleased that the new Float80 implementation is about 1/2 as much code as before (which is appropriate for this rarely-used format) while still managing to be about 3x faster than the previous version. The previous code required almost 6k of Float80-specific tables; that's now been reduced to just 896 bytes. The total Float80-specific code and tables is now around 4.5k, compared to 9k earlier. Further work could probably cut the code size down even further, with only modest additional sacrifice in performance. Also note that the Float80 logic here should be accurate enough to support Float128 if there's ever a need.
## Interesting Notes
The power-of-10 estimate implies that `10^p > 2^e`. This in turn means that the result of the initial scaling has a decimal ULP (difference between adjacent decimal forms) greater than the binary ULP (difference between adjacent binary forms). This is the basis for the assertion that whenever the initial scaling gives us a value in the rounding interval, it is unique and therefore must be correct. Results with fewer significant digits occur exactly when the result at this step has trailing zeros that can be elided.
The power-of-10 estimate also implies that `2^e > 10^(p-1)`. So a single additional digit gives us a decimal form with an ULP smaller than the binary ULP, which is the basis for the assertion that one additional digit always suffices.
The core algorithm takes a pair of integers (s,e) representing s * 2^e and produces a pair of integers (s', p) representing s' * 10^p. The bounds just above give us interesting bounds on the relationship between s and s', especially for subnormal cases where s can itself be small.
Working from the upper bound of the rounding interval in the initial scaling is inspired by Grisu. This is a refinement of Giulietti's algorithm that avoids the need to test two values in this phase.
Correct handling of cases where the nearest decimal is exactly at the boundary of the rounding interval relies on rounding the interval calculation wider for even significands and narrower for odd ones. This is the same technique used in earlier versions of this code.
There is no special logic for subnormals. They have smaller binary significands and the logic above naturally generates smaller decimal significands. There are however several points where we need special casing to handle asymmetric rounding intervals around exact powers of two.
Proof of correctness for Float16 and Float32 simply involved comparing the result for all 2^16 + 2^32 values between the old and new implementations. This took about 2 minutes.
Proof of correctness for Float64 and Float80 is based on an extensive set of test cases derived from the Errol paper that have proven effective in finding flaws in other implementations. (Unfortunately, the full set of 70 million test cases is too large to directly include in a Swift test suite. The Swift test suite includes a dramatically cut-down subset of those cases.)
## Future Work
It would be interesting to experiment with a Float16 formatter that used Double arithmetic throughout instead of integer arithmetic.
The fact that `Double.debugDescription` is now dominated by heap allocation means that we should explore APIs that append to an existing buffer rather than allocating from scratch.
This had to be temporarily reverted due to some test failures, but I hope to resolve those soon.
5 Likes