[Tutor] please return flys in ointment

Dave Angel davea at davea.name
Sun Jul 7 01:30:04 CEST 2013


On 07/06/2013 06:34 PM, Dave Angel wrote:
> On 07/06/2013 05:38 PM, Jim Mooney wrote:
>> I reworked my numbers_to_name program so I can actually follow the logic.
>> Since I think the best way to learn is to follow something through to
>> completion, I'll put it online so I learn how to do that. But naturally,
>> lest I look dumb, I'd appreciate criticism, improvements of bad form,
>> or if
>> anyone can break it, let me know ;') . Seems to work with a bit of random
>> testing, but ya never know. If you did, Msoft wouldn't be putting out
>> those
>> updates all the time. Works with Py 27 or 33, but that brings up a
>> question: I think most of my programs work with both if I don't do
>> anything
>> exotic, except for input(), which is the fly in the ointment. Instead of
>> using the routine below, could I redefine input() to use the routine,
>> or it
>> that a bad idea?
>>
>> I have a feeling the leftpad routine could be simplified, but it isn't
>> coming to me.
>>
>
> General comments:
>
> 1) several lines wrapped in the newreader, so I had to paste things
> together.  Presumably this won't happen whereever you put it online, but
> you should make sure and check after uploading it to make sure no such
> problems were introduced.
>
> Also, instead of using the backslash line-continuation character, I
> generally try to use the syntax of the language instead.  So if you have
> a quote which won't fit on one line, but you don't want embedded
> newlines in it either, you can use the implicit concatenation of
> adjacent literals.  Frequently you have to put parens around the whole
> thing to make sure the compiler sees it as one line.  But I think it's
> usually more readable that way.
>
> First example:  the prompt in the input/raw_input lines.
>
>
>> # Using C:\Python33\python.exe on Win 7 in c:\python33\jimprogs - not
>> Windows-specific
>> # Also works with python 2.7
>>
>
> You always take the data in by input/raw_input.  Why not permit a value
> on the command line?  And even more important, you're mixing
> input/output with algorithm here.
>
>> import sys
>>
>> # Data
>> ones = {'1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five',
>> '6': 'six',
>> '7': 'seven', '8': 'eight', '9': 'nine'}
>>
>> tens = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty', '6':
>> 'sixty',
>> '7': 'seventy', '8': 'eighty', '9': 'ninety'}
>>
>> doubles = {'0': 'ten', '1': 'eleven', '2': 'twelve', '3': 'thirteen',
>> '4':
>> 'fourteen',
>> '5': 'fifteen', '6': 'sixteen', '7': 'seventeen', '8': 'eighteen', '9':
>> 'nineteen'}
>>
>> powers_of_1000 = (' thousand', ' million', ' billion', ' trillion',
>> ' quadrillion', ' quintillion', ' sextillion', ' septillion', '
>> octillion',
>> ' nonillion', ' decillion')
>>
>> '''add these later, and also option for dollars-and-cents ending.
>> 'vigintillion', 'novemdecillion', 'octodecillion', 'septendecillion',
>> 'sexdecillion', 'quindecillion', 'quattuordecillion', 'tredecillion',
>> 'duodecillion', 'undecillion',
>> 'decillion', 'nonillion'
>
> Those last two you've already implemented
>
>> '''
>>
>> # Functions
>> def make_triplets_from_input():
>>      '''Enter a positive integer. A list of triplets in the same order
>> will
>> be returned.
>>      Raise ValueError to loop on non-integer input, or return 'zero'
>> trigger-value of
>>      'xxx' if all zeroes are entered. If input is okay, strip leading
>> zeroes, then
>>      zero-pad leftmost triplet to three characters, so all slices are
>> triplets. Adjust
>>      input for different Python versions.'''
>>      while True:
>>          try:
>>              if sys.version[0:3] == '2.7':
>
> I'd do the version checking in initialization code, not here.  And I'd
> do it by redefining input to do what it does in 3.3
>
>>                  numbers_str = original = raw_input('Enter a positive
>> integer, space \
>> separated if desired.')
>>              elif sys.version[0:3] == '3.3':
>>                  numbers_str = original = input('Enter a positive
>> integer,
>> space \
>> separated if desired.')
>
> Note:  you explicitly support zero, so this should say  non-negative,
> rather than positive.
>
> I'm not sure why you support spaces between the digits, and not commas,
> but no biggie.  BTW, you also support other white space, like tabs.
>
>>              else:
>>                  print('Python versions 2.7 and 3.3 only supported')
>>                  sys.exit()
>>
>>              numbers_str = ''.join(numbers_str.split())
>>              if numbers_str == '' or numbers_str == ' ': raise ValueError
>>              numbers_int = int(numbers_str)
>>              if numbers_int == 0:
>>                  print('Zero')
>>                  sys.exit()
>
> This is just weird.  You're in the middle of an input routine, and you
> just print something and exit the code??
>
>
>>              numbers_str = str(numbers_int)
>>              if len(numbers_str) > 36: raise ArithmeticError
>>              break
>>          except KeyboardInterrupt:
>>              print('Program cancelled by user')
>>              sys.exit()
>>          except ValueError as err:
>>              print(original, "is not an integer.")
>>              continue
>>          except ArithmeticError as err:
>>              print(original, "is too big.\n999...decillion is max:
>> 10**37-1
>> or 36 chars \
>> or 12 groups of 3 chars")
>>
>>      leftpad = len(numbers_str) % 3 # zero-pad left, so we get all
>> 3-character triplets
>>      if leftpad == 0: leftpad = ''
>>      elif leftpad == 2: leftpad = '0'
>>      else: leftpad = '00'
>
> Instead of these four lines, how about:
>         leftpad = "0" * (-i)%3
>
>>      numbers_str = leftpad + numbers_str
>>      triplets = [numbers_str[x:x+3] for x in range(0,len(numbers_str),3)]
>>      return (triplets, len(triplets))
>
> Seems a little silly to make this a tuple, when the second value is
> trivially figured from the first.  You could just have the second
> function take a single list argument and take its len() for itself.
>
> And even then, I'd recommend using a slice when calling it instead of
> passing it a number.  Just one more bug lurking in the wings.
>>
>> def numbers_to_name(triplets, triplen):
>>      '''Create a name from each triplet and append the power of ten'''
>>      triplen -= 2
>>      number_name = ''
>>
>>      for triplet in triplets:
>>          triplet_name = ''
>>          first, second, third, last_two = (triplet[0], triplet[1],
>> triplet[2], triplet[1:])
>>          if triplet == '000':
>>              triplen -= 1
>>              continue
>>          if first > '0':
>>              if last_two == '00': # special case - snip extra space
>> separator
>>                  triplet_name += ones.get(first) + ' hundred'
>>              else:
>>                  triplet_name += ones.get(first) + ' hundred '
>>          if second == '0':
>>              if third > '0':
>>                  triplet_name += ones.get(third)
>>          elif second == '1':
>>              triplet_name += doubles.get(third)
>>          elif second > '1':
>>              triplet_name += tens.get(second)
>>              if third > '0':
>>                  triplet_name += '-' + ones.get(third)
>>          number_name += triplet_name
>>          if triplen > -1:
>>              number_name += powers_of_1000[triplen] + ', '
>>          triplen -= 1
>>      if number_name[-2] == ',':
>>          number_name = number_name[0:-2] # special case - snip extraneous
>> ending comma
>>      return number_name
>>
>> # Main Program - turn numeric input into number-names
>> triplets, triplen = make_triplets_from_input()
>> print(numbers_to_name(triplets, triplen))
>>
>
> If you included the usual if __name__ ==  test around those two lines,
> you'd be able to use this same file as an importable module.
>
> You should have four functions, not two:
>
> 1) parse argv
> 2) get input from user
> 3) turn string into list  ( make_triplets_from_input)
> 4) turn list into string, ready to print  (numbers_to_name)
>
> and then the top-level code does something like:
>
> if __name__ == "__main__":
>      if len(argv) == 2:
>          raw = parseargv()
>      elif len(argv) == 1:
>          raw = getinputfromuser()
>      else:
>             someerrormessage
>             exit
>      triplets =  make_triplets_from_input(raw)
>      print(numbers_to_name(triplets))
>
>

I see that I forgot to include the input/raw_input logic.  I'd put in 
your source, right after imports:


#No need to check version, just try to use raw_input.  If it's not defined,
#  then input should already work
try:
     input = raw_input
except NameError as e:
     pass      #we must be on version 3.3

Once those lines are executed, just pretend you're on 3.3, and use input.

In numbers_to_name(), you can start with:

     if triplets == ["000"]:
         return "zero"

which will take care of the zero case where it belongs, not in the input 
function.  Incidentally, I changed it to lower case to match the rest of 
the values.

BTW, you use the word billion for 10**9.  That's good in the US.  But in 
England at least, a billion is 10**12.  So you should qualify that your 
program is intended for US word-numbers.

With the refactoring I suggested, I now can test the code with a simple 
loop:


for i in range(100000):
     triplets = make_triplets_from_input(str(i))
     print(i, numbers_to_name(triplets))

I pipe the output into 'less' and I can examine all those values 
quickly.  I could also write a much more exhaustive test, testing 
various assertions about the results.  And of course I can trivially 
write/generate a test suite with a list of maybe 10,000 values to check, 
and use that test after every change to check for regressions.

More later.

-- 
DaveA



More information about the Tutor mailing list