Программисты пишут программы, которые потом могут использоваться в разных странах с разными стандартами и традициями, поэтому им приходиться обращать внимание на такие детали, которые в обычной жизни не замечаются в силу своей очевидности.
Вот, например, у меня на компьютере сегодняшняя дата пишется так — 06.01.2023, а на одном из моих серверов в уголке экрана светится 01/06/23. Это американский формат, и я каждый раз мучительно вспоминаю, 01/06 это первое июня или всё таки шестое января.
Ещё одно национальное отличие — символ отделения целой и дробной части в числах. В России по традиции и в соответствии со стандартами надо отделять дробную часть запятой (Пи = 3,14), а американцы (и вслед за ними программисты) используют десятичную точку (Pi = 3.14). Из-за этого различия иногда случаются разные казусы.
В русском Excel в разделе "Сохранить как" есть формат файла "CSV (разделитель — запятая)". Если вы сохраните таблицу в этом формате, то получите текстовый файл, в котором разделителем колонок будет… Нет, не запятая, как было обещано, а точка с запятой, поскольку запятая уже занята как разделитель дробной части. Если же вы попробуете открыть "американский" CSV-файл (с разделителем запятой и десятичной точкой), скачанный из интернета, то у вас возникнет сразу несколько проблем. Во-первых, все столбцы сольются в один, поскольку не будет распознан разделитель столбцов. Это можно преодолеть, предварительно вставив в начало файла магическую строку sep=,
, но тут вы нарветесь на вторую проблему — теперь ваши числа с десятичными точками будут восприняты не как числа, а как текст. В принципе, можно выделить колонки с числами и сделать во всех ячейках замену точки на запятую, но у себя я наткнулся на третью проблему — при открытии файла Excel попытался угадать формат ячеек и безвозвратно заменил небольшие числа вида "1.2" на даты — "1 февраля 2023".
Для того, чтобы упростить работу с национальными форматами, программисты разработали специальные базы данных, автоматизирующие процесс локализации программ (по-английскии кратко пишется L10N поскольку L[ocalizatio]N). На программистском жаргоне эти базы называются локалями и обозначаются либо названием языка ("Русский"), либо аббревиатурой, составленной из названия языка и названия страны (en_US — американский английский). В Windows можно выбрать стандартную локаль (ищем в настройках Windows 10 слово "Регион") и (при желании) поправить отдельные ее параметры ("Панель управления\Часы и регион"). В командной строке Linux локаль в целом выбирается установкой значения переменной LANG или LC_ALL, а частичные изменения настраиваются через переменные LC_NUMERIC, LC_TIME и т.д. У меня, например, в Windows отдельно настроена десятичная точка, а в Linux установлен программистский языковой стандарт для чисел LC_NUMERIC=C
. В Excel в дополнительных параметрах присутствует отдельный пункт для разделителя целой и дробной частей числа — можно выбирать между разделителем из системной локали и произвольным символом, например, точкой.
А теперь примеры программистских ошибок.
В 2000-х годах в суперкомпьютерном центра МГУ эксплуатировался NUMA сервер от IBM. Сейчас не могу вспомнить, какой именно модели, поскольку в последний раз получал к нему доступ десять лет назад в 2013 году. На сервере использовался родной компилятор от IBM и этот компилятор имел замечательную ошибку. При установленной русской локали ru_RU он не мог распознать вещественные числа в программах, поскольку считал, что разделителем десятичных знаков должна быть запятая. Программы не собирались, а компилятор выдавал сообщение о недопустимом символе в позициях, в которых стояли десятичные точки. Когда моя коллега впервые наткнулась на эту ошибку у меня ушло минут 40 для того, чтобы осознать, что дело не в кодировке и не в невидимых символах в окрестности десятичных точек, а именно в локали. Ради интереса, я поменял все десятичные точки на запятые, но это не помогло: в зависимости от контекста, эти запятые интерпретировались либо как разделители параметров функции, либо как оператор «запятая», что, в общем-то, было вполне ожидаемо.
Недавно наткнулся в собственной программе на то, что при установленной русской локали вывод вещественных чисел в C++ двумя разными способами дает разные результаты:
#include <locale.h>
#include <unistd.h>
#include <stdio.h>
#include <iostream>
int main(int argc, char *argv[]){
float pi=3.14;
// Укажем, что мы хотим форматировать числа, даты, валюты и пр.
// по правилам русского языка
setlocale(LC_ALL, "Russian");
// libc по умолчанию использует пользовательские настройки
printf(«%f\n», pi); // 3,14
// Стандартные потоки по умолчанию используют локаль POSIX
std::cout << pi << std::endl; // 3.14
// Для вывода в cout по национальным правилам
// надо проделать не совсем очевидные вещи:
// Создание копии пользовательской локали
std::locale loc("");
std::cout << "Locale name = " << loc.name() << std::endl; //Russian
// Установка формата вывода в соответствии с указанной локалью
std::cout.imbue(loc);
// Вот теперь можно выводить числа в национальном формате
std::cout << pi << std::endl; // 3,14
return 0;
}
Давно хотел написать эту заметку, но объём материала казался каким-то незначительным. Сегодня наткнулся в блоге у Евгения Степанищева на ещё один интересный пример, на этот раз из ранних версий PHP4, и решил, что пора писать обобщающий материал. Хотя бы для того, чтобы обратить на эту проблему внимание начинающих программистов.
Пример от Евгения (можно поиграться в песочнице). Установка локали в PHP 4.2.3 влияет на конвертацию строки в число:
<?php
var_dump('1,4' + '0,7'); // int(1)
var_dump('1,4' + '0.7'); // float(1.7)
setlocale(LC_ALL, 'ru_RU.koi8r');
var_dump('1,4' + '0,7'); //float(2,1)
var_dump('1,4' + '0.7'); // float(1.4)