BIM
CODE
-2022-

Architectural Units <-> Decimal Feet

Converting between the two in python without imports

I made a simple and configurable AV tool. It displays height & width, minimum viewing distance and more based on screen size class and resolution class.But that isn't very interesting, really just an exercise in creating a user experience in TKinter. What maybe is interesting, is that in making it, I was briefly consumed with this more generalizable sub-task of working with imperial and architectural units.

Specifically I want to be able to go back and forth between architectural units and decimal feet in a configurable way, and I wanted to do it without any imports.

What I'm calling architectural units is lengths given in the format of

1'-2 3/4"

That means one foot plus two inches plus three quarters of one inch. But if you're processing keyboard input, so should these:

#Put the dash in the wrong place
1' 2-3/4"

# Call the units something else
1ft 2 3/4inches

# Omit units altogether
1 2 3/4

Going from decimal feet to architectural units is easier than the other way around. If you're learning python, you should tackle this first as I think it illuminates the way to do the reverse. Here's how I did it. It's a pretty readable (read: naïve) solution:

def arch_units(num, max_denominator=16):
    # Special case for omitting fraction in output
    if max_denominator == 0:
        max_denominator = 1

    # Calculate values
    ft = math.floor(num)
    inch = math.floor((num - ft) * 12)
    error = (num - ft - (inch / 12)) * 12
    frac = round(error * max_denominator)
    error = (num - ft - (inch / 12) - (frac / (12 * max_denominator))) / (12 * max_denominator)

    # Carry the one
    if frac == max_denominator:
        inch = inch + 1
        frac = frac - max_denominator

Then just build the output string how you like

    # Build output string
    output = []

   # Output format
    ft_display = f"{ft}' "
    inch_display = f'{inch}'
    frac_display = f'{frac}/{max_denominator}'


    if ft != 0:
        output.append(ft_display)
    if inch != 0:
        output.append(inch_display)
    if inch != 0 and frac != 0:
        output.append('-')
    if frac != 0:
        output.append(frac_display)
    if frac + inch != 0:
        output.append('"')
    sep = ""
    output = str(sep.join(output))
    return output

And then here is where I do the opposite:

def arch2dec(s):  ## input is any common format for imperial length measurement, ex: 6'4-/4", 10.5 mi, 8f oot 1
    s = s.strip()  # trim whitespace
    s = s.replace('-', ' ')
    indices = []
    n = 0
    mode = 'U'  # mode is U so first number increments n counter

Setting things up

    for i, c in enumerate(s):  # check every character
        number_bool = c.isnumeric() or c == '.'  # is it a number or '.'
        if mode == 'N':  # if we're reading a number
            if not number_bool:  # and we find a non-number
                mode = 'U'  # switch to reading a unit
                indices.append(i)  # and record the index of where this happened
        if mode == 'U':  # if we're reading a unit
            if number_bool:  # and we find a number
                mode = 'N'  # switch to reading a number
                n = n + 1  # increment the number of number-unit pairs we're dealing with
                indices.append(i)  # and record the index of where this happened
    indices.append(len(s))
    # print(i, s[i], mode)
    phrases = []
    #   Create phrases of entire number strings and strings of whatever is between them:
    for a, b in zip(indices, indices[1:]):
        phrase = s[a:b]
        phrase = phrase.strip()
        phrases.append(phrase)  # make a list of phrases
    #   If a non-number phrase is recognized as an alias for a unit, replace it with that unit
    phrases = [aliases.get(item, item) for item in phrases]  # lookup units
    phrases = [phrase for phrase in phrases if phrase != '']  # remove empty items
    # print(phrases, n)
    #   Find fractions by looking for '/ and evaluating it as division:
    for i in phrases:
        if i == '/':
            div_index = phrases.index('/')
            numerator = float(phrases[div_index - 1])
            denominator = float(phrases[div_index + 1])
            phrases[div_index - 1] = numerator / denominator
            del phrases[div_index]
            del phrases[div_index]
            n = n - 1
    #   Create separate lists for numbers and non-numbers:
    numbers = []
    factors = []
    total = 0
    #   Create and sum components
    for i, phrase in enumerate(phrases):
        d = (i, phrase)
        if isfloat(phrase):
            numbers.append(d)
        else:
            factors.append(d)
    # print(f'number, factors: {numbers}, {factors}. n = {n}')
    #   If list of units is empty, assume numbers are presented as feet>inches>inches
    #   example: 6 4 1/2 is interpreted as 6' 4-1/2"
    if not factors:
        # print('no units')
        for i, number in enumerate(numbers):
            total = total + unit_order[i] * float(number[1])
        return total
    #   Units affect numbers that are before themselves but after the previous unit,
    #   and numbers after themselves if they are the last unit
    step = 0
    multipy = 1
    for j, number in numbers:
        component = 0
        for index, factor in enumerate(factors):
            i, f = factor
            f = units[f]
            if step <= j < i:
                multipy = f
            # print(f'i={i} j={j}')
            # print(f'value is {number}, unit is {factor}, factor is,{f}')
            step = i
        component = component + float(number) * float(multipy)
        # print(f'component: {number} {factor[1]} * {multipy}')
        total = total + component
    return total
-Published 8 pm Tue, Feb 7 2023-