!

Dette materialet blir ikke lenger vedlikeholdt. Du vil finne oppdatert materiale på siden: http://borres.hiof.no/wep/

pyUnit
Testing
Børre Stenseth
Kodetesting > Pythontesting >Snart

Beregning av avstand i tid

Hva
Avstand i tid, en webtjeneste

Jeg vil lage en webtjeneste som returnerer hvor mange dager det er mellom to datoer. Løsningen skal være enkel og den skal nåes via en URL + parametere. Det eneste som skal returneres fra tjenesten er et tall, antall dager. Eller dersom feil : ?. Jeg vil ha tre former for beregninger:

  • until, typisk bruk:<url>?until&date=2007-12-24
  • since, typisk bruk:<url>?since&date=2006-12-24
  • between, typisk bruk:<url>?between&date=2007-12-24&date2=2008-12-24

Jeg vil videre ha det slik at dersom året angis som xxxx, skal det bety nærmeste forekomst av datoen, enten dette året, neste år(until) eller forrige år(since). xxxx for år i between skal tolkes slik at date betraktes som since og date2 betraktes som until.

Videre vil jeg at dersom det gis inn gale eller mangende formater, ikke eksisterende datoer eller andre feil, skal tjenesten returnere: ?. Tjenesten skal altså tåle hva som helst og aldri gi annen output enn et tall, positivt eller negativt, eller ?.

Jeg vil forsøke å løse dette ved å "lage testen før jeg skriver koden", ved hjelp av pyUnit. Utviklingen er beskrevet nedenfor i noen steg.

Steg1

Vi begynner med å lage en kodestrategi. Jeg vil gjøre det slik at jeg lager en funksjon for until-beregninger, en for since-beregninger og en for between-beregninger. Disse vil jeg kalle på grunnlag av de parametrne modulen mottar. Tolking av disse parametrene vil jeg teste senere. Nå konsentrerer jeg meg om de tre funksjonene. Skjelettet for modulen min blir slik:

"""
    This module will calculate the difference in days
    between today and any other date, or between two dates
"""
import datetime
def daysUntil(thisDay,datstr):
    """ thisDay is reference date, datstr is target date in string format"""
    return '?'
def daysSince(thisDay,datstr):
    """ thisDay is reference date, dat is target date in string format"""
    return '?'
def daysBetween(dat1str,dat2str):
    """ the two involved dates in string format"""
    return '?'

Så etablerer jeg testen slik:

"""
Testing the module daydiff
"""
# import the module we want to test:
import daydiff
# import library for making dates
import datetime
# import the PyUnit class we want to subclass:
import unittest
class testdatediff(unittest.TestCase):
    
    def setUp(self):
        # set up a refrence date that will be used 
        # in stead of datetime.date.today()
        self.refdate=datetime.date(2007,9,15)
    def testUntil(self):
        """ testing the until-case """
        for d in [['2007-09-15','0'],
                  ['2007-09-16','1'],
                  ['2007-08-15','-31'],
                  ['xxxx-09-25','10'],
                  ['xxxx-09-05','356'],
                  ['09-05','?'],
                  ['2006:09-05','?']]:
            print d
            self.assert_(daydiff.daysUntil(self.refdate,d[0])==d[1])

    def testSince(self):
        """ testing the since-case """
        for d in [['2007-09-15','0'],
                  ['2007-09-16','-1'],
                  ['xxxx-09-16','364'],
                  ['xxxx-09-14','1']
                  ]:
            print d
            self.assert_(daydiff.daysSince(self.refdate,d[0])==d[1])
    def testBetween(self):
        """ testing the between-case """
        for d in [['2007-09-16','2007-09-17','1'],
                  ['2007-09-16','2007-09-11','-5'],
                  ['2007-09-16','xxxx-09-17','1'],
                  ['2007-09-16','xxxx-09-15','365'],
                  ['xxxx-09-15','xxxx-09-14','365']
                  ]:
            print d
            self.assert_(daydiff.daysBetween(d[0],d[1])==d[2])
def makeTestSuite():
    suite=unittest.TestSuite()
    suite.addTest(testdatediff('testUntil'))
    suite.addTest(testdatediff('testSince'))
    suite.addTest(testdatediff('testBetween'))
    return suite
suite = makeTestSuite()
unittest.TextTestRunner(verbosity=2).run(suite)

Første forsøk på å teste går naturlig nok galt, alle de første testsettene for hver funksjon feiler:

testing the until-case ... ['2007-09-15', '0']
FAIL
testing the since-case ... ['2007-09-15', '0']
FAIL
testing the between-case ... ['2007-09-16', '2007-09-17', '1']
FAIL
.......

Steg2

Jeg bearbeider funksjonene og får noe slikt.

"""
    This module will calculate the difference in days
    between today and any other date, or between two dates
"""
import datetime
import os
def makeLegalDateParts(datstr):
    """ make legal y,m,d from the string datstr. y=xxxx is legal"""
    prts=datstr.split('-')
    if len(prts) != 3:
        return [-1,-1,-1]
    (y,m,d)=prts
    m=int(m.lstrip('0'))
    d=int(d.lstrip('0'))
    if y.isdigit():
        y=y.lstrip('0')
        return [y,m,d]
    if y=='xxxx':
        return [y,m,d]
    return [-1,-1,-1]
def daysUntil(thisDay,datstr):
    """ thisDay is reference date, 
        datstr is target date in string format"""
    (y,m,d)=makeLegalDateParts(datstr)
    if y == -1:
        return '?'
    if y.isdigit():
        try:
            dat=datetime.date(int(y),m,d)
            return str((dat-thisDay).days)
        except:
            return '?'
    # we have an xxxx situation
    for y in range(thisDay.year,thisDay.year+2):
        try:
            dat=datetime.date(y,m,d)
            if dat > thisDay:
                return str((dat-thisDay).days)
        except:
            return '?'
    return '?'
def daysSince(thisDay,datstr):
    """ thisDay is reference date, 
        dat is target date in string format"""
    (y,m,d)=makeLegalDateParts(datstr)
    if y == -1:
        return '?'
    if y.isdigit():
        try:
            dat=datetime.date(int(y),m,d)
            return str((thisDay-dat).days)
        except:
            return '?'
    # we have an xxxx situation
    for y in range(thisDay.year,thisDay.year-2,-1):
        try:
            dat=datetime.date(y,m,d)
            if dat < thisDay:
                return str((thisDay-dat).days)
        except:
            return '?'
    return '?'
def daysBetween(dat1str,dat2str):
    """ the two involved dates in string format"""
    (y1,m1,d1)=makeLegalDateParts(dat1str)
    (y2,m2,d2)=makeLegalDateParts(dat2str)
    if y1==-1 or y2==-1:
        return '?'
    if y1.isdigit():
        try:
            return daysUntil(datetime.date(int(y1),m1,d1),dat2str)
        except:
            return '?'
    elif y2.isdigit():
        try:
            return daysSince(datetime.date(int(y2),m2,d2),dat1str)
        except:
            return '?'
    else:
        # we have two xxxx cases, we make a choice
        # go for a positive value
        try:
            dat1=datetime.date(datetime.date.today().year,m1,d1)
            dat2=datetime.date(datetime.date.today().year,m2,d2)
            if dat1 > dat2:
                dat1=datetime.date(datetime.date.today().year-1,m1,d1)
                return daysUntil(dat1,dat2str)
        except:
            return '?'
    return '?'

Testkoden er som før og jeg får:

testing the until-case ... ['2007-09-15', '0']
['2007-09-16', '1']
['2007-08-15', '-31']
['xxxx-09-25', '10']
['xxxx-09-05', '356']
['09-05', '?']
['2006:09-05', '?']
ok
testing the since-case ... ['2007-09-15', '0']
['2007-09-16', '-1']
['xxxx-09-16', '364']
['xxxx-09-14', '1']
ok
testing the between-case ... ['2007-09-16', '2007-09-17', '1']
['2007-09-16', '2007-09-11', '-5']
['2007-09-16', 'xxxx-09-17', '1']
['2007-09-16', 'xxxx-09-15', '365']
['xxxx-09-15', 'xxxx-09-14', '365']
ok
----------------------------------------------------------------------
Ran 3 tests in 0.063s
OK

Vi slår oss til ro med at vi har skrevet tre testede funksjoner.

Steg3

Nå skal vi bruke de tre funksjonene i en modul som ligger på nettet og besvarer spørsmål. Vi har nå flere veier å gå når vi skal fortsette og teste. Jeg velger å lage meg en vevside som tester modulen. Igjen skriver jeg testen før jeg går videre med koden.

Testsiden kan se slik ut http://www.it.hiof.no/~borres/dw/days/_full_test.html

Det som testes er nå den ferdige modulen:

#! /usr/bin/python
"""
Calculating the difference between days
since:  yyyy.mm.dd
        xxxx.mm.dd ( days since last time the date (mm.dd) occured)
until:  yyy.mm.dd
        xxx.mm.dd  ( days until next time the date (mm.dd) will occur)
between:yyyy.mm.dd yyyy.mm.dd
        yyyy.mm.dd xxxx.mm.dd ( days from from first date until next time the date (mm.dd) will occur)
        xxxx.mm.dd yyyy.mm.dd ( days since the date (mm.dd) occured, as seen from second date)
        xxxx.mm.dd xxxx.mm.dd ( days between dates in same year, this year is used. possibly a leapyear)
Returns: a single integer, possibly negative
Returns: ? if parameters does not make sence, or are in bad format
"""
import datetime
import os
"""
 pack up a datestr and find integers
"""
def makeLegalDateParts(datstr):
    """ make legal y,m,d from the string datstr. y=xxxx is legal"""
    prts=datstr.split('-')
    if len(prts) != 3:
        return [-1,-1,-1]
    (y,m,d)=prts
    m=int(m.lstrip('0'))
    d=int(d.lstrip('0'))
    if y.isdigit():
        y=y.lstrip('0')
        return [y,m,d]
    if y=='xxxx':
        return [y,m,d]
    return [-1,-1,-1]
"""
 until is ordered
"""
def daysUntil(thisDay,datstr):
    """ thisDay is reference date, 
        datstr is target date in string format"""
    (y,m,d)=makeLegalDateParts(datstr)
    if y == -1:
        return '?'
    if y.isdigit():
        try:
            dat=datetime.date(int(y),m,d)
            return str((dat-thisDay).days)
        except:
            return '?'
    # we have an xxxx situation
    for y in range(thisDay.year,thisDay.year+2):
        try:
            dat=datetime.date(y,m,d)
            if dat > thisDay:
                return str((dat-thisDay).days)
        except:
            return '?'
    return '?'
"""
 since is ordered
"""
def daysSince(thisDay,datstr):
    """ thisDay is reference date, 
        dat is target date in string format"""
    (y,m,d)=makeLegalDateParts(datstr)
    if y == -1:
        return '?'
    if y.isdigit():
        try:
            dat=datetime.date(int(y),m,d)
            return str((thisDay-dat).days)
        except:
            return '?'
    # we have an xxxx situation
    for y in range(thisDay.year,thisDay.year-2,-1):
        try:
            dat=datetime.date(y,m,d)
            if dat < thisDay:
                return str((thisDay-dat).days)
        except:
            return '?'
    return '?'
"""
 between is ordered
"""
def daysBetween(dat1str,dat2str):
    """ the two involved dates in string format"""
    (y1,m1,d1)=makeLegalDateParts(dat1str)
    (y2,m2,d2)=makeLegalDateParts(dat2str)
    if y1==-1 or y2==-1:
        return '?'
    if y1.isdigit():
        try:
            return daysUntil(datetime.date(int(y1),m1,d1),dat2str)
        except:
            return '?'
    elif y2.isdigit():
        try:
            return daysSince(datetime.date(int(y2),m2,d2),dat1str)
        except:
            return '?'
    else:
        # we have two xxxx cases, we make a choice
        # go for a positive value
        try:
            dat1=datetime.date(datetime.date.today().year,m1,d1)
            dat2=datetime.date(datetime.date.today().year,m2,d2)
            if dat1 > dat2:
                dat1=datetime.date(datetime.date.today().year-1,m1,d1)
                return daysUntil(dat1,dat2str)
        except:
            return '?'
    return '?'
if __name__=="__main__":    
    import cgi
    form=cgi.FieldStorage()
    #---------------------------
    # expect to find parameters:
    # diff= until | since | between
    # diff=until | since -> date
    # dif=between -> date and date2
    #----------------------------
    thisDay=datetime.date.today()
    # if requested from testpage we set reference date
    if os.environ.has_key('HTTP_REFERER')\
       and str(os.environ['HTTP_REFERER']).find('_full_test.html') != -1:
        thisDay=datetime.date(2007,9,15)
    
    print 'Content-type: text/plain\n'
    diff=''
    date1=''
    date2=''
    if form.has_key('diff'):
        diff=form['diff'].value
    if form.has_key('date'):
        date1=form['date'].value
    if form.has_key('date2'):
        date2=form['date2'].value
    #print(diff+' '+date)
    if diff=='until':
        print daysUntil(thisDay,date1)
    elif diff=='since':
        print daysSince(thisDay,date1)
    elif diff=='between':
        print daysBetween(date1,date2)
    else:
        print '?'

Vi har da en modul daydiff.py, vi har en testmodul, daytest.py og vi har en testside, fulltest,html. Hele testbatteriet er intakt og vi kan test senere når vi oppdager feil, eller dersom vi ønsker å skrive om koden. (det forekommer meg at den kan forenkles noe ?)

Bruk

Den ferdige modulen kan brukes herfra: http://www.it.hiof.no/~borres/cgi-bin/ajax/days/daydiff.py. Dokumentasjonen kan være omtrent slik:

url:http://www.it.hiof.no/~borres/cgi-bin/ajax/days/daydiff.py
Calculating and returning the day difference between dates
Parameters:
diff since | until | between
since date 
      date yyyy.mm.dd
      date xxxx.mm.dd ( days since last time the date (mm.dd) occured)
until date yyyy.mm.dd
      date xxxx.mm.dd  ( days until next time the date (mm.dd) will occur)
between: date date 2
         date=yyyy.mm.dd date2=yyyy.mm.dd ( days between the two dates)
         date=yyyy.mm.dd date2=xxxx.mm.dd ( days from from first date until next time the date2 will occur)
         date=xxxx.mm.dd date2=yyyy.mm.dd ( days since the date (mm.dd) occured, as seen from second date)
         date=xxxx.mm.dd date2=xxxx.mm.dd ( days between dates, baed on date, always positive)

Returns: a single integer, possibly negative
Returns: ? if parameters does not make sence, or are in bad format

Samples:
url?diff=until&date=2011-12-24
url?diff=since&date=2011-12-24
url?diff=between&date=2011-12-24&date2=2013-12-24

Eksempel på bruk: Så mange dager er det til jul:

<p>
 Eksempel på bruk: Så mange dager er det til jul: 
 <span id="jul" style="color:red;fontweight:bold"></span>
</p>
<script>
$('#jul').load('http://www.it.hiof.no/~borres/cgi-bin/ajax/days/daydiff.py?diff=until&amp;date=xxxx-12-24');
</script>

Det er laget en forklaringsside: http://www.it.hiof.no/~borres/cgi-bin/ajax/days/explained.html.

Referanser

Se modulen: Pythontesting

Vedlikehold

B.Stenseth, september 2007

( Velkommen ) Kodetesting > Pythontesting >Snart ( JStesting )