Implement float number truncation

Hi,

I had to deal with float numbers in one of my Nix related projects and found out that float support in the Nix libraries (both builtin and from nixpkgs) is very limited. Same goes for integers but the support is slightly better than for floats.

For example, there’s no function to get the power of a number, so I had to make my own (getScalingFactor is a function to get the power of 10, but it could be slightly modified to get the power of any number).

In the end, I came up with a working solution so I thought it could be useful to share it. If anyone feels something could be done differently or be simpler, I’m all ears!

The goal of the function below is to take a float and truncate it at a given number of decimals.

# Truncate a floating-point number to a specified number of decimal places
# This performs truncation (toward zero) rather than rounding to nearest
#
# Examples:
#   truncateFloat 1.379 2 → 1.37 (truncates 0.009)
#   truncateFloat 3.14159 3 → 3.141 (truncates 0.00059)
#   truncateFloat (-2.789) 1 → -2.7 (truncates toward zero)
#
# Type: Float → Int → Float
float: precision:
let
  # Locate the position of the decimal separator in a numeric string representation
  # Returns the zero-based index of the decimal point character
  # Type: String → Int
  findDecimalPointIndex =
    numericString:
    lib.lists.findFirstIndex (character: character == ".") ((builtins.stringLength numericString) - 1) (
      lib.stringToCharacters numericString
    );

  # Calculate the scaling factor (power of 10) for the specified decimal precision
  # Returns 10^precision for shifting decimal point during truncation
  # Type: Int → Int
  getScalingFactor =
    precision: builtins.foldl' (accumulator: x: accumulator * x) 1 (builtins.genList (_: 10) precision);

  # Reconstruct a floating-point number from its truncated integer representation
  # Type: Int → Int → Float
  intToFloatAtPrecision =
    int: precision:
    let
      scalingFactor = getScalingFactor precision;
    in
    ((int + (1.0 / scalingFactor)) / scalingFactor) - (1.0 / (scalingFactor * scalingFactor));

  # Extract the integer part from a float string
  # Performs truncation by discarding fractional component
  # Type: String → Int
  floatStringToInt =
    floatString: lib.toInt (lib.substring 0 (findDecimalPointIndex floatString) floatString);

  # Convert float to string representation after scaling by precision factor
  # This shifts the decimal point right by decimalPlaces positions
  # Example: 1.379 with precision 2 becomes "137.900000"
  # Type: Float → Int → String
  toStringFloat = float: precision: lib.strings.floatToString (float * (getScalingFactor precision));

in
intToFloatAtPrecision (floatStringToInt (toStringFloat float precision)) precision

Implementation-wise, the process is to shift the decimal point right by a given number (precision), truncate the result into an integer and shift back the decimal to get the truncated float.

The function could use some more safeguards like validating the inputs with isFloat and isInt. It also doesn’t append zeros if the requested precision is greater than the float input.

Note for moderators: I’m not sure where to put this so feel free to move it around if needed!

Since Nix 2.4, you can sort of get by with floating point numbers because conversions from floats to integers are possible via floor and ceil.

let
  # needs Nix >= 2.4
  trunc = f: if f < 0 then builtins.ceil f else builtins.floor f;
  pow = b: n: builtins.foldl' builtins.mul 1 (builtins.genList (_: b) n);
  truncateFloat = f: p: trunc (f * pow 10 p) / pow 10.0 p;
  #                   force conversion back to float ^
in

[
  (truncateFloat 1.379 2)    # 1.37
  (truncateFloat 3.14159 3)  # 3.141
  (truncateFloat (-2.789) 1) # -2.7
]
3 Likes

Oh that’s smart, I didn’t thought searching for ceil/floor functions. And it’s far much simpler than my solution!

I’ll borrow it if you don’t mind!

Thanks :slight_smile:

1 Like