When formatting numbers in web development, developers often rely on sprintf('%.2f', $number) to enforce a fixed number of decimal places. However, real-world UI requirements are sometimes more complex. What if you need dynamic precision—specifically, displaying at least 3 total digits (before or after the decimal separator combined), but rounding to a whole number if the integer part already has 3 or more digits?

Consider this mapping of inputs to desired outputs:

  • 68674.27168674 (5 digits before, 0 after)
  • 9863.6839864 (4 digits before, 0 after)
  • 387.961388 (3 digits before, 0 after)
  • 68.34468.3 (2 digits before, 1 after)
  • 9.1159.12 (1 digit before, 2 after)
  • 0.7970.80 (1 digit before, 2 after)

The Solution: Dynamic Precision Calculation

Since sprintf() cannot conditionally change its precision argument based on the input value, we must calculate the required precision dynamically before passing it to the formatter.

The mathematical rule is straightforward: if the number of digits before the decimal point (let's call it $digitsBefore) is 3 or more, we want 0 decimal places. Otherwise, we want 3 - $digitsBefore decimal places.

function formatDynamic($number) {
    // Handle negative numbers by using absolute value
    $abs = abs($number);

    // Determine the number of digits before the decimal point
    $digitsBefore = ($abs < 1) ? 1 : strlen(floor($abs));

    // Calculate precision: at least 3 total digits, minimum of 0 decimals
    $precision = max(0, 3 - $digitsBefore);

    // Format using dynamic precision in sprintf
    return sprintf("%.{$precision}f", $number);
}

// Test cases
echo formatDynamic(68674.271) . "\n"; // Output: 68674
echo formatDynamic(9863.683) . "\n";  // Output: 9864
echo formatDynamic(387.961) . "\n";   // Output: 388
echo formatDynamic(68.344) . "\n";    // Output: 68.3
echo formatDynamic(9.115) . "\n";     // Output: 9.12
echo formatDynamic(0.797) . "\n";     // Output: 0.80

How This Logic Works

  • Absolute Value (abs): We convert the number to positive to prevent the minus sign from being counted as an integer digit.
  • Digits Before Decimal: If the absolute value is less than 1 (e.g., 0.797), we treat it as having 1 digit before the decimal (the leading 0). For numbers ≥ 1, we use floor() to strip decimals, convert the integer to a string, and count its length using strlen().
  • Precision Formula: Subtracting the integer digits from 3 gives us the required decimal places. Wrapping this in max(0, ...) ensures we never get a negative precision for large numbers.

Bonus: JavaScript Implementation

If you need to perform this exact formatting on the frontend, you can implement the same logical routine in JavaScript using toFixed():

function formatDynamic(number) {
    const abs = Math.abs(number);
    const digitsBefore = abs < 1 ? 1 : Math.floor(abs).toString().length;
    const precision = Math.max(0, 3 - digitsBefore);
    return number.toFixed(precision);
}

console.log(formatDynamic(68674.271)); // "68674"
console.log(formatDynamic(68.344));    // "68.3"
console.log(formatDynamic(0.797));     // "0.80"