5

tl;dr

I'm just looking for two functions, f from double to string and g from string to double, such that g(f(d)) == d for any double d (scalar and real double).

Original question

How do I convert a double to a string or char array in a reversible way? I mean, in such a way that afterward I can convert that string/char array back to double retrieving the original result.

I've found formattedDisplayText, and in some situations it works:

>> x = eps

x =

     2.220446049250313e-16

>> double(formattedDisplayText(x, 'NumericFormat', 'long')) - x

ans =

     0

But in others it doesn't

x = rand(1)

x =

   0.546881519204984

>> double(formattedDisplayText(x, 'NumericFormat', 'long')) - x

ans =

     1.110223024625157e-16

As regards this and other tools like num2str, mat2str, at the end they all require me to decide a precision, whereas I would like to express the idea of "use whatever precision is needed for you (MATLAB) to be able to read back your own number".

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • `mat2str` is supposed to do exactly what you want, it can even preserve the numeric type. You don’t need to specify a precision, you shouldn’t specify a precision. – Cris Luengo Sep 28 '21 at 14:24
  • @CrisLuengo, and what is its reverse operation? `str2double(mat2str(eps)) - eps` is not zero, so `str2double` is not good for reading that string back to the number it came from. The doc reads _`A = eval(chr)` reproduces the values from the original matrix to the precision specified in `chr`_, but `eval(mat2str(eps)) - eps` is also not zero. – Enlico Sep 28 '21 at 14:29
  • Hummm, interesting. You should report that as a bug, since it's not what the documentation describes. – Cris Luengo Sep 28 '21 at 15:26
  • It seems like it uses one digit too few. `eval(mat2str(eps,16))-eps` works correctly. 16 digits is enough for every possible double value. If you are dealing with values of other types, it'll get more complex. `mat2str` was supposed to take that complexity away. – Cris Luengo Sep 28 '21 at 15:27
  • Type `edit mat2str` in MATLAB, and you can see the implementation. At the top it says `precision = 15;`. This should have been 16. Pretty careless. You can copy and modify the M-file to suit your needs. – Cris Luengo Sep 28 '21 at 15:30
  • @CrisLuengo, based on what it should have been 16? I guess yours is getting close to be an answer :/ – Enlico Sep 28 '21 at 15:52
  • I was wrong, it needs to be 17 decimal digits: "The 53-bit significand precision gives from 15 to 17 significant decimal digits precision (2−53 ≈ 1.11 × 10−16). [...] If an IEEE 754 double-precision number is converted to a decimal string with at least 17 significant digits, and then converted back to double-precision representation, the final result must match the original number." https://en.wikipedia.org/wiki/Double-precision_floating-point_format – Cris Luengo Sep 28 '21 at 16:08
  • 1
    Any 15-digit decimal number can be converted to double precision float without loss. Maybe this is why `mat2str` uses 15 there, someone got confused. – Cris Luengo Sep 28 '21 at 16:10
  • 1
    Would this trick work for you? (I don't know your use case). It uses the internal byte representation of `x`: `str = ['typecast(uint8(' mat2str(typecast(x, 'uint8')) '),''' class(x) ''')']`. Check with `eval(str)-x` – Luis Mendo Sep 28 '21 at 16:49
  • Also, do you need a string/char vector that when _`eval`_'ed gives the number? Or are you more flexible with the function that should be applied to the string to produce the number? – Luis Mendo Sep 28 '21 at 17:44
  • @LuisMendo, I'm just looking for two functions, `f` from `double` to `string` and `g` from `string` to `double`, such that `g(f(d)) == d` for any `double` `d`. – Enlico Sep 28 '21 at 17:53
  • Then `eval` can be avoided: As `f` use `str = mat2str(typecast(x, 'uint8'));`. As `g` use `x_recovered = typecast(uint8(str2num(str)), 'double');`. Let me know if this suits your needs so I can post it as an answer – Luis Mendo Sep 28 '21 at 18:00
  • 2
    Also, does the answer need to handle complex-valued `x`? Or non-scalar `x`? (I suggest you edit your question to include that information) – Luis Mendo Sep 28 '21 at 18:05

2 Answers2

4

Here are two simpler solutions to convert a single double value to a string and back without loss.

I want the string to be a human-readable representation of the number

Use num2str to obtain 17 decimal digits in string form, and str2double to convert back:

>> s = mat2str(x,17) 
s =
    '2.2204460492503131e-16'
>> y = str2double(s);
>> y==x
ans =
  logical
   1

Note that 17 digits are always enough to represent any IEEE double-precision floating-point number.

I want a more compact string representation of the number

Use matlab.net.base64encode to encode the 8 bytes of the number. Unfortunately you can only encode strings and integer arrays, so we type cast to some integer array (we use uint8 here, but uint64 would work too). We reverse the process to get the same double value back:

>> s = matlab.net.base64encode(typecast(x,'uint8'))
s =
    'AAAAAAAAsDw='
>> y = typecast(matlab.net.base64decode(s),'double');
>> x==y
ans =
  logical
   1

Base64 encodes every 3 bytes in 4 characters, this is the most compact representation you can easily create. A more complex algorithm could likely convert into a smaller UTF-8-encoded string (which uses more than 6 bytes per displayable character).

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
3

Function f: from double real-valued scalar x to char vector str

str = num2str(typecast(x, 'uint8'));

str is built as a string containing 8 numbers, which correspond to the bytes in the internal representation of x. The function typecast extracts the bytes as a numerical vector, and num2str converts to a char vector with numbers separated by spaces.

Function g: from char vector str to double real-valued scalar y

y = typecast(uint8(str2double(strsplit(str))), 'double');

The char vector is split at spaces using strsplit. The result is a cell array of char vectors, each of which is then interpreted as a number by str2double, which produces a numerical vector. The numbers are cast to uint8 and then typecast interprets them as the internal representation of a double real-valued scalar.

Note that str2double(strsplit(str)) is preferred over the simpler str2num(str), because str2num internally calls eval, which is considered evil bad practice.

Example

>> format long
>> x = sqrt(pi)
x =
   1.772453850905516
>> str = num2str(typecast(x, 'uint8'))
str =
    '106  239  180  145  248   91  252   63'
>> y = typecast(uint8(str2double(strsplit(str))), 'double')
y =
   1.772453850905516
>> x==y
ans =
  logical
   1
Luis Mendo
  • 110,752
  • 13
  • 76
  • 147