Saturday, September 23, 2017

Unit Conversions.

A little background

Stephan Fitzpatrick had a blog post entitled Recursion, what is it good for? this week where a friend introduced him to a problem and he decided to solve it in a tutorial style using recursion.
I liked the question and gave an iterative solution on reddit.
I had some time, and couldn't help myself from riffing on the question and expanded on the theme.
Later on, I started to get a niggle, and started to search Rosetta Code for that question, and sure enough, the task appears, from June 2015, where I had indeed posted two Python solutions!

The Original question

On RC it begins:
Write a function or program which:
  • takes a positive integer representing a duration in seconds as input (e.g., 100), and
  • returns a string which shows the same duration decomposed into weeks, days, hours, minutes, and seconds as detailed below (e.g., "1 min, 40 sec"). ...
Not knowing I had already solved it I posted this solution on reddit

Reddit solution

In [ ]:
def say_secs(seconds):
    names = 'sec min hour day week'.split()
    modulos = [60, 60, 24, 7]
    units, remainder = [], seconds
    for mod in modulos:
        remainder, unit = divmod(remainder, mod)
        units.append(unit)
    units.append(remainder)
    plurals = [('' if count == 1 else 's') for count in units]
    out = ' '.join(f'{count} {name}{p}'
                   for count, name, p 
                     in (zip(units[::-1], names[::-1], plurals[::-1]))
                   if count)
    return out or '0 secs'


if __name__ == '__main__':
    for s in [0, 100, 66400, 172801, 987987]:
        print(f'{s:6} seconds is: {say_secs(s)}')

What's it good for?

I looked at my reddit solution and thought "I could make that work for other units by modifying the modulos name appropriately".
A quick look on Wikipedia gave me info on Imperial units of measurement and I decided to add length in inches.

Refactorings to add units of length

I was going to need to change the names and modulos variables for another system of units, that was obvious. Looking at the new names I had though made me realise that plurals can't always be made by simply adding an "s". Inch becomes inches; foot becomes feet, so I had to pull namesmodulos, and pluralsout of the function above.
This stage of the code, supporting two units of measurement became:
In [ ]:
def say_secs(seconds):
    names = 'sec min hour day week'.split()
    plurals = [f'{n}s' for n in names]
    modulos = [60, 60, 24, 7]  # next unit is x times this  for unit in names
    return say_units(seconds, names, plurals, modulos)

def say_inches(inches, progression=False):
    names = 'inch foot yard chain furlong mile'.split()
    plurals = 'inches feet yards chains furlongs miles'.split()
    modulos = [12, 3, 22, 10, 8]
    return say_units(inches,  names, plurals, modulos)

def say_units(amount, names, plurals, modulos):
    units, remainder = [], amount
    for mod in modulos:
        remainder, unit = divmod(remainder, mod)
        units.append(unit)
    units.append(remainder)
    named = [(singular if count == 1 else plural) for 
             count, singular, plural in zip(units, names, plurals)]
    out = ' '.join(f'{count} {name}'
                   for count, name
                     in (zip(units[::-1], named[::-1]))
                   if count)
    return out or f'0 {plurals[0]}'


if __name__ == '__main__':
    print('\n###')
    for s in [0, 100, 66400, 172801, 987987]:
        print(f'{s:6} seconds is: {say_secs(s)}')
    print('\n###')
    say_inches(0, progression=True)
    for i in [0, 100, 66400, 172801, 987987]:
        print(f'{i:6} inches are: {say_inches(i)}')

Progressions

Too aid me I found I added the comment on the first assignment to modulos of
# next unit is x times this  for unit in names
The above was to remind me of how each measure relates to the next. Thinking about it, I thought it would make a nice printout too so added it requirements.

Adding Volumes

I wanted to add imperial units of volume, pints and what not. I found some reference material here.
Hmm, one, two, many - that's my mantra for checking if I need to maybe create a function and loop over something if I am repeating myself.
I now have three units of measurements which would have three pretty similar sections of code in the if __name__ == ... block.
With the additional requirement of wanting to print the progressions" of the units I decided to Use a class. One instantiaon per unit of measurement; make it callable for it's main action.

Result supporting three units of measurement and using classes

In [3]:
class Say_unit():
    def __init__(self, names, plurals, modulos):
        self.names = names
        self.plurals = plurals
        self.modulos = modulos
    
    def __call__(self, amount):
        return self.say_units(amount)
    
    def say_units(self, amount):
        names, plurals, modulos = self.names, self.plurals, self.modulos
        units, remainder = [], amount
        for mod in modulos:
            remainder, unit = divmod(remainder, mod)
            units.append(unit)
        units.append(remainder)
        named = [(singular if count == 1 else plural) for 
                 count, singular, plural in zip(units, names, plurals)]
        out = ' '.join(f'{count} {name}'
                       for count, name
                         in (zip(units[::-1], named[::-1]))
                       if count)
        return out or f'0 {plurals[0]}'

    def print_progression(self):
        for i, mod in enumerate(self.modulos):
            print('  '* i + f'{mod} {self.plurals[i]} in 1 {self.names[i+1]}')
        print()

def sample_unit_conversion(title, names, plurals, modulos):
    print(f'\n### {title}')
    unit_convertor = Say_unit(names, plurals, modulos)
    unit_convertor.print_progression()
    for units in [0, 100, 66400, 172801, 987987]:
        print(f'{units:6} {plurals[0]} is: {unit_convertor(units)}')
         
    

if __name__ == '__main__':
    title = 'Seconds of time'
    names = 'sec min hour day week'.split()
    plurals = [f'{n}s' for n in names]
    modulos = [60, 60, 24, 7]
    sample_unit_conversion(title, names, plurals, modulos)
 
    title = 'Inches of length'
    names = 'inch foot yard chain furlong mile'.split()
    plurals = 'inches feet yards chains furlongs miles'.split()
    modulos = [12, 3, 22, 10, 8]
    sample_unit_conversion(title, names, plurals, modulos)

    title = 'Fluid ounces of volume'
    names = 'fluid ounce, gill, cup, pint, quart, gallon, peck'.split(', ')
    plurals = [f'{n}s' for n in names]
    modulos = [5, 2, 2, 2, 4, 2]
    sample_unit_conversion(title, names, plurals, modulos)
    
    
### Seconds of time
60 secs in 1 min
  60 mins in 1 hour
    24 hours in 1 day
      7 days in 1 week

     0 secs is: 0 secs
   100 secs is: 1 min 40 secs
 66400 secs is: 18 hours 26 mins 40 secs
172801 secs is: 2 days 1 sec
987987 secs is: 1 week 4 days 10 hours 26 mins 27 secs

### Inches of length
12 inches in 1 foot
  3 feet in 1 yard
    22 yards in 1 chain
      10 chains in 1 furlong
        8 furlongs in 1 mile

     0 inches is: 0 inches
   100 inches is: 2 yards 2 feet 4 inches
 66400 inches is: 1 mile 3 chains 18 yards 1 foot 4 inches
172801 inches is: 2 miles 5 furlongs 8 chains 4 yards 1 inch
987987 inches is: 15 miles 4 furlongs 7 chains 10 yards 3 inches

### Fluid ounces of volume
5 fluid ounces in 1 gill
  2 gills in 1 cup
    2 cups in 1 pint
      2 pints in 1 quart
        4 quarts in 1 gallon
          2 gallons in 1 peck

     0 fluid ounces is: 0 fluid ounces
   100 fluid ounces is: 2 quarts 1 pint
 66400 fluid ounces is: 207 pecks 1 gallon
172801 fluid ounces is: 540 pecks 1 fluid ounce
987987 fluid ounces is: 3087 pecks 3 quarts 1 pint 1 gill 2 fluid ounces
We have a unit of volume that is two words fluid ounce so I had to make a slight change of its name string initial value and splitting. Say_unit.print_progression is new, and function sample_unit_conversion cuts down on the cut-n-paste coding.
END.
In [ ]:
 

1 comment:

  1. Hi Paddy
    Sis here.
    Just started to do some programming myself.
    Well laid out and the thought processes you went through make it very readable, even for a beginner.
    Not able to follow all of it as yet, but I get the general jist.

    ReplyDelete