How can I get the fractional part of a float only?

So I have a float and I am interested in splitting it to an integer and the fractional part:

{3, 0.14} = magic_function(3.14)

Is there such a thing somewhere in Elixir? If not, I’d guess it should be done like this:

def split_float(f) when is_float(f) do
  i = trunc(f)
  {i, f - i}
end

That however runs into the normal phenomena of non-intuitively showing a slightly different fractional part in the end result.

Is there a better way?

1 Like

Can you give an example of an edge case that you care about? (I used to work in modeling floating points)

:thinking:

Make it a string and then parse it as such would work in your use case? At least you won’t get the rounding errors.

You’ll just get different rounding errors.

3 Likes

Don’t you always get rounding errors with floats?

4 Likes

Seems to work for up to 16 decimal points.

iex(31)> Float.to_string(0.14000000000000001)
"0.14"
iex(32)> Float.to_string(0.1400000000000001) 
"0.1400000000000001"
iex(33)> 

Anyway, floats are devil :smiling_imp:

1 Like

Yep. Just realised that. :laughing:

That’s even more inefficient than mine!

Yes, sometimes you will not get rounding errors, which is always the case with floats.

Nothing special at all. In this case I just wanted to be able to get a UNIX time with a fractional part and convert it into a NaiveDateTime to the microseconds:

time = 1485425805.49934
fraction = 0.49934

time
|> trunc()
|> DateTime.from_unix!()
|> NaiveDateTime.to_erl()
|> NaiveDateTime.from_erl!(trunc(fraction * 1_000_000))

~N[2017-01-26 10:16:45.499340]

I was just looking for a way to have {unix_seconds, fraction} = magic_function(fractional_unix_seconds), that’s all.

Where’s your unix time with fractional part coming from? The unix APIs return structs with secs & microsecs to begin with. Postgres stores as microsecs. Etc. You might just need to get your initial time from a different API, if possible. Of course if it’s being handed to you by an external library, then your first idea is probably still best.

1 Like

Legacy WordPress database. People put all kinds of stuff in there, you wouldn’t believe the things I’ve found in production dumps while I was troubleshooting (years ago). Including PHP-serialized objects (which is a TLV string-encoded format).

Well alrighty then!

1 Like

Did you have an example? I don’t think there’s any case where you can subtract the integer part and get a value with less precision.

Not less precision, just not exactly the same number, you know:

time = 1485425805.49934
time - trunc(time)
0.4993400573730469

I am aware why this is, was just wondering if there’s a better way than the one I posted in OP.

Seems that Decimal is to the rescue again:

  def split_float(f) when is_float(f) do
    i = trunc(f)

    {
      i,
      Decimal.sub(
        Decimal.from_float(f),
        Decimal.new(i)
      )
      |> Decimal.to_float()
    }
  end

And finally, the function that made me go on this search, is working quite well:

  def datetime_from_fractional_unix_seconds(f) when is_float(f) do
    {unix_seconds, useconds} = split_float(f)

    unix_seconds
    |> DateTime.from_unix!()
    |> NaiveDateTime.to_erl()
    |> NaiveDateTime.from_erl!(trunc(useconds * 1_000_000))
  end

REPL:

iex> time = 1485425805.49934
1485425805.49934
iex> split_float(time)
{1485425805, 0.49934}
iex> datetime_from_fractional_unix_seconds(time)
~N[2017-01-26 10:16:45.499340]
4 Likes

oh yeah. That’s actually numerically correct behavior. Strictly cosmetically bad for most purposes. No claims are made about if fluid dynamics simulations will or won’t be messed up by this.

For anyone else tuning in who is confused by this:

time = 1485425805.49934
time - trunc(time)
0.4993400573730469

it’s because 148542580549934/100000 has trimmed precision that isn’t the same as when you do 49934/100000 because there’s more precision with IEEE floating point around [0,1]

1 Like

pretty sure that the rounding error will be small enough that trunc((time - trunc_time) * 1_000_000) would also give you what you want

1 Like

True. I simply wanted to chase this to some as-much-satisfactory-as-possible conclusion. :slight_smile:

(I mean, in terms of trying to write as correct as possible code. But yeah, binary floating-point will never be viewed as correct by us the humans who are used to the decimal system. Hence we never use binary floats for financial operations.)