Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Fixing Optional Calendars for Persian Culture in .NET

0.00/5 (No votes)
30 Mar 2013 1  
This article describes how to fix CultureInfo for Persian culture so that .NET developers can use PersianCalendar.

Introduction

This article describes how Persian culture can be fixed so that Persian calendar can be used with this culture.

It's been annoying for programmers in Persian language that the favorite simple code line:

CultureInfo.CurrentCulture.DateTimeFormat.Calendar = new PersianCalendar()

doesn't work and raises an ArgumentOutOfRange exception. This article shows how to get rid of this.

Background

Although .NET provides both PersianCalendar and Persian CultureInfo, there are some issues in Persian culture that prevents the smooth use of Persian culture. You can see the user complaints under the PersianCalendar page on MSDN. The questions are:

  • How to set PersianCalendar on a DateTimeFormatInfo?
  • Why calendar resources such as month names are not correct and how to fix them?
  • Why Persian calendar is not included in the set of Optional Calendars in Persian culture?

These problems have already been noted by Reza Taroosheh in his great article How to set PersianCalendar to CultureInfo. He also provides a PersianCalendarHelper class to fix these issues based on Reflection. This class reasonably addresses the first two issues. It adds Persian month names and also sets the Persian calendar on the DateTimeFormat. This normally suffices to have a working Persian culture for a .NET application.

But it fails to add the Persian calendar to the set of optional calendars. Troosheh suggests to install a new custom locale to solve this issue. In this article, a new approach is introduced to fix the optional calendars issue.

Fixing Optional Calendars

Inspecting internal codes of CultureInfo by Reflection reveals that the array of optional calendars is actually read from a TableRecord structure from the Windows stored locale data. The actual location of this array is back calculated, so that one can fix this array. Actually, the memory is in a protected area, so that we should use the VirtualProtect API before trying to fix the array.

public static  CultureInfo FixOptionalCalendars(CultureInfo culture, int CalenadrIndex)
{
    InvokeHelper ivCultureInfo = new InvokeHelper(culture);
    if (!ivCultureInfo.HasField("m_cultureTableRecord"))
    {
        // This is .Net 4. 
        return _FixOptionalCalendars4(culture, CalenadrIndex);
    }

    InvokeHelper ivTableRecord = 
      new InvokeHelper(ivCultureInfo.GetField("m_cultureTableRecord"));
    // Get the m_pData pointer as *void
    System.Reflection.Pointer m_pData = 
      (System.Reflection.Pointer)ivTableRecord.GetField("m_pData");
    ConstructorInfo _intPtrCtor = typeof(IntPtr).GetConstructor(
                    new Type[] { Type.GetType("System.Void*") });
    // Construct a new IntPtr
    IntPtr DataIntPtr = (IntPtr)_intPtrCtor.Invoke(new object[1] { m_pData });
        
    Type TCultureTableData = Type.GetType("System.Globalization.CultureTableData");
    // Convert the Pointer class to object if type CultureTableData to work with
    // reflection API.
    Object oCultureTableData = 
      System.Runtime.InteropServices.Marshal.PtrToStructure(DataIntPtr, TCultureTableData);
    InvokeHelper ivCultureTableData = new InvokeHelper(oCultureTableData);
    // Get waCalendars pointer
    uint waCalendars = (uint)ivCultureTableData.GetField("waCalendars");
    object IOPTIONALCALENDARS = ivTableRecord.GetProperty("IOPTIONALCALENDARS");

    // Get m_Pool pointer
    System.Reflection.Pointer m_pool = (
      System.Reflection.Pointer)ivTableRecord.GetField("m_pPool");

    IntPtr PoolInPtr = (IntPtr)_intPtrCtor.Invoke(new object[1] { m_pool });
    // Add the waCalendars offset to pool pointer
    IntPtr shortArrayPtr = new IntPtr((PoolInPtr.ToInt64() + waCalendars*sizeof(ushort)));
    short[] shortArray = new short[1];
    // Now shortArray points to an arry of short integers.
    // Go to read the first value which is the number of elements.
    // Marshal array to read elements.
    System.Runtime.InteropServices.Marshal.Copy(shortArrayPtr, shortArray, 0, 1);
    // shortArray[0] is the number of optional calendars.
    short[] calArray = new short[shortArray[0]];
    // Add one element of short type to point to array of calendars
    IntPtr calArrayPtr = new IntPtr(shortArrayPtr.ToInt64() + sizeof(short));
    // Finally read the array
    System.Runtime.InteropServices.Marshal.Copy(calArrayPtr, calArray, 0, shortArray[0]);

    uint old;
    VirtualProtect(calArrayPtr, 100, 0x4, out old);
    calArray[CalenadrIndex] = 0x16;
    System.Runtime.InteropServices.Marshal.Copy(calArray, 0, calArrayPtr, calArray.Length);
    VirtualProtect(calArrayPtr, 100, old, out old);

    return culture; 
}

The InvokeHelper class in the above code is just a helper class to get the private fields by Reflection.

Optional Calendars in .NET 4

The TableRecord approach has been depreciated in .NET 4. Now OptionalCalendars is a managed array that can be easily set through Reflection:

        private static CultureInfo _FixOptionalCalendars4(CultureInfo culture, int CalenadrIndex)
        {
            FieldInfo cultureDataField = typeof(CultureInfo).GetField("m_cultureData",
                 BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );
            Object cultureData = cultureDataField.GetValue(culture);
            //
            PropertyInfo calendarIDProp = cultureData.GetType().GetProperty("CalendarIds",
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            int[] calendars = (int[]) calendarIDProp.GetValue(cultureData,null);
            if (CalenadrIndex >= 0 && CalenadrIndex < calendars.Length)
                calendars[CalenadrIndex] = 0x16;
            return culture;
        }

How to Use the Code

One may use the static methods of PersianCalendarHelper to fix the Persian culture. The simplest way is calling the FixAndSetCurrentCulture method somewhere in application startup. This will set the current thread culture to a new fixed culture.

PersianCultureHelper.FixAndSetCurrentCulture();

Another approach will be using the FixPersianCulture method with the appropriate options. This way the programmer can control things to be fixed. For instance:

CultureInfo culture = PersianCultureHelper.FixPersianCulture(null,foptOptionalCalendars)

only fixes the optional calendars array.

Points of Interest

The m_pData field of the TableRecord class is actually an unsafe pointer. It was a challenge to cast it to an object so that Reflection could be used:

System.Reflection.Pointer m_pData = 
      (System.Reflection.Pointer)ivTableRecord.GetField("m_pData");
ConstructorInfo _intPtrCtor = typeof(IntPtr).GetConstructor(
                new Type[] { Type.GetType("System.Void*") });
// Construct a new IntPtr
IntPtr DataIntPtr = (IntPtr)_intPtrCtor.Invoke(new object[1] { m_pData });
        
Type TCultureTableData = Type.GetType("System.Globalization.CultureTableData");
// Convert the Pointer class to object if type CultureTableData to work with
// reflection API.
Object oCultureTableData = 
  System.Runtime.InteropServices.Marshal.PtrToStructure(DataIntPtr, TCultureTableData);
InvokeHelper ivCultureTableData = new InvokeHelper(oCultureTableData);

Acknowledgments

Again I should acknowledge the work of Taroosheh, this work is only a contribution to his effort.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here