Rounding Question

Alex Martelli aleaxit at yahoo.com
Wed Feb 21 09:31:50 CET 2001


"Jacob Kaplan-Moss" <jacobkm at cats.ucsc.edu> wrote in message
news:jacobkm-07145C.22285820022001 at news.ucsc.edu...
> Hello All --
>
> So I've got a number between 40 and 130 that I want to round up to the
> nearest 10.  That is:
>
>    40 --> 40
>    41 --> 50

So, not really to the *nearest* 10, but to the *least multiple of 10
that is >= the original number*.  OK.  An *excellent* not-quite-toy
example on which to hinge some general-interest discussion, IMHO...


> >>> import math
> >>> depth = <<whatever>>
> >>> tableDepth = int(math.ceil( depth/10.0 ) * 10)
>
> (NAUI divers will recognize me translating from actual depth to a table
> depth entry)
>
> This works just fine for my purposes, but its somewhat counterintuitive;
> I wonder if there is a more self-explanatory way...

It is sometimes hard to say what people will find "intuitive" or
"self-explanatory", but, what about:

    tableDepth = (depth/10) * 10
    if tableDepth < depth: tableDepth += 10

The first step insures that tableDepth is a multiple of 10, <= depth;
the second step changes the < part of it into a >, as required.

People who are allergic to one-line if-statements might perhaps
want to change the second step to:

    tableDepth += 10*(tableDepth<depth)

but that seems slightly less intuitive to me, as it depends on
the fact that '<' returns 0 or 1, rather than just on its true/
false behavior which is what is normally required; similarly "not
really all that clear" (IMHO) are alternative expressions such as

    tableDepth += (0,10)[tableDepth<depth]

or

    tableDepth += tableDepth<depth and 10

although the latter does go back to the simpler true/false idea,
the subtlety it depends on is the fact that Python's "and" does
NOT just return true/false (like C's "&&", which it resembles
in other ways), but, rather, its RHS operand if its LHS one is
true, else its (false) LHS operand (without, in this latter
case, evaluating its RHS, but that's immaterial in this case).

I would not consider breaking the if-statement into two lines
a plus, either -- it's bad enough that all of these ideas
split a single concept into two steps, without making it
appear like THREE steps are somehow involved:-).


Getting back to ONE concept for any of these forms is not
hard at all, but, again, intuitiveness is in the eye of
the beholder.  Take, for example, the latter form:
    tableDepth = (depth/10) * 10
    tableDepth += tableDepth<depth and 10
how do we map this back into one expression? I think the
clearest conceptual transformation relies on recognizing
that an intermediate variable is involved, although we
chose to name it "tableDepth" (the same as the final target
value...):
    temp = (depth/10)*10
    tableDepth = temp + (temp<depth and 10)
and we can expand 'temp' to its defining expression in
the second step:
    tableDepth = (depth/10)*10 + ((depth/10)*10<depth and 10)

This may not look like much of a progress, but it does
enable us to recognize that the key test:
    (depth/10)*10<depth
means "is depth a multiple of 10"...?  And we can
express THAT one more directly:
    tableDepth = (depth/10)*10 + (depth%10 and 10)
and of course we could factor out the 10 multiplier:
    tableDepth = 10 * ((depth/10) + (depth%10 and 1))
or
    tableDepth = 10 * ((depth/10) + depth%10 != 0)


Whenever we have a division-with truncation, AND a
modulo-operation, with the same operands, the builtin
divmod function always comes to mind, leading to such
further alternative expressions as:
    quotient, remainder = divmod(depth, 10)
    if remainder: quotient += 1
    tableDepth = quotient * 10
or, trying to gain back some compactness for the
last two statements,
    tableDepth = (quotient + (remainder!=0)) * 10
which is really similar to the last expression in
the previous paragraph.


But the most classic way to express this would be:
    tableDepth = 10 * ((depth+9)/10)
which also happens to be just about the most compact
and concise one; its "intuitiveness" is probably
dependent on having seen it a zillion times before:-).

Deep down, it does depend on some moderately subtle
theorems of modulo-arithmetic, but familiarity does
have a way to compensate for that.  So, the "eye
of the beholder" effect may dominate (it's hard for
me to judge the actual "clarity" of something I have
written or read so many thousands of times in the
past).

Depending on your audience, a simple comment or
docstring:
    # set tableDepth to the least multiple of 10 >= depth
may suffice to make any or all of these expressions
acceptable (this is a controversial statement, as a
school exists that says too-complex code must be made
simpler, rather than explained in a comment, since
comments have a nasty way to get out of sync with code,
etc; however, there are arguments on both sides when
something has a certain "minimum irreducible intrinsic
complexity" and particularly when that "complexity" is
actually more of a *subtlety*...).

So, all in all, I would suggest avoiding the floating
point maths that your original approach used -- your
intuition, IMHO, was *QUITE* on target suggesting that
such an approach is sub-optimal... the complexities of
FP, after all, make the subtleties of modulo arithmetic
pale in comparison!  In the end, something like:

def depthToTableDepth(depth):
    "compute the least multiple of 10 >= depth"
    return int((depth+9)/10) * 10

may be the best compromise.  The 'int' call will be
redundant when depth is an integer, but it does not
hurt, and may in fact clarify the code by reminding
the reader that we are using TRUNCATING division --
and it will let this work unaltered if and when
depth should become a FP number at some later time
in the program's lifecycle, just like your original
FP-based expression would keep working then.  One
should not introduce unwarranted generality when
it (as is often the case) complicates things (the
important "you ain't gonna need it" principle), but,
at times, higher generality actually CLARIFIES and
SIMPLIFIES things wrt a more targeted/ad-hoc approach;
and I opine we're in such a case here, with the
clarity coming from the _explicitness_ of 'int()'.


Note that putting this into a function of its own,
rather than inline code, may often be best (although,
here, that's not wholly clear).  A further _delicate_
judgment decision is whether generalizing from '10'
is a gain or loss in clarity...:

def nextHigherOrEqualMultiple(depth, N):
    "compute the least multiple of N >= depth"
    return int((depth+N-1)/N) * N

with

tableDepth = nextHigherOrEqualMultiple(depth, 10)

I *think* it would be a substantial gain *IF* a name
that is both clear AND concise could be found for the
function, but not if we DO have to use a 25-characters
name for clarity:-).


Alex






More information about the Python-list mailing list