Problem 673864 · medium · Level 06 Heuristics & Optimization

Centimetres, Inches and Everything in Between

py-descriptors · py-classes · units of measurement · parsing · validation

A shipping service mixes measurements from many sources: "30 cm" from one form, (12, "in") from another, a bare number from a third. Each class should say once which unit each attribute is kept in, and every assignment should be converted to that unit automatically.

The setup defines the table UNITS, mapping each dimension ("length", "mass", "time") to its units and their size in the base unit (UNITS["length"]["cm"] == 0.01 metres). Write a descriptor class Quantity(dimension, unit), used like this:

class Parcel:
    length = Quantity("length", "cm")
    weight = Quantity("mass", "kg")
  • Creating a Quantity whose unit is not a unit of its dimension (or whose dimension is unknown) raises ValueError.
  • An assigned value may be a number (int or float, not bool), taken to be in the attribute's own unit; a string made of a number, whitespace and a unit, such as "2.5 ft"; or a tuple (number, unit). Strings and tuples are converted from their unit to the attribute's unit.
  • A unit of another dimension, an unknown unit, a string that is not of that form, or a negative amount raises ValueError; a value of any other type raises TypeError. Every such message starts with the attribute's name, and a refused assignment keeps the previous value.
  • Reading the attribute gives a float in the attribute's unit. Reading it on the class gives the descriptor, which has the attributes name, dimension and unit.

The tests build classes with your descriptor in setup helpers you can call with Run: parcel_class(), run_class(), parcels(rows), runs(rows), described(), keep_on_error() and random_parcels(n, seed). outcome(call) returns a result or the exception's name and the first word of its message.

Examples

Input:  parcels([(30, "0.2 m", (4, "in"), "1500 g"), (10, 10, "1 km", 1), (10, 10, 10, "2 m"), (10, 10, 10, [1, "kg"])])
Output: [(30.0, 20.0, 10.16, 1.5, 6.096), (10.0, 10.0, 100000.0, 1.0, 10000.0), ("ValueError", "weight"), ("TypeError", "weight")]

Input:  runs([("10 km", "50 min"), ((5, "mi"), "0.75 h")])
Output: [(10.0, 50.0, 5.0), (8.04672, 45.0, 5.592341997)]

Constraints

  • Floats are compared with a relative tolerance of 1e-6.
  • Parse a string by splitting it on whitespace into exactly two parts; the number part is anything float() accepts.

Goals

  • Write a parameterised descriptor that converts values as they are assigned
  • Accept several input forms (a number, a string, a pair) and refuse the rest with clear errors
  • Use the descriptor on several classes with different units for the same dimension
Starting Python…