""" This module provides a set of constructors and routines to convert
    between DateTime[Delta] instances and ISO representations of date
    and time.

    Note: Timezones are only interpreted by ParseDateTimeGMT(). All
    other constructors silently ignore the time zone information.

    (c) 1998, Copyright Marc-Andre Lemburg; All Rights Reserved.
    See the documentation for further information on copyrights,
    or contact the author (mal@lemburg.com).
"""
import DateTime

import re,string

# Grammar: ISO 8601 (not all, but what we need from it)
_year = '(?P<year>\d?\d\d\d)'
_month = '(?P<month>\d?\d)'
_day = '(?P<day>\d?\d)'
_hour = '(?P<hour>\d?\d)'
_minute = '(?P<minute>\d?\d)'
_second = '(?P<second>\d?\d(?:.\d+)?)'
_sign = '(?P<sign>[-+])'
_week = 'W(?P<week>\d?\d)'
_zone = '(?P<zone>[-+]\d\d:?\d\d)'

_weekdate = _year + '-?(?:' + _week + '-?' + _day + '?)?'
_date = _year + '-?' + '(?:' + _month + '-?' + _day + '?)?'
_time = _hour + ':?' + _minute + ':?' + _second + '?(?:' + _zone + ')?'

_isodatetime = re.compile(_date + '(?:[ T]' + _time + ')?$')
_isodate = re.compile(_date + '$')
_isotime = re.compile(_time + '$')
_isodelta = re.compile(_sign + '?' + _time + '$')
_isoweek = re.compile(_weekdate + '$')
_isoweektime = re.compile(_weekdate + '(?:[ T]' + _time + ')?$')

def WeekTime(year,isoweek=1,isoday=1,hour=0,minute=0,second=0.0):

    """Week(year,isoweek=1,isoday=1,hour=0,minute=0,second=0.0)

    Returns a DateTime instance pointing to the given ISO week and day.
    isoday defaults to 1, which corresponds to Monday in the ISO
    numbering. The time part is set as given."""

    d = DateTime.DateTime(year,1,1,hour,minute,second)
    if d.iso_week[0] == year:
	# 1.1. belongs to year (backup to Monday)
	return d + (-d.day_of_week + 7 * (isoweek-1) + isoday - 1)
    else:
	# 1.1. belongs to year-1 (advance to Monday)
	return d + (7-d.day_of_week + 7 * (isoweek-1) + isoday - 1)

# Alias
Week = WeekTime

# Aliases for the other constructors (they all happen to already use
# ISO format)
Date = DateTime.Date
Time = DateTime.Time
TimeDelta = DateTime.TimeDelta

_zoneoffset = re.compile('(?P<sign>[+-])(?P<hour>\d\d):?(?P<minute>\d\d)')

def _utc_offset(zone,

		atoi=string.atoi):

    """ Return the UTC time zone offset as DateTimeDelta instance.
    """ 
    if zone is None:
	return 0
    info = _zoneoffset.match(zone)
    if not info:
	raise ValueError,'wrong time zone format, use +-HH:MM'
    sign,hours,minutes = info.groups()
    value = atoi(hours)*60 + atoi(minutes)
    if sign == '-':
	value = -value
    return value * DateTime.oneMinute

def ParseDateTime(isostring,

		  strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseDateTime(isostring)

    Returns a DateTime instance reflecting the given ISO date. A time
    part is optional and must be delimited from the date by a space
    or 'T'. """

    s = strip(isostring)
    date = _isodatetime.match(s)
    if not date:
	raise ValueError,'wrong format, use YYYY-MM-DD HH:MM:SS'
    year,month,day,hour,minute,second,zone = date.groups()
    year = atoi(year)
    if month is None:
	month = 1
    else:
	month = atoi(month)
    if day is None:
	day = 1
    else:
	day = atoi(day)
    if hour is None:
	hour = 0
    else:
	hour = atoi(hour)
    if minute is None:
	minute = 0
    else:
	minute = atoi(minute)
    if second is None:
	second = 0.0
    else:
	second = atof(second)
    return DateTime.DateTime(year,month,day,hour,minute,second)

def ParseDateTimeGMT(isostring,

		     strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseDateTimeGMT(isostring)

    Returns a DateTime instance in UTC reflecting the given ISO
    date. A time part is optional and must be delimited from the date
    by a space or 'T'. Timezones are honored."""

    s = strip(isostring)
    date = _isodatetime.match(s)
    if not date:
	raise ValueError,'wrong format, use YYYY-MM-DD HH:MM:SS'
    year,month,day,hour,minute,second,zone = date.groups()
    year = atoi(year)
    if month is None:
	month = 1
    else:
	month = atoi(month)
    if day is None:
	day = 1
    else:
	day = atoi(day)
    if hour is None:
	hour = 0
    else:
	hour = atoi(hour)
    if minute is None:
	minute = 0
    else:
	minute = atoi(minute)
    if second is None:
	second = 0.0
    else:
	second = atof(second)
    offset = _utc_offset(zone)
    return DateTime.DateTime(year,month,day,hour,minute,second) - offset

# Alias
ParseDateTimeUTC = ParseDateTimeGMT

def ParseDate(isostring,

	      strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseDate(isostring)

    Returns a DateTime instance reflecting the given ISO date. A time
    part may not be included."""

    s = strip(isostring)
    date = _isodate.match(s)
    if not date:
	raise ValueError,'wrong format, use YYYY-MM-DD'
    year,month,day = date.groups()
    year = atoi(year)
    if month is None:
	month = 1
    else:
	month = atoi(month)
    if day is None:
	day = 1
    else:
	day = atoi(day)
    return DateTime.DateTime(year,month,day)

def ParseWeek(isostring,

	      strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseWeek(isostring)

    Returns a DateTime instance reflecting the given ISO date. A time
    part may not be included."""

    s = strip(isostring)
    date = _isoweek.match(s)
    if not date:
	raise ValueError,'wrong format, use yyyy-Www-d, e.g. 1998-W01-1'
    year,week,day = date.groups()
    year = atoi(year)
    if week is None:
	week = 1
    else:
	week = atoi(week)
    if day is None:
	day = 1
    else:
	day = atoi(day)
    return Week(year,week,day)

def ParseWeekTime(isostring,

		  strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseWeekTime(isostring)

    Returns a DateTime instance reflecting the given ISO date. A time
    part is optional and must be delimited from the date by a space
    or 'T'."""

    s = strip(isostring)
    date = _isoweektime.match(s)
    if not date:
	raise ValueError,'wrong format, use e.g. "1998-W01-1 12:00:30"'
    year,week,day,hour,minute,second,zone = date.groups()
    year = atoi(year)
    if week is None:
	week = 1
    else:
	week = atoi(week)
    if day is None:
	day = 1
    else:
	day = atoi(day)
    if hour is None:
	hour = 0
    else:
	hour = atoi(hour)
    if minute is None:
	minute = 0
    else:
	minute = atoi(minute)
    if second is None:
	second = 0.0
    else:
	second = atof(second)
    return WeekTime(year,week,day,hour,minute,second)

def ParseTime(isostring,

	      strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseTime(isostring)

    Returns a DateTimeDelta instance reflecting the given ISO time.
    Hours and minutes must be given, seconds are optional. Fractions
    of a second may also be used, e.g. 12:23:12.34."""

    s = strip(isostring)
    time = _isotime.match(s)
    if not time:
	raise ValueError,'wrong format, use HH:MM:SS'
    hour,minute,second,zone = time.groups()
    hour = atoi(hour)
    minute = atoi(minute)
    if second is not None:
	second = atof(second)
    else:
	second = 0.0
    return DateTime.TimeDelta(hour,minute,second)

def ParseTimeDelta(isostring,

		   strip=string.strip,atoi=string.atoi,atof=string.atof):

    """ParseTimeDelta(isostring)

    Returns a DateTimeDelta instance reflecting the given ISO time as
    delta. Hours and minutes must be given, seconds are
    optional. Fractions of a second may also be used,
    e.g. 12:23:12.34. In addition to the ISO standard a sign may be
    prepended to the time, e.g. -12:34."""

    s = strip(isostring)
    time = _isodelta.match(s)
    if not time:
	raise ValueError,'wrong format, use [-]HH:MM:SS'
    sign,hour,minute,second,zone = time.groups()
    hour = atoi(hour)
    minute = atoi(minute)
    if second is not None:
	second = atof(second)
    else:
	second = 0.0
    if sign and sign == '-':
	return -DateTime.TimeDelta(hour,minute,second)
    else:
	return DateTime.TimeDelta(hour,minute,second)

def ParseAny(isostring):

    """ParseAny(isostring)

    Parses the given string and tries to convert it to a
    DateTime[Delta] instance."""

    try:
	return ParseDateTime(isostring)
    except ValueError:
	pass
    try:
	return ParseWeekTime(isostring)
    except ValueError:
	pass
    try:
	return ParseTimeDelta(isostring)
    except ValueError:
	raise ValueError,'unsupported format'

def str(datetime,tz=None,
        # Locals:
        tz_offset=DateTime.tz_offset):

    """str(datetime,tz=DateTime.tz_offset(datetime))

    Returns the datetime instance as ISO date string. tz can be given
    as DateTimeDelta instance providing the time zone difference from
    datetime's zone to UTC. It defaults to DateTime.tz_offset(datetime)
    which assumes local time. """

    if tz is None:
        tz = tz_offset(datetime)
    return '%04i-%02i-%02i %02i:%02i:%02i%+03i%02i' % (
	datetime.year, datetime.month, datetime.day, 
	datetime.hour, datetime.minute, datetime.second,
	tz.hour,tz.minute)

def strGMT(datetime):

    """strGMT(datetime)

    Returns the datetime instance as ISO date string assuming it
    is given in UTC. """

    return '%04i-%02i-%02i %02i:%02i:%02i+0000' % (
	datetime.year, datetime.month, datetime.day, 
	datetime.hour, datetime.minute, datetime.second)

# Alias
strUTC = strGMT
