6

I need some help. I'm trying to make my for loop work with decimals, but my code won't accept floats and I'm not sure what to do next. Can anyone point out where I went wrong?

It's a code used for converting Celsius to Fahrenheit in steps (Delta) that the user defines. Here it is:

def main():

    # Handshake
    print("This program will convert a range of Celsius degrees to")
    print("Fahrenheit degrees based on your input.")

    # Ask and read low end of range
    Rangelow = eval(input("Enter the low end of your range: "))

    # Ask and read top end of range
    Rangehigh = 1 + eval(input("Enter the high end of your range: "))

    # Ask and read Delta
    Delta = eval(input("Enter the Delta for your range: "))

    #Display output
    print("Celsius to Fahrenheit by", Delta)
    for i in range(Rangelow, Rangehigh, Delta):
        print(i, "               ", 9/5 * i + 32)



main()

This is an example of what I mean:

This program will convert a range of Celsius degrees to Fahrenheit degrees based on your input. Enter the low end of your range: 3.8 Enter the high end of your range: 14.7 Enter the Delta for your range: 1.1 Celsius to Fahrenheit by 1.1 Traceback (most recent call last): File "C:\Users\jarre\Desktop\Python Programs\Conversion.py", line 27, in main() File "C:\Users\jarre\Desktop\Python Programs\Conversion.py", line 22, in main for i in range(Rangelow, Rangehigh + 1, Delta): TypeError: 'float' object cannot be interpreted as an integer

I should note that the problem seems to lie with the input, the output has no issue throwing out a decimal after the input has been converted.

Jarbcd
  • 63
  • 1
  • 1
  • 6
  • 1
    When you say "my code won't accept floats", what does that mean? If there is a traceback, please edit your question and add it. – Gerrat Sep 27 '16 at 01:23
  • Check out the docs: https://docs.python.org/2/library/functions.html... `The arguments must be plain integers.` – blacksite Sep 27 '16 at 01:25
  • 1
    Side-note: Please don't use `eval` on user supplied input. If you expect `float`, then use `float` instead of `eval`. If you want to allow `int` or `float` (or any Python literal), you can use [`ast.literal_eval`](https://docs.python.org/3/library/ast.html#ast.literal_eval), which will safely handle arbitrary Python literals without opening you up to executing arbitrary code. – ShadowRanger Sep 27 '16 at 01:28
  • @ShadowRanger float only results in an error: Traceback (most recent call last): File "C:\Users\jarre\Desktop\Python Programs\Conversion.py", line 27, in main() File "C:\Users\jarre\Desktop\Python Programs\Conversion.py", line 22, in main for i in range(Rangelow, Rangehigh + 1, Delta): TypeError: 'float' object cannot be interpreted as an integer – Jarbcd Sep 27 '16 at 02:53
  • @Jarbcd: That was a side-note for a reason. Using `float` is an example of what you might accept if the rest of your code expected `float`s (it doesn't, because `range` only supports `int`). The point is that `eval` is unsafe/unstable, and you should be using something more specific to what you want. If you want `float` (probably a bad idea here; if you really want to allow decimals, you'd want `decimal.Decimal` for lossless storage and use AChampion's `decimal_range` function with it), use `float`. If you want `int`, use `int`. If you want any old Python literal, use `ast.literal_eval`. – ShadowRanger Sep 27 '16 at 02:56
  • Okay, I think I got it @ShadowRanger Instead of `range` I should use `float`? or `decimal.Decimal`? (I've only just started coding, btw) How would I implement that? I'd bet it's not just as simple as replacing `range` with `decimal.Decimal` Or would it maybe be as simple as using the code that AChampion wrote as it is? – Jarbcd Sep 27 '16 at 03:04
  • @Jarbcd: No. Instead of `float` or `eval` you'd use `decimal.Decimal`. Instead of `range`, you'd use AChampion's `decimal_range` function. – ShadowRanger Sep 27 '16 at 03:08

3 Answers3

12

You can't use the built in to do float/decimal increments but it is fairly easy to construct your own generator:

def decimal_range(start, stop, increment):
    while start < stop: # and not math.isclose(start, stop): Py>3.5
        yield start
        start += increment

for i in decimal_range(Rangelow, Rangehigh, Delta):
    ...

Or you could use numpy but this feels like a sledgehammer cracking a nut:

import numpy as np
for i in np.arange(Rangelow, Rangehigh, Delta):
    ...
AChampion
  • 29,683
  • 4
  • 59
  • 75
  • Note: This will not behave as you expect if you use it with `float`s in many cases. For example, if the arguments are `0.2, 0.3, 0.1`, you'll get one output (correctly excluding a "roughly 0.3" output). But if you pass it `0.7, 0.8, 0.1`, you'll get two outputs, for `0.7`, and `0.7999999999999...`. As the name suggests, the only safe way to use this would be to convert input strings directly to `decimal.Decimal`, not to `float`. – ShadowRanger Sep 27 '16 at 01:34
  • Yes, float rounding errors are a problem - so agree Decimal is the way to go. You could use `math.isclose()` to fix some of the errors for Py > 3.5 – AChampion Sep 27 '16 at 01:41
  • How would I implement this? I'm having a hard time figuring this out – Jarbcd Sep 27 '16 at 03:39
  • @ShadowRanger When I use the Decimal code above, all I get is an error about three missing positions – Jarbcd Sep 27 '16 at 04:02
  • @Jarbcd You may want to ask another question because without seeing what you've done - it's hard to tell what the error is. – AChampion Sep 29 '16 at 11:19
0
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import decimal

def range_decimal(start, stop, step, stop_inclusive=False):
    """ The Python range() function, using decimals.  A decimal loop_value generator.

    Note: The decimal math (addition) defines the rounding.

    If the stop is None, then:
        stop = start
        start = 0 (zero)

    If the step is 0 (zero) or None, then:
        if (stop < start) then step = -1 (minus one)
        if (stop >= start) then step = 1 (one)

    Example:
        for index in range_decimal(0, 1.0, '.1', stop_inclusive=True):
            print(index)

    :param start: The loop start value
    :param stop: The loop stop value
    :param step: The loop step value
    :param stop_inclusive: Include the stop value in the loop's yield generator: False = excluded ; True = included
    :return: The loop generator's yield increment value (decimal)
    """
    try:
        # Input argument(s) error check
        zero = decimal.Decimal('0')

        if start is None:
            start = zero

        if not isinstance(start, decimal.Decimal):
            start = decimal.Decimal(f'{start}')

        if stop is None:
            stop = start
            start = zero

        if not isinstance(stop, decimal.Decimal):
            stop = decimal.Decimal(f'{stop}')

        if step is None:
            step = decimal.Decimal('-1' if stop < start else '1')

        if not isinstance(step, decimal.Decimal):
            step = decimal.Decimal(f'{step}')

        if step == zero:
            step = decimal.Decimal('-1' if stop < start else '1')

        # Check for valid loop conditions
        if start == stop or (start < stop and step < zero) or (start > stop and step > zero):
            return  # Not valid: no loop

        # Case: increment step ( > 0 )
        if step > zero:
            while start < stop:  # Yield the decimal loop points (stop value excluded)
                yield start
                start += step

        # Case: decrement step ( < 0 )
        else:
            while start > stop:  # Yield the decimal loop points (stop value excluded)
                yield start
                start += step

        # Yield the stop value (inclusive)
        if stop_inclusive:
            yield stop

    except (ValueError, decimal.DecimalException) as ex:
        raise ValueError(f'{__name__}.range_decimal() error: {ex}')

This is a Python range() equivalent function, using decimals. The yielded values are exact.


    Rangelow = 36
    Rangehigh = 37
    Delta = 0.1
    
    print("Celsius to Fahrenheit by", Delta)
    for i in range_decimal(Rangelow, Rangehigh, Delta, stop_inclusive=True):
        print(f'{i:.1f}               {i * 9 / 5 + 32}')

Celsius to Fahrenheit by 0.1
36.0               96.8
36.1               96.98
36.2               97.16
36.3               97.34
36.4               97.52
36.5               97.7
36.6               97.88
36.7               98.06
36.8               98.24
36.9               98.42
37.0               98.6
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
cinsight
  • 1
  • 1
  • This will support the input types: integer, float, decimal.Decimal. The yielded output decimal will work well for addition, subtraction, and multiplication. Division in Python is a float, and the decimal type has different levels of support, depending on the Python version and order of precedence. – cinsight May 09 '22 at 15:14
  • {i * 9 / 5 + 32} and {9 * i / 5 + 32} will return a value. {9 / 5 * i + 32} returns a TypeError: unsupported operand type(s) for *: 'float' and 'decimal.Decimal' {9 / 5 * float(i) + 32} converts the decimal to a float and returns a value. Your computer's floating_point to binary number bit_resolution may not return the number (in base 10) that you expect. – cinsight May 09 '22 at 15:15
0

Here's an implementation of the decimal_range idea that covers both incrementing and decrementing without converting to decimal class and without bothering with a ton of validation and handling for none types etc.

It will also return the stop value itself

def decimal_range(start, stop, increment):
    """
    Like built-in range, but works for floating point numbers
    """
    current = start

    while (start < stop and current < stop) \
          or (start > stop and current > stop) \
          or decimal_is_close(current, stop, increment / 4.0):

        # optional, but useful if you really need to guarantee the stop value itself
        # is returned and not something that is just really really close
        if decimal_is_close(current, stop, increment / 4.0):
            current = stop

        yield current
        current += increment

def decimal_is_close(value, target, what_is_close):
    return abs(value - target) < what_is_close
ashbygeek
  • 759
  • 3
  • 20