#	Formatter.py
#
#	------------------------------------------------------------
#	Copyright 2002, 2004 by Samuel Reynolds. All rights reserved.
#
#	Permission to use, copy, modify, and distribute this software and its
#	documentation for any purpose and without fee is hereby granted,
#	provided that the above copyright notice appear in all copies and that
#	both that copyright notice and this permission notice appear in
#	supporting documentation, and that the name of Samuel Reynolds
#	not be used in advertising or publicity pertaining to distribution
#	of the software without specific, written prior permission.
#
#	SAMUEL REYNOLDS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
#	INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
#	EVENT SHALL SAMUEL REYNOLDS BE LIABLE FOR ANY SPECIAL, INDIRECT, OR
#	CONSEQUENTIAL DAMAGES, OR FOR ANY DAMAGES WHATSOEVER RESULTING FROM
#	LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
#	NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
#	WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#	------------------------------------------------------------


"""
Formatters for converting and validating data values.
"""

import copy, re, time

class Formatter( object ):
	"""
	Formatter/validator for data values.
	"""
	def __init__( self, *args, **kwargs ):
		pass
	
	# Default (dummy) validate routine
	def validate( self, value ):
		"""
		Return true if value is valid for the field.
		value is a string from the UI.
		"""
		return True
	
	# Default (dummy) format routine
	def format( self, value ):
		"""Format a value for presentation in the UI."""
		if value == None:
			return ''
		return str(value)
	
	# Default (dummy) coerce routine
	def coerce( self, value ):
		"""Convert a string from the UI into a storable value."""
		return value


class EnumFormatter( Formatter ):
	"""
	Formatter for enumerated (EnumType) data values.
	"""
	def __init__( self, enumeration, *args, **kwargs ):
		
		super(EnumFormatter,self).__init__( *args, **kwargs )
		
		self.enumeration = enumeration
	
	
	def validValues( self ):
		"""
		Return list of valid value (id,label) pairs.
		"""
		return copy.copy( self.enumeration.items() )
	
	
	def validate( self, value ):
		"""
		Return true if value is valid for the field.
		value is a string from the UI.
		"""
		vv = [ s for i, s in self.validValues() ]
		return ( value in vv )
	
	
	def format( self, value ):
		"""Format a value for presentation in the UI."""
		return self.enumeration[value]
	
	
	def coerce( self, value ):
		"""Convert a string from the UI into a storable value."""
		return getattr( self.enumeration, value )


class FormatterMeta( type ):
	"""
	Metaclass for subclasses of Formatter.
	
	Each instance class MUST define either validate(self, value) method
	or re_validation regular expression string.
	
	If the latter, the validate method will be autogenerated.
	
	If re_validation is defined, validate method is overridden to
	validate the string value against the regular expression.
	
	If re_validation_flags is defined, the flags will be
	used when the re_validation regular expression string is compiled.
	
	Each instance class MAY define:
	-	format(self, value) method.
	-	coerce(self, value) method.
	"""
	def __new__( cls, classname, bases, classdict ):
		newdict = copy.copy( classdict )
		
		# Generate __init__ method
		# Direct descendants of Formatter automatically get __init__.
		# Indirect descendants don't automatically get one.
		if Formatter in bases:
			def __init__( self, *args, **kwargs ):
				Formatter.__init__( self, *args, **kwargs )
				initialize = getattr( self, 'initialize', None )
				if initialize:
					initialize()
			newdict['__init__'] = __init__
		else:
			def __init__( self, *args, **kwargs ):
				super(self.__class__,self).__init__( *args, **kwargs)
				initialize = getattr( self, 'initialize', None )
				if initialize:
					initialize()
			newdict['__init__'] = __init__
		
		# Generate validate-by-RE method if specified
		re_validation = newdict.get( 're_validation', None )
		if re_validation:
			# Override validate method
			re_validation_flags = newdict.get( 're_validation_flags', 0 )
			newdict['_re_validation'] = re.compile( re_validation, re_validation_flags )
			def validate( self, value ):
				return ( self._re_validation.match( value ) != None )
			newdict['validate'] = validate
		
		# Delegate class creation to the expert
		return type.__new__( cls, classname, bases, newdict )


class ObjectIdFormatter( Formatter ):
	"""
	Object ID is assumed to be a large (32 bit?) unsigned integer.
	"""
	__metaclass__ = FormatterMeta
	re_validation = '^[0-9]+$'
	def coerce( self, value ):
		if value: return long(value)
		return value

class StringFormatter( Formatter ):
	__metaclass__ = FormatterMeta

class AlphaFormatter( StringFormatter ):
	"""Alphabetic characters only."""
	re_validation = '^[a-zA-Z]*$'

class AlphaNumericFormatter( StringFormatter ):
	"""Alphanumeric characters only."""
	re_validation = '^[a-zA-Z0-9]*$'

class EmailFormatter( StringFormatter ):
	"""Internet email addresses (more or less)."""
	# This regex does not match all legal email addresses, but
	# it does a pretty good job.
	# Strangely enough, '/' is legal in email addresses.
	# However, I've never seen it used, so I prefer to leave it out.
	_re_subs = {
			'sub1' : r'[a-zA-Z~_-][a-zA-Z0-9_:~-]*',
			'sub2' : r'(\.[a-zA-Z0-9_:~-]+)*',
			'sfx'  : r'\.[a-zA-Z]{2,3}'
		}
	re_validation = '^%(sub1)s%(sub2)s[@]%(sub1)s%(sub2)s%(sfx)s$' % _re_subs

class MoneyFormatter( StringFormatter ):
	"""Assumes decimal money, but doesn't assume currency."""
	re_validation = '^(([0-9]+([.][0-9]{2})?)|([0-9]*[.][0-9]{2}))$'

class IntFormatter( Formatter ):
	"""Signed or unsigned integer."""
	__metaclass__ = FormatterMeta
	#re_validation = '^[-+]?[0-9]+$'
	def validate( self, value ):
		try:
			v = int( value )
			return True
		except:
			return False
	def coerce( self, value ):
		if value: return int(value)
		return value

class Int8Formatter( IntFormatter ):
	pass

class Int16Formatter( IntFormatter ):
	pass

class Int24Formatter( IntFormatter ):
	def coerce( self, value ):
		if value: return long(value)
		return value

class Int32Formatter( IntFormatter ):
	def coerce( self, value ):
		if value: return long(value)
		return value

class Int64Formatter( IntFormatter ):
	def coerce( self, value ):
		if value: return long(value)
		return value

class UIntFormatter( Formatter ):
	"""Unsigned integer."""
	__metaclass__ = FormatterMeta
	re_validation = '^[0-9]+$'
	def coerce( self, value ):
		if value: return int(value)
		return value

class UInt8Formatter( UIntFormatter ):
	pass

class UInt16Formatter( UIntFormatter ):
	def coerce( self, value ):
		if value: return long(value)
		return value

class UInt24Formatter( UIntFormatter ):
	def coerce( self, value ):
		if value: return long(value)
		return value

class UInt32Formatter( UIntFormatter ):
	def coerce( self, value ):
		if value: return long(value)
		return value

class FloatFormatter( Formatter ):
	"""Signed or unsigned floating-point number."""
	__metaclass__ = FormatterMeta
	re_validation = '^[-+]?(([0-9]+[.]?[0-9]*)|([0-9]*[.]?[0-9]+))$'
	def coerce( self, value ):
		if value: return float(value)
		return value

class DoubleFormatter( FloatFormatter ):
	pass

class UFloatFormatter( Formatter ):
	"""Unsigned floating-point number."""
	__metaclass__ = FormatterMeta
	re_validation = '^(([0-9]+[.]?[0-9]*)|([0-9]*[.]?[0-9]+))$'
	def coerce( self, value ):
		if value: return float(value)
		return value

class UDoubleFormatter( UFloatFormatter ):
	pass


class TextFormatter( Formatter ):
	__metaclass__ = FormatterMeta


class TimeElapsedFormatter( Formatter ):
	"""Elapsed time string (HH:MM:SS)."""
	__metaclass__ = FormatterMeta
	re_validation = '^([1][0-2]|[0]?[0-9]):[0-5][0-9](:[0-5][0-9])?$'

class DateFormatter( Formatter ):
	"""
	Date string (YYYY-MM-DD).
	
	Storage format:      YYYY-MM-DD
	Presentation format: YYYY-MM-DD
	
	Accepts only YYYY-MM-DD format (allows variant separators '/' and '.').
	Accepts dates in range (1000-2999)-(01-12)-(01-31).
	Leading zeros optional in month and day.
	Does not enforce # of days in month.
	"""
	__metaclass__ = FormatterMeta
	
	re_validation = r'^[1-2][0-9]{3}([-/.])([0][1-9]|[1][0-2])\1([0][1-9]|[12][0-9]|[3][0-1])$'
	
	def coerce( self, value ):
		"""Convert alternate date separators to '-'."""
		return re.sub( r'[/.]', '-', value )

class DateFormatterMDY( DateFormatter ):
	"""Alternate date string (MM-DD-YYYY).
	
	Storage format:      YYYY-MM-DD
	Presentation format: MM-DD-YYYY
	
	Accepts only MM-DD-YYYY format  (allows variant separators '/' and '.').
	Accepts dates in range (01-12)-(01-31)-(1000-2999).
	Leading zeros optional in month and day.
	Does not enforce # of days in month.
	"""
	re_validation = r'^([0][1-9]|[1][0-2])([-/.])([0][1-9]|[12][0-9]|[3][0-1])\1[12][0-9]{3}$'

	def format( self, value ):
		dt = time.strptime( value, '%Y-%m-%d' )
		return time.strftime( '%m-%d-%Y', dt )
	
	def coerce( self, value ):
#		value = re.sub( r'[/.]', '-', value )
#		dt = time.strptime( value, '%m-%d-%Y' )
#		return time.strftime( '%Y-%m-%d', dt )
		m, d, y = re.split( '[-/.]', value )
		return '%04d-%02d-%02d' % ( int(y), int(m), int(d) )

class TimeFormatter( Formatter ):
	"""
	Time string (12-hour or 24-hour format, with or without seconds or am/pm).
	
	Storage format:      HH:MM:SS -- 24-hour format.
	Presentation format: HH:MM    -- 24-hour format.
	
	Accepts 12-hour or 24-hour format, with or without seconds or am/pm.
	"""
	__metaclass__ = FormatterMeta
	
	reTime24 = r'(([0]?[0-9]|[1][0-9]|[2][0-3]):[0-5][0-9](:[0-5][0-9])?)'
	reTimeAP = r'(([1][0-2]|[0]?[0-9]):[0-5][0-9](:[0-5][0-9])?[ ]*([aApP][mM])?)'
	re_validation = r'^%s|%s$' % ( reTime24, reTimeAP )
	
	def format( self, value ):
		return ':'.join( value.split(':')[:2] )
	
	def coerce( self, value ):
		for fmt in ( '%H:%M:%S', '%H:%M', '%I:%M:%S %p', '%I:%M %p','%I:%M:%S' ):
			try:
				dt = time.strptime( value, fmt )
				break
			except ValueError:
				pass
		return time.strftime( '%H:%M:%S', dt )

class TimeFormatter12H( TimeFormatter ):
	"""
	Alternate time string (12-hour format, without seconds).
	
	Storage format:      HH:MM(:SS) -- 24-hour format.
	Presentation format: HH:MM( aa) -- 12-hour format.
	                     (aa may be 'am' or 'pm')
	"""
	
	def format( self, value ):
		dt = time.strptime( value, '%H:%M:%S' )
		return time.strftime( '%I:%M %p', dt )

class DateTimeFormatter( Formatter ):
	"""
	Date/time string.
	
	Uses a DateFormatter and a TimeFormatter.
	"""
	__metaclass__ = FormatterMeta
	
	# Storage format: YYYY-MM-DD HH:MM:SS -- 24-hour format.
	# Presentation format: same as storage format.
	
	def validate( self , value ):
		datef = DateFormatter()
		timef = TimeFormatter()
		date, time = re.split( r'[ ]+', value )
		return ( datef.validate( date ) and timef.validate( time ) )


if __name__ == '__main__':
	
	from EnumType import EnumType
	
#	from prs.PRSObject import PRSObject
#	
#	print 'dir(ObjectIdField) =', dir(ObjectIdField)
#	print 'ObjectIdField.__init__ =', ObjectIdField.__init__
#	print 'ObjectIdField.__init__.func_doc =', ObjectIdField.__init__.func_doc
#	anIdField = ObjectIdFormatter( False )
#	print 'dir(anIdField) =', dir(anIdField)
#	print 'anIdField.__class__ =', anIdField.__class__
#	
#	print
#	print 'dir(DateFormatterMDY) =', dir(DateFormatterMDY)
#	print
	
	# ========== EnumFormatter ==========
	print
	print 'EnumFormatter'
	TestEnum = EnumType( 'A', 'B', 'C', 'D' )
	formatter = EnumFormatter( TestEnum )
	passCount = 0
	failCount = 0
	
	# formatter.validValues
	expectedValidValues = [ (0,'A'), (1,'B'), (2,'C'), (3,'D') ]
	temp = formatter.validValues()
	if temp == expectedValidValues:
		passCount += 1
	else:
		failCount += 1
		print 'formatter.validValues() = %r -- WRONG -- expected %r' % (temp, expectedValidValues )
	
	# formatter.format and formatter.coerce
	for id, name in TestEnum.items():
		# formatter.format
		temp = formatter.format( id )
		if temp == name:
			passCount += 1
		else:
			failCount += 1
			print 'formatter.format(%d) = %r -- WRONG -- expected %r' % (id, temp, name )
		# formatter.coerce
		temp = formatter.coerce( name )
		if temp == id:
			passCount += 1
		else:
			failCount += 1
			print 'formatter.coerce(%r) = %r -- WRONG -- expected %r' % (name, temp, id )
	if failCount == 0:
		print '\tPASS (%d tests)' % passCount
	
	
	# ========== EmailFormatter ==========
	print
	print 'EmailFormatter'
	
	tests = [
			( 'a@b.com', True ),
			( 'a@b', False ),
			( '@b', False ),
			( '@b.c', False ),
			( '@b.us', False ),
			( 'a@b.us', True ),
			( '~joe_public-73@bozo.net', True ),
		]
	formatter = EmailFormatter()
	passCount = 0
	failCount = 0
	
	for email, expect in tests:
		actual = formatter.validate( email )
		if actual == expect:
			passCount += 1
		else:
			failCount += 1
	if failCount == 0:
		print '\tPASS (%d tests)' % passCount
