Index: django/contrib/localflavor/my/my_states.py
===================================================================
--- django/contrib/localflavor/my/my_states.py	(revision 0)
+++ django/contrib/localflavor/my/my_states.py	(revision 0)
@@ -0,0 +1,25 @@
+"""
+A list of Malaysian states for use as `choices` in a form or model field.
+"""
+
+from django.utils.translation import gettext_lazy as _
+
+STATE_CHOICES = (
+    ('JHR', _(u'Johor')),
+    ('KDH', _(u'Kedah')),
+    ('KTN', _(u'Kelantan')),
+    ('MLK', _(u'Melaka')),
+    ('NSN', _(u'Negeri Sembilan')),
+    ('PHG', _(u'Pahang ')),
+    ('PNG', _(u'Pulau Pinang')),
+    ('PRK', _(u'Perak')),
+    ('PLS', _(u'Perlis')),
+    ('SGR', _(u'Selangor')),
+    ('TRG', _(u'Terengganu')),
+    ('SBH', _(u'Sabah')),
+    ('SRW', _(u'Sarawak')),
+    ('KUL', _(u'W.P Kuala Lumpur')),
+    ('LBN', _(u'W.P Labuan')),
+    ('PJY', _(u'W.P Putrajaya')),
+    ('WP',  _(u'Wilayah Persekutuan')),
+)
Index: django/contrib/localflavor/my/__init__.py
===================================================================
Index: django/contrib/localflavor/my/models.py
===================================================================
--- django/contrib/localflavor/my/models.py	(revision 0)
+++ django/contrib/localflavor/my/models.py	(revision 0)
@@ -0,0 +1,85 @@
+"""
+Malaysia-specific model fields.
+"""
+
+from django.conf import settings
+from django.db.models.fields import Field
+
+class IdentityCardNumberField(Field):
+    """
+    A ``CharField`` validates its input is a Malaysian Identity Card number.
+    """
+    def get_internal_type(self):
+        return "IdentityCardNumberField"
+
+    def db_type(self):
+        if settings.DATABASE_ENGINE == 'oracle':
+            return 'VARCHAR2(14)'
+        else:
+            return 'varchar(14)'
+
+    def formfield(self, **kwargs):
+        from django.contrib.localflavor.my.forms import MYIdentityCardNumberField
+        defaults = {'widget': MYIdentityCardNumberField}
+        defaults.update(kwargs)
+        return super(MYIdentityCardNumberField, self).formfield(**defaults)
+
+class PostCodeField(Field):
+    """
+    A model field that forms represent as a ``forms.MYPostCodeField`` and
+    validates its input is a Malaysian postcode.
+    """
+    def get_internal_type(self):
+        return "PostCodeField"
+
+    def db_type(self):
+        if settings.DATABASE_ENGINE == 'oracle':
+            return 'CHAR(5)'
+        else:
+            return 'varchar(5)'
+
+    def formfield(self, **kwargs):
+        from django.contrib.localflavor.my.forms import MYPostCodeField
+        defaults = {'widget': MYPostCodeField}
+        defaults.update(kwargs)
+        return super(MYPostCodeField, self).formfield(**defaults)
+
+class PhoneNumberField(Field):
+    """
+    A model field that forms represent as a ``forms.MYPhoneNumberField`` and
+    validates its input is a Malaysian phone number.
+    """
+    def get_internal_type(self):
+        return "PhoneNumberField"
+
+    def db_type(self):
+        if settings.DATABASE_ENGINE == 'oracle':
+            return 'VARCHAR2(14)'
+        else:
+            return 'varchar(14)'
+
+    def formfield(self, **kwargs):
+        from django.contrib.localflavor.my.forms import MYPhoneNumberField
+        defaults = {'widget': MYPhoneNumberField}
+        defaults.update(kwargs)
+        return super(MYPhoneNumberField, self).formfield(**defaults)
+
+class StateField(Field):
+    """
+    A model field that forms represent as a ``forms.MYStateField`` and stores the
+    three-letter Malaysian state abbreviation in the database.
+    """
+    def get_internal_type(self):
+        return "StateField"
+
+    def db_type(self):
+        if settings.DATABASE_ENGINE == 'oracle':
+            return 'CHAR(3)'
+        else:
+            return 'varchar(3)'
+
+    def formfield(self, **kwargs):
+        from django.contrib.localflavor.my.forms import MYStateSelect
+        defaults = {'widget': MYStateSelect}
+        defaults.update(kwargs)
+        return super(MYStateSelect, self).formfield(**defaults)
Index: django/contrib/localflavor/my/my_bpcodes.py
===================================================================
--- django/contrib/localflavor/my/my_bpcodes.py	(revision 0)
+++ django/contrib/localflavor/my/my_bpcodes.py	(revision 0)
@@ -0,0 +1,13 @@
+"""
+A list of valid BP (Birth Place) codes in the Malaysian Identity Card number.
+
+Source: http://en.wikipedia.org/wiki/MyKad#BP_codes_-_Codes_representing_place_of_birth
+"""
+
+BP_CODES = ('00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10',
+'11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23',
+'24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36',
+'37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49',
+'50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62',
+'63', '64', '65', '66', '67', '68', '74', '75', '76', '77', '78', '79', '82',
+'83', '84', '85', '86', '87', '88', '89', '90', '91', '92', '93')
Index: django/contrib/localflavor/my/forms.py
===================================================================
--- django/contrib/localflavor/my/forms.py	(revision 0)
+++ django/contrib/localflavor/my/forms.py	(revision 0)
@@ -0,0 +1,102 @@
+"""
+Malaysia-specific form helpers.
+"""
+
+from django.forms import ValidationError
+from django.forms.fields import Field, RegexField, Select, EMPTY_VALUES
+from django.utils.encoding import smart_unicode
+from django.utils.translation import gettext_lazy as _
+import re
+
+ic_re = re.compile(r'^(?P<dob>\d{6})-?(?P<bp>\d{2})-?(?P<serial>\d{4})$')
+phone_re = re.compile(r'^(\+?6?)(\d{2,3}-?)(\d{6,8})$')
+
+class MYIdentityCardNumberField(Field):
+    """
+    A form field that validates its input is a valid Malaysian Identity Card number.
+    (YYMMDD-BP-XXXX)
+
+    The regular expression used is sourced from: http://en.wikipedia.org/wiki/MyKad
+
+    Takes one extra optional argument:
+
+    .. attribute:: MYIdentityCardNumberField.check_bp_code
+
+        If ``True``, field will validate the BP digits is a valid BP (birth place)
+        code (see above Wikipedia link for more information). Default: ``True``
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.'),
+    }
+
+    def __init__(self, *args, **kwargs):
+        self.check_bp_code = kwargs.get('check_bp_code', True)
+        if 'check_bp_code' in kwargs:
+            del kwargs['check_bp_code']
+        Field.__init__(self, *args, **kwargs)
+
+    def clean(self, value):
+        super(MYIdentityCardNumberField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        match = re.match(ic_re, value)
+        if not match:
+            raise ValidationError(self.error_messages['invalid'])
+        dob, bp, serial = match.groupdict()['dob'], match.groupdict()['bp'], match.groupdict()['serial']
+
+        # DOB is a valid date
+        from datetime import date
+        try:
+            yy, mm, dd = int(dob[0:2]), int(dob[2:4]), int(dob[4:])
+            d = date(yy, mm, dd)
+        except ValueError:
+            raise ValidationError(self.error_messages['invalid'])
+
+        # BP is a valid birth place code
+        if self.check_bp_code:
+            from my_bpcodes import BP_CODES
+            if not bp in BP_CODES:
+                raise ValidationError(self.error_messages['invalid'])
+
+        return u'%s-%s-%s' % (dob, bp, serial)
+
+class MYPostCodeField(RegexField):
+    """
+    A form field that validates its input is a 5-digit Malaysian postcode.
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a postcode in the format XXXXX.')
+    }
+
+    def __init__(self, *args, **kwargs):
+        super(MYPostCodeField, self).__init__(r'^\d{5}$',
+            max_length=None, min_length=None, *args, **kwargs)
+
+class MYPhoneNumberField(Field):
+    """
+    A form field that validates its input is a Malaysian phone number.
+
+    Accepts phone numbers in the format XXX-XXXXXXXX, with or without an
+    international country code prefix (+6)
+    """
+    default_error_messages = {
+        'invalid': _(u'Enter a phone number in the format XXX-XXXXXXXX.')
+    }
+
+    def clean(self, value):
+        super(MYPhoneNumberField, self).clean(value)
+        if value in EMPTY_VALUES:
+            return u''
+        value = re.sub('(\(|\)|\s+)', '', smart_unicode(value))
+        m = phone_re.search(value)
+        if m:
+            return u'%s%s%s' % (m.group(1), m.group(2), m.group(3))
+        raise ValidationError(self.error_messages['invalid'])
+
+class MYStateSelect(Select):
+    """
+    A Select widget that uses a list of Malaysian states as its choices.
+    """
+    def __init__(self, attrs=None):
+        from my_states import STATE_CHOICES
+        super(MYStateSelect, self).__init__(attrs, choices=STATE_CHOICES)
Index: tests/regressiontests/forms/localflavor/my.py
===================================================================
--- tests/regressiontests/forms/localflavor/my.py	(revision 0)
+++ tests/regressiontests/forms/localflavor/my.py	(revision 0)
@@ -0,0 +1,254 @@
+# Tests for the contrib/localflavor/ MY form fields.
+
+tests = r"""
+# MYIdentityCardNumberField ###################################################
+
+>>> from django.contrib.localflavor.my.forms import MYIdentityCardNumberField
+>>> f = MYIdentityCardNumberField()
+>>> f.clean('821226-10-5007')
+u'821226-10-5007'
+>>> f.clean('821226105007')
+u'821226-10-5007'
+>>> f.clean('821X2-1@-5007') # non-numeric
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('82122-10-5007') # missing digits
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821232-10-5007') # invalid date
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821226-99-5007') # invalid BP code
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> from django.contrib.localflavor.my.forms import MYIdentityCardNumberField
+>>> f = MYIdentityCardNumberField(required=False)
+>>> f.clean('821226-10-5007')
+u'821226-10-5007'
+>>> f.clean('821226105007')
+u'821226-10-5007'
+>>> f.clean('821X2-1@-5007') # non-numeric
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('82122-10-5007') # missing digits
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821232-10-5007') # invalid date
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821226-99-5007') # invalid BP code
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+>>> from django.contrib.localflavor.my.forms import MYIdentityCardNumberField
+>>> f = MYIdentityCardNumberField(check_bp_code=False)
+>>> f.clean('821226-10-5007')
+u'821226-10-5007'
+>>> f.clean('821226105007')
+u'821226-10-5007'
+>>> f.clean('821X2-1@-5007') # non-numeric
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('82122-10-5007') # missing digits
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821232-10-5007') # invalid date
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821226-99-5007') # ignores invalid BP code
+u'821226-99-5007'
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> from django.contrib.localflavor.my.forms import MYIdentityCardNumberField
+>>> f = MYIdentityCardNumberField(check_bp_code=False, required=False)
+>>> f.clean('821226-10-5007')
+u'821226-10-5007'
+>>> f.clean('821226105007')
+u'821226-10-5007'
+>>> f.clean('821X2-1@-5007') # non-numeric
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('82122-10-5007') # missing digits
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821232-10-5007') # invalid date
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a valid Identity Card number in XXXXXX-XX-XXXX format.']
+>>> f.clean('821226-99-5007') # ignores invalid BP code
+u'821226-99-5007'
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# MYPostCodeField #############################################################
+
+>>> from django.contrib.localflavor.my.forms import MYPostCodeField
+>>> f = MYPostCodeField()
+>>> f.clean('41150')
+u'41150'
+>>> f.clean(41150)
+u'41150'
+>>> f.clean(4115)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postcode in the format XXXXX.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> from django.contrib.localflavor.my.forms import MYPostCodeField
+>>> f = MYPostCodeField(required=False)
+>>> f.clean('41150')
+u'41150'
+>>> f.clean(41150)
+u'41150'
+>>> f.clean(4115)
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a postcode in the format XXXXX.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# MYPhoneNumberField ##########################################################
+
+>>> from django.contrib.localflavor.my.forms import MYPhoneNumberField
+>>> f = MYPhoneNumberField()
+>>> f.clean('+603-33430633')  # full, 2-digit prefix
+u'+603-33430633'
+>>> f.clean('+60333430633')   # no dash
+u'+60333430633'
+>>> f.clean('03-33430633')    # no icc
+u'03-33430633'
+>>> f.clean('0333430633')     # no dash, no icc
+u'0333430633'
+>>> f.clean('+6016-3469486')  # full, 3-digit prefix (mobile phones)
+u'+6016-3469486'
+>>> f.clean('+60163469486')   # no dash
+u'+60163469486'
+>>> f.clean('016-3469486')    # no icc
+u'016-3469486'
+>>> f.clean('0163469486')     # no dash, no icc
+u'0163469486'
+>>> f.clean('0163-469486')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a phone number in the format XXX-XXXXXXXX.']
+>>> f.clean('016-46948')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a phone number in the format XXX-XXXXXXXX.']
+>>> f.clean('016-469486789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a phone number in the format XXX-XXXXXXXX.']
+>>> f.clean(None)
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+>>> f.clean('')
+Traceback (most recent call last):
+...
+ValidationError: [u'This field is required.']
+
+>>> from django.contrib.localflavor.my.forms import MYPhoneNumberField
+>>> f = MYPhoneNumberField(required=False)
+>>> f.clean('+603-33430633')  # full, 2-digit prefix
+u'+603-33430633'
+>>> f.clean('+60333430633')   # no dash
+u'+60333430633'
+>>> f.clean('03-33430633')    # no icc
+u'03-33430633'
+>>> f.clean('0333430633')     # no dash, no icc
+u'0333430633'
+>>> f.clean('+6016-3469486')  # full, 3-digit prefix (mobile phones)
+u'+6016-3469486'
+>>> f.clean('+60163469486')   # no dash
+u'+60163469486'
+>>> f.clean('016-3469486')    # no icc
+u'016-3469486'
+>>> f.clean('0163469486')     # no dash, no icc
+u'0163469486'
+>>> f.clean('0163-469486')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a phone number in the format XXX-XXXXXXXX.']
+>>> f.clean('016-46948')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a phone number in the format XXX-XXXXXXXX.']
+>>> f.clean('016-469486789')
+Traceback (most recent call last):
+...
+ValidationError: [u'Enter a phone number in the format XXX-XXXXXXXX.']
+>>> f.clean(None)
+u''
+>>> f.clean('')
+u''
+
+# MYStateSelect ##########################################################
+
+>>> from django.contrib.localflavor.my.forms import MYStateSelect
+>>> f = MYStateSelect()
+>>> print f.render('state', 'SGR')
+<select name="state">
+<option value="JHR">Johor</option>
+<option value="KDH">Kedah</option>
+<option value="KTN">Kelantan</option>
+<option value="MLK">Melaka</option>
+<option value="NSN">Negeri Sembilan</option>
+<option value="PHG">Pahang </option>
+<option value="PNG">Pulau Pinang</option>
+<option value="PRK">Perak</option>
+<option value="PLS">Perlis</option>
+<option value="SGR" selected="selected">Selangor</option>
+<option value="TRG">Terengganu</option>
+<option value="SBH">Sabah</option>
+<option value="SRW">Sarawak</option>
+<option value="KUL">W.P Kuala Lumpur</option>
+<option value="LBN">W.P Labuan</option>
+<option value="PJY">W.P Putrajaya</option>
+<option value="WP">Wilayah Persekutuan</option>
+</select>
+"""
Index: tests/regressiontests/forms/tests.py
===================================================================
--- tests/regressiontests/forms/tests.py	(revision 11378)
+++ tests/regressiontests/forms/tests.py	(working copy)
@@ -19,6 +19,7 @@
 from localflavor.is_ import tests as localflavor_is_tests
 from localflavor.it import tests as localflavor_it_tests
 from localflavor.jp import tests as localflavor_jp_tests
+from localflavor.my import tests as localflavor_my_tests
 from localflavor.nl import tests as localflavor_nl_tests
 from localflavor.pl import tests as localflavor_pl_tests
 from localflavor.ro import tests as localflavor_ro_tests
@@ -53,6 +54,7 @@
     'localflavor_is_tests': localflavor_is_tests,
     'localflavor_it_tests': localflavor_it_tests,
     'localflavor_jp_tests': localflavor_jp_tests,
+    'localflavor_my_tests': localflavor_my_tests,
     'localflavor_nl_tests': localflavor_nl_tests,
     'localflavor_pl_tests': localflavor_pl_tests,
     'localflavor_ro_tests': localflavor_ro_tests,
Index: docs/ref/contrib/localflavor.txt
===================================================================
--- docs/ref/contrib/localflavor.txt	(revision 11378)
+++ docs/ref/contrib/localflavor.txt	(working copy)
@@ -52,6 +52,7 @@
     * India_
     * Italy_
     * Japan_
+    * Malaysia_
     * Mexico_
     * `The Netherlands`_
     * Norway_
@@ -93,6 +94,7 @@
 .. _India: `India (in\_)`_
 .. _Italy: `Italy (it)`_
 .. _Japan: `Japan (jp)`_
+.. _Malaysia: `Malaysia (my)`_
 .. _Mexico: `Mexico (mx)`_
 .. _Norway: `Norway (no)`_
 .. _Peru: `Peru (pe)`_
@@ -166,7 +168,7 @@
 
 .. class:: at.forms.ATStateSelect
 
-    A ``Select`` widget that uses a list of Austrian states as its choices. 
+    A ``Select`` widget that uses a list of Austrian states as its choices.
 
 .. class:: at.forms.ATSocialSecurityNumberField
 
@@ -406,6 +408,58 @@
 
     A ``Select`` widget that uses a list of Japanese prefectures as its choices.
 
+Malaysia (``my``)
+=================
+
+.. class:: my.forms.MYIdentityCardNumberField
+
+    A form field that validates its input is a valid Malaysian Identity Card number.
+    (YYMMDD-BP-XXXX)
+
+    The regular expression used is sourced from: http://en.wikipedia.org/wiki/MyKad
+
+    Takes one extra optional argument:
+
+    .. attribute:: MYIdentityCardNumberField.check_bp_code
+
+        If ``True``, field will validate the BP digits is a valid BP (birth place)
+        code (see above Wikipedia link for more information). Default: ``True``
+
+.. class:: my.forms.MYPostCodeField
+
+    A form field that validates its input is a 5-digit Malaysian postcode.
+
+.. class:: my.forms.MYPhoneNumberField
+
+    A form field that validates its input is a Malaysian phone number.
+
+    Accepts phone numbers in the format +6XXX-XXXXXXXX, with or without the
+    international country code prefix (+6)
+
+.. class:: my.forms.MYStateSelect
+
+    A ``Select`` widget that uses a list of Malaysian states as its choices.
+
+.. class:: my.models.IdentityCardNumberField
+
+    A model field that forms represent as a ``forms.MYIdentityCardNumberField``
+    and validates its input is a Malaysian Identity Card number.
+
+.. class:: my.models.PostCodeField
+
+    A model field that forms represent as a ``forms.MYPostCodeField`` and
+    validates its input is a Malaysian postcode.
+
+.. class:: my.models.PhoneNumberField
+
+    A model field that forms represent as a ``forms.MYPhoneNumberField`` and
+    validates its input is a Malaysian phone number.
+
+.. class:: my.models.StateField
+
+    A model field that forms represent as a ``forms.MYStateField`` and stores the
+    three-letter Malaysian state abbreviation in the database.
+
 Mexico (``mx``)
 ===============
 
@@ -516,7 +570,7 @@
 
 .. class:: ro.forms.ROIBANField
 
-    A form field that validates its input as a Romanian International Bank 
+    A form field that validates its input as a Romanian International Bank
     Account Number (IBAN). The valid format is ROXX-XXXX-XXXX-XXXX-XXXX-XXXX,
     with or without hyphens.
 
