Недавно мне понадобилось получать классический Unix Timestamp (количество секунд с 1 января 1970 года). Полез в мануал Yabasic — а встроенной функции-то и нет! Нам доступны только строковые date$ и time$, которые выдают форматированный текст.
Обычно эту проблему решают двумя путями, и оба они, честно говоря, так себе:
1. Дёргать системную утилиту Linux через system$("date +%s"). Это намертво убивает переносимость кода (на Windows всё упадёт) и дико тормозит, потому что операционке приходится на каждый чих создавать новый процесс.
2. Использовать FFI и вызывать Си-функцию time из libc.so.6. Но это тоже вариант который на простых проектах не соответствует принципу K.I.S.S
Я решил подойти к вопросу инженерно и написать полностью автономный, быстрый и переносимый алгоритм на чистом BASIC, который будет работать везде.
Как устроен алгоритм
За основу взят проверенный астрономический алгоритм Поуппа (вычисление Юлианского дня для Григорианского календаря). Он без всяких тяжелых циклов за пару математических действий переводит дату и время в секунды.
Но при реализации я столкнулся с двумя серьезными подводными камнями, о которых хочу предупредить:
1. Опасность плавающих индексов (почему mid$ — это зло). Сначала я хотел быстро нарезать строку date$ через mid$. Но формат даты в Yabasic коварен: первый элемент — это день недели. В зависимости от того, однозначное это число или текст в вашей сборке ОС, жесткие индексы mid$ могут съехать на дефисы. Функция val() вернет ноль, и вся математика превратится в тыкву. Поэтому использовать нужно только split() по дефисам.
2. Проблема полуночного сплита (Race Condition). Если вызвать сначала num_d = split(date$,...), а на следующей строчке num_t = split(time$,...), есть микроскопический шанс, что первая строчка выполнится в 23:59:59, а вторая — уже в 00:00:00 следующего дня. В итоге код посчитает время со старой датой и улетит в прошлое на целые сутки! В финальном коде я добавил циклическую защиту repeat...until, которая проверяет, не сменилась ли дата в момент парсинга.
Бенчмарк на 2 000 000 итераций
Мне стало интересно, насколько такой расчет нагружает интерпретатор. Я написал сравнительный тест: гонял «лоб в лоб» чистый split против mid$.
Результаты на 2 миллионах циклов:
* Метод mid$ (прямые строки): 65 секунд.
* Метод split() (безопасные массивы): 73 секунды.
Да, mid$ на 10% быстрее, но использовать его в продакшене нельзя из-за нестабильности формата строки. А вот безопасный метод на split() выдает результат всего за 36.5 микросекунд. Это значит, что код может выполнять больше 27 000 расчетов в секунду вообще без значительной нагрузки на CPU!
Выкладываю готовый код сравнительного бенчмарка. Вы можете запустить его у себя и использовать функцию get_pure_unix_time в своих проектах.
Где это применять на практике?
Обычно эту проблему решают двумя путями, и оба они, честно говоря, так себе:
1. Дёргать системную утилиту Linux через system$("date +%s"). Это намертво убивает переносимость кода (на Windows всё упадёт) и дико тормозит, потому что операционке приходится на каждый чих создавать новый процесс.
2. Использовать FFI и вызывать Си-функцию time из libc.so.6. Но это тоже вариант который на простых проектах не соответствует принципу K.I.S.S
Я решил подойти к вопросу инженерно и написать полностью автономный, быстрый и переносимый алгоритм на чистом BASIC, который будет работать везде.
Как устроен алгоритм
За основу взят проверенный астрономический алгоритм Поуппа (вычисление Юлианского дня для Григорианского календаря). Он без всяких тяжелых циклов за пару математических действий переводит дату и время в секунды.
Но при реализации я столкнулся с двумя серьезными подводными камнями, о которых хочу предупредить:
1. Опасность плавающих индексов (почему mid$ — это зло). Сначала я хотел быстро нарезать строку date$ через mid$. Но формат даты в Yabasic коварен: первый элемент — это день недели. В зависимости от того, однозначное это число или текст в вашей сборке ОС, жесткие индексы mid$ могут съехать на дефисы. Функция val() вернет ноль, и вся математика превратится в тыкву. Поэтому использовать нужно только split() по дефисам.
2. Проблема полуночного сплита (Race Condition). Если вызвать сначала num_d = split(date$,...), а на следующей строчке num_t = split(time$,...), есть микроскопический шанс, что первая строчка выполнится в 23:59:59, а вторая — уже в 00:00:00 следующего дня. В итоге код посчитает время со старой датой и улетит в прошлое на целые сутки! В финальном коде я добавил циклическую защиту repeat...until, которая проверяет, не сменилась ли дата в момент парсинга.
Бенчмарк на 2 000 000 итераций
Мне стало интересно, насколько такой расчет нагружает интерпретатор. Я написал сравнительный тест: гонял «лоб в лоб» чистый split против mid$.
Результаты на 2 миллионах циклов:
* Метод mid$ (прямые строки): 65 секунд.
* Метод split() (безопасные массивы): 73 секунды.
Да, mid$ на 10% быстрее, но использовать его в продакшене нельзя из-за нестабильности формата строки. А вот безопасный метод на split() выдает результат всего за 36.5 микросекунд. Это значит, что код может выполнять больше 27 000 расчетов в секунду вообще без значительной нагрузки на CPU!
Выкладываю готовый код сравнительного бенчмарка. Вы можете запустить его у себя и использовать функцию get_pure_unix_time в своих проектах.
Код: Выделить всё
// Укажите смещение вашего часового пояса относительно Гринвича (UTC)
// Например: 3 для Москвы (MSK), 0 для самого Гринвича
tz_offset_hours = 3
iterations = 2000000
print "=== СРАВНИТЕЛЬНЫЙ БЕНЧМАРК (2 000 000 итераций) ==="
print "Сравниваем безопасный Метод Массивов (split) и Метод Строк (mid$)"
print "Пожалуйста, подождите..."
print ""
// -------------------------------------------------------------------
// ТЕСТ 1: Безопасный метод через split() и динамические массивы
// -------------------------------------------------------------------
start_1 = peek("secondsrunning")
for i = 1 to iterations
res1 = get_unix_split(tz_offset_hours)
next i
end_1 = peek("secondsrunning")
time_split = end_1 - start_1
print "1. Метод (split + массивы с защитой от полуночи):"
print " Результат (GMT): ", res1
print " Время: ", time_split, " сек."
print ""
// -------------------------------------------------------------------
// ТЕСТ 2: Опасный метод через прямое вырезание подстрок mid$
// -------------------------------------------------------------------
start_2 = peek("secondsrunning")
for i = 1 to iterations
res2 = get_unix_mid(tz_offset_hours)
next i
end_2 = peek("secondsrunning")
time_mid = end_2 - start_2
print "2. Метод (чистый mid$ с жесткими индексами):"
print " Результат (GMT): ", res2
print " Время: ", time_mid, " sec."
print ""
print "=================================================="
// === АЛГОРИТМ 1: БЕЗОПАСНЫЙ (РЕКОМЕНДУЕМЫЙ) ===
sub get_unix_split(tz)
dim d_parts$(1), t_parts$(1)
local d1$, d2$, t$
local num_d, num_t, month, day, year, hour, minute, second
local a, y, m, total_days, local_ts
// Защита от "полуночного сплита"
repeat
d1$ = date$
t$ = time$
d2$ = date$
until (d1$ = d2$)
num_d = split(d1$, d_parts$(), "-")
num_t = split(t$, t_parts$(), "-")
month = val(d_parts$(2))
day = val(d_parts$(3))
year = val(d_parts$(4))
hour = val(t_parts$(1))
minute = val(t_parts$(2))
second = val(t_parts$(3))
a = int((14 - month) / 12)
y = year + 4800 - a
m = month + 12 * a - 3
total_days = day + int((153 * m + 2) / 5) + 365 * y + int(y / 4) - int(y / 100) + int(y / 400) - 32045
return (total_days - 2440588) * 86400 + hour * 3600 + minute * 60 + second - (tz * 3600)
end sub
// === АЛГОРИТМ 2: ОПАСНЫЙ (ДЛЯ СРАВНЕНИЯ СКОРОСТИ) ===
sub get_unix_mid(tz)
local d$, t$, month, day, year, hour, minute, second
local a, y, m, total_days
d$ = date$
t$ = time$
month = val(mid$(d$, 3, 2))
day = val(mid$(d$, 6, 2))
year = val(mid$(d$, 9, 4))
hour = val(mid$(t$, 1, 2))
minute = val(mid$(t$, 4, 2))
second = val(mid$(t$, 7, 2))
a = int((14 - month) / 12)
y = year + 4800 - a
m = month + 12 * a - 3
total_days = day + int((153 * m + 2) / 5) + 365 * y + int(y / 4) - int(y / 100) + int(y / 400) - 32045
return (total_days - 2440588) * 86400 + hour * 3600 + minute * 60 + second - (tz * 3600)
end sub
- Логирование: Писать в файлы текстовую дату неудобно для последующей обработки. Намного проще сохранять один integer (например, 1789224672). Его потом легко сортировать, фильтровать и скармливать базам данных.
- Разница между датами: Чтобы узнать, сколько дней прошло между двумя событиями, просто вычитаем один timestamp из другого и делим полученное число на 86400. Никакой возни с високосными годами и разным количеством дней в месяцах.
- Планировщики задач: Легко делать условия для выполнения действий по времени: if (get_unix_split(3) >= target_time) then....