import math from .construction_roof_pitch_types import RoofPitch, RoofType from .math_atan import atan from .math_round_float import round_float # 180 / pi as a double, written out so no language computes it differently. DEGREES_PER_RADIAN = 57.29577951308232 LIMIT = 10000 def _check_length(name: str, value: float, allow_zero: bool) -> None: ok = ( not isinstance(value, bool) and isinstance(value, (int, float)) and math.isfinite(value) and value <= LIMIT and (value >= 0 if allow_zero else value > 0) ) if not ok: want = "zero or more" if allow_zero else "greater than zero" raise ValueError( "%s must be a finite number of metres, %s and at most %d, received %r" % (name, want, LIMIT, value) ) def roof_pitch(span: float, rise: float, roof_length: float, roof_type: RoofType) -> RoofPitch: """Pitch, rafter length and slope area of a duo- or mono-pitch roof. The area is worked from the unrounded rafter, not the one rounded to a millimetre for display: over a long roof the difference shows in the second decimal place. """ _check_length("span", span, False) _check_length("rise", rise, True) _check_length("roofLength", roof_length, False) if roof_type == "duo": run = span / 2 slopes = 2 elif roof_type == "mono": run = float(span) slopes = 1 else: raise ValueError('unknown roof type "%s"' % (roof_type,)) radians = atan(rise / run) # sqrt is one of the operations IEEE 754 requires to be correctly rounded, # so math.sqrt is the same double in every language. rafter = math.sqrt(run * run + rise * rise) return RoofPitch( pitch_degrees=round_float(radians * DEGREES_PER_RADIAN, 2), rafter_length=round_float(rafter, 3), roof_area=round_float(rafter * roof_length * slopes, 2), )