3
\$\begingroup\$

I want to build a function that, given an area size (target), returns a list of three lengths. The last item of the list (my_list[-1]) corresponds to the square plate.

The first two lengths in the list correspond to an orthogonal plate whose length is approximately 1.5 times larger than the one of the square plate and whose width is 1.5 times smaller. I want the lengths to be whole numbers.

The function I wrote for the task is given below, but I am sure there is a much better/faster solution. Keep in mind that when it comes to rounding floats, the bigger size for the length must be chosen since it is more conservative, but the main consideration is to not overshoot the target more than I have to.

import math


def get_plates(target):
    square = math.ceil(target ** 0.5)
    ortho1 = [math.ceil(target ** 0.5 * 1.5), math.floor(target ** 0.5 * 1.5)]
    ortho2 = [math.ceil(target ** 0.5 / 1.5), math.floor(target ** 0.5 / 1.5)]
    temp = list(zip([ortho1[0] for _ in ortho2], ortho2))
    temp.extend(list(zip([ortho1[1] for _ in ortho2], ortho2)))
    temp = [x for x in temp if x[0]*x[1] >= target]
    temp.sort(key=lambda x: abs(x[0] * x[1] - target))
    return [*temp[0], square]

For example, for a target = 700 the get_plates(700) correctly returns [39, 18, 27] and not [40, 18, 27] since 39x18 = 702 whereas 40x18 = 720 and 40x17 = 680

How can the above be made more efficient?

\$\endgroup\$
4
\$\begingroup\$

You don't need your temp list, you can just use itertools.product to build it:

>>> list(itertools.product(ortho1, ortho2))
[(41, 18), (41, 17), (40, 18), (40, 17)]

Also, you should use some more descriptive names.

You can use the fact that tuples are also iterable and can be written like so: t = 0, 1, saving you the unneeded list brackets.

You can also compute the square root of target once and save it.

You should add a docstring describing what your function does.

I would separate the two concerns of your function. One is to find the side length of a square most closely matching target, the other one is finding a rectangle with ratio about 1:2.

You can use min and re-use your key function for that, since you only need the pair with the smallest distance and don't care about all others. min is \$\mathcal{O}(n)\$, whereas sort is usually \$\mathcal{O}(n \log n)\$. The abs is also not needed anymore, since we made sure that only those pairs with a product greater or equal the target are still left.

With this, your code becomes:

import math
import itertools


def get_plates_square(target):
    """Returns the integer side length of a square with an area at least `target`."""
    return math.ceil(target ** 0.5)


def get_plates(target):
    """
    Return a rectangle with integer sides with a ratio of about 2:1,
    which is closest to the given `target` area.
    """
    target_sqrt = target ** 0.5
    sides_a = math.ceil(target_sqrt * 1.5), math.floor(target_sqrt * 1.5)
    sides_b = math.ceil(target_sqrt / 1.5), math.floor(target_sqrt / 1.5)
    rectangles = [(a, b) 
                  for a, b in itertools.product(sides_a, sides_b)
                  if a * b >= target]
    return min(rectangles, key=lambda x: x[0] * x[1] - target)
\$\endgroup\$
  • \$\begingroup\$ I would keep them both in the same function since they would never be needed separately but apart from that I am going to adopt your code. Thanks! P.S: import itertools \$\endgroup\$ – Ev. Kounis Feb 6 '17 at 12:12
  • \$\begingroup\$ @Ev.Kounis Fixed, thanks. Should have checked it still works after the last changes :D Regarding the separation: Of course it depends on your use case, but two functions is more modular, and adheres better to the single responsibility principle. It is also more re-usable. But in the end, if you only ever use them together, do what works best! \$\endgroup\$ – Graipher Feb 6 '17 at 12:51
4
\$\begingroup\$

It's just a rounding problem

The problem can be solved with standard mathematical operations:

(Code edited to include Graipher's suggestions.)

from math import floor, ceil, sqrt

def get_plates(n):
    short_side = round(sqrt(n / 2.25))
    long_side = floor(n / short_side)
    if short_side * long_side < n:
        long_side += 1
    return [long_side, short_side, ceil(sqrt(n))]

How does it work?

To get the closest approximation to the answer, rounding is applied to the short side-length first. This is done because any rounding applied to the short side will affect the end product more than rounding of the long side by the same amount.

Then, you stated that you need the final product area to be at least as large as the target area. The conditional if short side... achieves this by only rounding the long side down if it doesn't produce an answer less than the target.

The resulting area is always the closest to the original target achievable without undershooting the target.

(The magic number \$2.25\$ is just \$1.5^2\$, which is the desired aspect ratio for the rectangle.)

A note about long expressions

PEP8 gives suggestions for breaking up long, single-line expressions into multiple lines, aided by the fact that Python expressions are not white-space sensitive, if they are contained between any kind of brackets. There is a good example of this in Graipher's answer. The technique might also neaten the appearance of your ortho1 = and ortho2 = lines.

\$\endgroup\$
  • \$\begingroup\$ One last suggestion. You could use integer division instead of math.floor and do long_side = n // short_side. \$\endgroup\$ – Graipher Feb 6 '17 at 20:22
  • \$\begingroup\$ Good point. Though sometimes it's helpful to a reader to use floor when that is the desired behaviour. Especially in code that already uses ceil to go the other way. \$\endgroup\$ – Ryan Mills Feb 6 '17 at 22:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.