حامد یک مطلبی نوشته و لینکش رو ارسال کرده با عنوان تبدیل اعداد انگلیسی به فارسی. با این که به نظر من تبدیل عددها به فارسی کار جالبی (حداقل در وب) نیست، کنجکاو شدم ببینم که واقعا سریعترین روش برای انجام این کار چیه. این هم نتیجهاش:
قبل از هر کاری متد زیر رو تعریف میکنیم تا به کمک اون بتونیم کارایی یک تابع رو محاسبه کنیم. این متد یک تابع (که رشته میگیره و رشته برمیگردونه) رو به عنوان پارامتر دریافت میکنه و اون رو به تعداد دفعاتی که مشخص شده اجرا میکنه و مدت زمانی که این کار طول میکشه رو محاسبه میکنه تا به یک تقریب نسبتا خوب از کارایی این تابع برسه.
void Benchmark(Func<string, string> func) {
string input = File.ReadAllText(@"D:\WINDOWS\updspapi.log");
int times = 1000;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < times; i++)
func(input);
stopwatch.Stop();
Console.WriteLine("{0,10:#,###} Ticks", stopwatch.ElapsedTicks / times);
}
اول از همه میریم سراغ Regular Expressions. در این روش از متد Replace از کلاس Regex استفاده میشه تا تمام رقمهای انگلیسی (با الگوی [9-0]) با معادل فارسیشون جایگزین بشن.
// Regex
Regex regex = new Regex(@"[0-9]");
Benchmark(str => {
return regex.Replace(str, match => ((char)(match.Value[0] + 1728)).ToString());
});
نتیجه: 2,100 تیک برای هر اجرا. کند بودن این روش از همون اول کار کاملا مشخص بود ولی به خاطر کامل شدن مطلب حدفش نکردم. RegExها برای پیدا کردن الگوهای پیچیده در متنها مناسب هستند و برای ما که فقط با ده رقم 0 تا 9 به طور مستقل کار داریم گزینه خوبی نیستند.
در روش بعدی خودمون به طور دستی توی متن جستجو میکنیم و هر رقم انگلیسی رو که پیدا کنیم با معادل فارسیش جایگزین میکنیم.
// String Block
char[] digits = Enumerable.Range(48, 10).Select(i => (char)i).ToArray();
Benchmark(str => {
while (true) {
int index = str.IndexOfAny(digits);
if (index == -1)
break;
str = str.Substring(0, index) + ((char)(str[index] + 1728)).ToString() + str.Substring(index + 1);
}
return str;
});
نتیجه: 48,700 تیک برای هر اجرا. خیلی بدتر شد! علت اصلی کندی این روش به غیر از جستجوی چند باره متن توسط تابع IndexOfAny) اینه که به ازای هر رقم پیدا شده، 4 شیئ از نوع String ساخته و دور ریخته میشه. هر چی هم متن برزگتر باشه تاثیر منفی این کار بیشتره.
حالا میریم سراغ دو تا روشی که حامد هم بهشون اشاره کرده. در روش اول متد Replace از کلاس String ده بار فراخوانی میشه و در هر بار یک رقم انگلیسی رو با معادل فارسیش جایگزین میکنیم.
// String.Replace
Benchmark(str => {
for (char i = (char)48; i <= 57; i++)
str = str.Replace(i, (char)(i + 1728));
return str;
});
نتیجه: 740 تیک برای هر اجرا. بد نیست ولی توجه داشته باشید که باز هم در این روش 9 رشته موقت ساخته میشه و تابع Replace مجبوره در هر اجرا یکبار کل متن رو جستجو کنه.
در روش بعدی کاراکترها رو تک تک رو بررسی میکنیم و اگه به رقم انگلیسی برخورد کردیم، معادلش رو جایگزین میکنم. در ضمن برای جلوگیری از ساختن و دور ریختن مکرر رشتهها از کلاس StringBuilder استفاده میکنیم.
// StringBuilder
Benchmark(str => {
StringBuilder sb = new StringBuilder(str.Length);
for (int i = 0; i < str.Length; i++)
if (str[i] >= 48 && str[i] <= 57)
sb.Append((char)(str[i] + 1728));
else
sb.Append(str[i]);
return sb.ToString();
});
نتیجه: 360 تیک برای هر اجرا. خیلی بهتر شد ولی هنوز جای کار داره. از اونجایی که ما تنها لازم داریم یک کاراکتر رو با فقط و فقط یک کاراکتر دیگه جایگزین کنیم، پس میشه خیلی بهینهتر عمل کرد. در روش بعدی ابتدا یک آرایه موقت از نوع char و به طول رشته اصلی درست میکنیم و بعد مثل روش قبل کاراکترها رو تک تک رو بررسی میکنیم و حاصل رو توی آرایه موقت کپی میکنیم. در آخر هم نتیجه رو به صورت String بر میگردونیم.
// Char[]
Benchmark(str => {
char[] temp = new char[str.Length];
for (int i = 0; i < str.Length; i++) {
char value = str[i];
if (value >= 48 && value <= 57)
temp[i] = (char)(value + 1728);
else
temp[i] = value;
}
return new String(temp);
});
نتیجه: 150 تیک برای هر اجرا. کمتر از نصف روش قبل. در واقع این روش سادهترین و کاراترین روشی هستش که من بهش رسیدم. برای بهینهتر کردن این روش میشه از اشارهگرها هم استفاده کرد:
// Char[] *Unsafe
Benchmark(str => {
unsafe {
fixed (char* src = str) {
fixed (char* temp = new char[str.Length]) {
for (int i = 0; i < str.Length; i++) {
char value = *(src + i);
if (value >= 48 && value <= 57)
*(temp + i) = (char)(value + 1728);
else
*(temp + i) = value;
}
return new String(temp);
}
}
}
});
نتیجه: 145 تیک برای هر اجرا. اختلافش با روش قبلی خیلی کمه و به نظر من اصلا به دردسرش نمیارزه. یک نکتهای هم که هست اینه که این روش در بعضی حالتها کندتر از روش قبلی عمل میکنه! کلا بهتره اجازه بدیم خود NET. حافظه رو مدیریت کنه و ما دخالت نکنیم.
یک کاره دیگه هم که میشه کرد اینه که به جای این که کاراکترهای غیر-رقمی رو تک تک کپی کنیم، همه رو نگه داریم و با هم کپی کنیم:
// Char[] Block
Benchmark(str => {
char[] src = str.ToCharArray();
char[] temp = new char[str.Length];
int lastIndex = 0;
for (int i = 0; i < src.Length; i++) {
char val = src[i];
if (val < 48 || val > 57)
continue;
Array.Copy(src, lastIndex, temp, lastIndex, i - lastIndex);
temp[i] = (char)(val + 1728);
lastIndex = i + 1;
}
Array.Copy(src, lastIndex, temp, lastIndex, src.Length - lastIndex);
return new String(temp);
});
نتیجه: 280 تیک برای هر اجرا. برخلاف انتظار، کندتر از دو روش قبلی شد؛ دلیل اصلیش هم استفاده از متد Array.Copy هست. Buffer.BlockCopy هم فرق چندانی نداره.
به روز رسانی: در روش RegEx الگوی d\ رو با [9-0] عوض کردم تا فقط شامل ارقام انگلیسی بشه.