DEV: Рубиновые тона
3.22K subscribers
143 photos
2 videos
8 files
972 links
Анонсы новых видео о программировании (Ruby/Rails, Solidity/Ethereum, Python, JS и не только), практические советы, обзор полезных инструментов и новости из мира IT
Download Telegram
Фото выше - это очень старое изображение, которое актуальности всё ещё не теряет. К сожалению, я не думаю, что в обозримом будущем оно вдруг станет неактуальным. С одной стороны, можно сказать, что нудный препод опять тут грузит какой-то ерундой, но с другой мы видим что будет, если нудные преподы грузить не будут.

Из другого - в субботу у нас заключительная игра года, надеюсь, будет интересно, вопросы я готовил достаточно долго. Сегодня был стрим, запись уже доступна. Возможно, на днях всё-таки устроим кулинарное суаре.

А фото к этому альбому было сделано в прошлом году, причём довольно неожиданно - я просто увидел подходящую локацию... Тут вообще город контрастов. https://soundcloud.com/ravens-die-laughing/a-place-to-call-home
👍17🌚2
В этом уроке по Rust мы поговорим о том, какие есть способы эффективной организации кода в проекте. Мы узнаем, что такое crates, модули, пакеты и как всё это между собой связано. Мы напишем несколько модулей, узнаем способы их подключения, а также рассмотрим подход с "прелюдией", который часто используется во многих библиотеках. https://www.youtube.com/watch?v=54m03X-_H3A
6❤‍🔥1
Что ж, конец года и время собрать набор финальных рекомендаций в уходящем 2023. 😄

Фильмы/анимация

* Kimitachi wa Dō Ikiru ka (видимо, финальная работа Миядзаки)

Книги (не специальная литература)

* Дом, который построил Свифт (Г. Горин)
* Носорог (Э. Ионеско)

Музыкальные альбомы

* Everything is alive (Slowdive)
* Linea Aspera LP (одноимённая группа)
* Electric Sun (VNV Nation)

Игры

* Phoenix Wright, Ace Attorney trilogy

Пока это всё, итоговый стрим будет 29 или 30 🤓
👍19
😁13😢4👍1😱1
Баллада об ASCII и Unicode

Эта статья написана с использованием руководства Джоэля Спольского, оригинал находится тут: https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/

Люди говорят, думают и пишут на естественных языках, многие из которых развивались на протяжении столетий. Конечно, и в мире IT мы хотели бы работать с текстовой информацией в привычном виде, но проблема заключается в том, что современные компьютеры понимают только нолики и единички: да - нет, правда - ложь, есть сигнал - нет сигнала. Впрочем, зачастую и людям тоже проще мыслить категориями "плохо - хорошо", а не разбираться в "градациях серого" некоего явления (о том, к чему это иногда приводит, я здесь рассуждать не буду). Так или иначе, наши тексты нужно каким-то образом хранить в цифровом виде, и сегодня мы попробуем понять, какие для этого используются подходы.

Разбираться во всём этом действительно полезно, потому что разных языков в мире очень много, вопрос интернационализации (про то, что это такое, я писал тут https://lokalise.com/blog/software-internationalization/) и локализации весьма актуален. Если перенестись в стародавние времена, когда была написана первая версия книги о языке C (а это было аж 40 лет назад), мир вообще был проще, трава зеленее, да и пиво не разбавляли. Основное внимание тогда уделяли обычным латинским символам безо всяких изысков (то есть без умляутов и прочего). Для них использовался стандарт кодировки ASCII (American Standard Code for Information Interchange), который изначально появился ещё в 60-каком-то-бородатом году.

Суть его проста: каждую букву можно закодировать числом, к примеру "A" - это 65, "B" - 66, и так далее. Это можно проверить и сейчас, к примеру написав "A".ord в Ruby (хотя тут есть особенность, об этом позднее). Кстати, заметим, что тип char в C - это фактически просто целое число. Так вот, каждый символ кодировался числами от 32 до 127, и это прекрасно влезало в 7 бит. Ну, а так как компьютеры оперировали и 8 битами без проблем, то оставался ещё целый свободный бит, с помощью которого можно было воплотить в жизнь самые сокровенные фантазии.

Числа от 0 до 31 включительно использовались для всяких тёмных дел, и назывались "контрольные символы" (aka "непечатные"). К примеру, один из символов мог заставить компьютер пищать в самом прямом смысле, другой означал табуляцию, и так далее (про \n, \t и прочие, думаю, все знают). Вот тут есть список всех кодов: https://www.robelle.com/library/smugbook/ascii.html

В общем, всё работало прекрасно, но только для тех, кто использовал английский язык. Естественно, многим пришла в голову простая мысль: "Раз коды от 128 до 255 ни для чего не используются, то их можно задействовать под что угодно!". К сожалению, понятие "что угодно" у всех разное. Поэтому в IBM-PC эти коды выводили на экран всякие чёрточки, загогулины и символы квадратного корня (вот тут весь набор https://www.jimprice.com/ascii-dos.gif). Но в других системах ситуация могла быть совершенно иной. Так, если на некоторых компьютерах код 130 выводил é, то на компьютерах, продающихся в Израиле, это число кодировало букву гимель`ג`. Получалось, что документ, составленный в США и отправленный в Израиль, мог неправильно выводится (вспомним о таком слове, как "résumé").

С кириллическими символами там тоже была целая история. На излёте СССР был принят ГОСТ, вводивший кодировку КОИ8, совместимую с ASCII, придумал её мэтр Чернов, который, к сожалению, скончался лет шесть тому назад. КОИ8 как раз задействовала "лишние" коды начиная со 128-го. Были разновидности этой кодировки, например, KOI8-RU, предназначенная сразу для русского, украинского и белорусского языков.
🔥9👍41
Но, в итоге, весь этот хаос было решено хоть немного упорядочить, и появился стандарт ANSI (American National Standards Institute), который чётко зафиксировал, что означают числа до 128 (хотя, честно говоря, это и так в основном соблюдалось). А вот с числами от 128 и далее поступили интересно, введя такую штуку, как "code pages" (cp, кодовые страницы). В таких страницах зашивались как обычные латинские символы, так и "нестандартные" буквы алфавита той страны, где вы находитесь. Поэтому на территории бывшего СССР многие нежно любили кодировку "windows cp 1251" ровно потому, что 1251 - это номер соответствующей страницы с кириллическими символами. В Израиле использовалась 862, в Греции - 737, и так далее, таких таблиц было выше крыши. Некоторые страницы могли использоваться сразу для нескольких языков, но это, скорее, исключение.

Проблема со всем этим в том, что одновременно можно использовать только одну страницу, то есть по-человечески работать сразу, например, с русским языком и ивритом на одном компьютере было нереально. Ну, разве что писать собственную программу и отображать некоторые символы в каком-то хитром виде.

В Азии же вообще царил трэш и угар, потому что в тамошних странах используются разнообразные иероглифы и всякие сложные закорючки, коих существует около 9000 - понятное дело, что в 8 бит это никак не уместить. Приходилось идти на всякие ухищрения и хранить некоторые символы в виде одного байта, некоторые - в виде двух, что весьма усложняло работу со строками (к примеру, не вполне ясно, как двигаться в строке назад). Да, конечно, для всего этого существовали вспомогательные функции, но в целом ситуация была так себе. Тем более, активно развивался интернет, тексты передавались с компьютера на компьютер, и нужно было этот бардак как-то разгребать.

Тогда появился Unicode, стандарт, цель которого была ввести единую кодировку для всех алфавитов, которые используются на планете (никаких клингонских языков там изначально, видимо, не планировалось, но в целом можно добавить их тоже).

Подход Unicode существенно отличался от того, что было раньше. Как мы уже выяснили, в ASCII ситуация простая: есть буква "A" латинского алфавита и есть её представление в виде битов "0100 0001", всё просто. В Unicode же букве сопоставляется code point (кодовая точка), которая в свою очередь имеет определённое представление в памяти или на диске, и это представление чётко не регламентировано.

Здесь есть важный момент. Мы понимаем, что латинская буква "A" - это не то же самое, что латинская "B". Отличается она и от маленькой буквы "a", так ведь? Но при этом "А", написанная курсивом или полужирным шрифтом, - это одно и то же. Больше того, "А", написанная шрифтом Arial, ничем со смысловой точки зрения не отличается от "А", написанной Helvetica. Значит ли это, что всякие "украшения" не важны и их можно никак не обозначать? Вообще-то, нет.

Думаю, многие знают, что в немецком языке есть символ ß. И это вовсе не "красивая буква B", а две буквы "s". В латышском языке имеется буква ķ, но это вовсе не "k" с чёрточкой для красоты, а отдельная буква. Поэтому "кот" будет именно "kaķis" ("катис", а не то, что вы подумали). Больше того, в иврите казалось бы одна и та же буква но написанная в разных местах и в разном "стиле" может иметь разное значение. Значит, эту информацию тоже нужно хранить! В общем, ситуация значительно сложнее, чем может показаться на первый взгляд, поэтому рабочая группа потратила целую кучу времени на поиски решения (тут есть и политические моменты, но нас это мало интересует).

Суть такова. Каждой "обычной букве без особых излишеств", то есть обычным "A", "B" и так далее, были присвоены специальные магические числа, записывающиеся в виде U+1234. Это магическое число - кодовая точка, U - понятное дело, Unicode, а сами цифры - шестнадцатиричные. На сайте Unicode все эти кодовые точки можно найти https://home.unicode.org/, к примеру, U+00BF - это такой перевёрнутный знак вопроса, использующися в испанском языке.
🔥11
Правда, всё несколько сложнее, потому что Unicode позволяет модифицировать символы и получать новые комбинации. То есть можно добавлять комбинируемые диакритические знаки и акценты (типичный пример - знак ударения). Хотя для многих комбинаций есть уже готовые коды, можно собирать новые символы самостоятельно. Грубо говоря, если взять букву "е" и прилепить к ней две точки, получится "ё". Ну, а букву é можно представить как U+0065 (обычная латинская "e") и U+0301 (акцент, применяемый к предыдущей букве). В принципе, это значит, что из любой "нормальной" буквы можно получить странного франкенштейна.

По факту, никакого строгого предела на количество символов в Unicode нет, хотя некоторая часть влезает в размерность 2 байта (то есть 65 536 штук). Вообще, валидных кодовых точек Unicode сейчас около 1 112 064, поэтому история о том, что Unicode оперирует только двумя байтами - миф.

Другой интересный вопрос - как эти кодовые точки должны быть представлены в памяти или в сообщениях (в тех же электронных письмах). Для этого используются кодировки. Первая идея была весьма простой - давайте хранить эти шестнадцатиричные числа в двухбайтовом виде! Тогда строка "Hello" будет представлена как U+0048 U+0065 U+006C U+006C U+006F, а в памяти - просто как 00 48 00 65 00 6C 00 6C 00 6F. Называется такой подход UCS-2 (потому что байта два, сообщает cpt. Obvious) или UTF-16 (потому что 16 бит). Собственно, отсюда и пошёл миф, что в Unicode может быть только два байта, не более.

С другой стороны можно ведь написать 48 00 65 00 6C 00 6C 00 6F 00 (то есть использовать low-endian или high-endian, про эти термины как-нибудь в другой раз поговорим) - тут уж в зависимости от того, с чем будет сподручнее работать процессору.

Выходит, форм хранения уже по крайней мере две. Как их тогда различать? Было предложено в начало каждой строки добавлять такую штуку как Unicode Byte Order Mark (то есть метку, сообщающую о порядке следования байтов). Она выглядела как "FE FF" или "FF FE" (во втором случае это значит, что нужно байты переставить местами).

Потом задались и другим вопросом - а чего нам хранить все эти нули? Это особенно актуально для англоговорящих разработчиков, которые в основном использовали коды до U+00FF. С их точки зрения выходило, что для хранения строк приходится тратить в два раза больше места непонятно зачем. Это не говоря о том, что с дедушкиных времён осталась гора документов в ANSI и ещё бог знает чём, и никому не хотелось это всё конвертировать. Короче, до какого-то момента Unicode не получал распространения, но часики-то оглушительно тикали, и ситуация становилась хуже.

Тогда в 2003 придумали концепцию UTF-8 (https://www.cl.cam.ac.uk/~mgk25/ucs/utf-8-history.txt), которую предлагалось использовать для хранения строк Unicode (то есть Unicode != UTF8). Как подсказывает цифра 8, создатели предложили хранить данных в октетах (байтах), но их число варьируется в зависимости от кодовой точки. Иными словами, от U+0000 до U+007F (от 0 до 127) используеся лишь один байт, от U+0080 до U+07FF - два байта, и так далее. Максимум - 4 байта информации, что позволяет закодировать весь миллион с хвостиком кодовых точек, имеющихся на данный момент.

Это весьма удобно для документов US-ASCII (US - United States), которые как раз используют символы до U+007F, то есть каждый символ как раз кодируется одним байтом. Из этого следует, что такие документы выглядят одинаково что в ASCII, что в Unicode, то есть 65 - это "А" в обоих случаях. Поэтому на самом деле "A".ord вернёт код буквы для UTF8 ("A".encoding почти наверняка сообщит как раз UTF8, во всяком случае, на любой нормальной системе). Да, небольшая проблема заключается в том, что всему остальному миру всё равно пришлось подстраиваться под новый стандарт, но что поделать, ka ir tas ir. Справедливости ради, английский - язык международного общения, плюс программы тоже пишутся латинскими буквами.
👍5🔥4
Таким образом, появилось уже две кодировки: UTF8 и UTF16. Только в одной каждый символ занимал по 2 байта и надо ещё было разбираться, в какой последовательности эти байты записаны, а в другой многие "привычные" буквы занимали всего байт. Несложно догадаться, какая кодировка получила большую популярность.

Кстати, это не единственные варианты. Был такой зверь как UTF7, похожий на UTF8, но гарантировавший, что старший бит всегда будет содержать ноль. Это было сделано, чтобы текстовые сообщения, отправляемые через некие странные почтовые системы, доходили по-нормальному (иначе там могло быть такое, что часть информации обрезалась). Был и UCS4 (по факту, UTF32), который хранил данные по 4 байта, но это казалось слишком большим расточительством.

Собственно, теперь мы понимаем, что всяких кодировок действительно придумали изрядное количество. Отсюда растут ноги у таинственных вопросительных знаков, которые можно периодически встретить в некоторых случаях - это символы, которые в данной кодировке не получается отобразить. Грубо говоря, можно взять любую кодовую точку из Unicode и попытаться отобразить в ASCII, но далеко не для всех чисел получится найти соответствие, что и приведёт к появлению весёлых вопросительных знаков.

Из всего этого получается интересный вывод: строка фактически не будет иметь никакого смысла, если мы не знаем, какую кодировку она использует. Хотя мы всё равно используем термин "plain text", он оказывается довольно размытым, потому что неясно в какой-такой кодировке он "plain". Если в нём используются любые символы после 127, то ASCII тут тоже не поможет. Именно поэтому при создании веб-страниц в тэге meta мы пишем кодировку (а если не пишем, то *стоило бы это делать*) - аналогичная история с письмами.

Кстати, с веб-страницами вообще очень интересная штука. Изначально планировалось, что кодировку будет сообщать веб-сервер при отправке HTML. Но ведь на одном сервере может лежать огромное количество страниц и сайтов, использующих самые разные языки. Значит, лучше, чтобы сама веб-страница говорила, что у неё за кодировка. Но позвольте, как тогда нам эту страницу начать читать, если мы не имеем представления о её кодировке? Выходит замкнутный круг - чтобы прочитать страницу, нужно знать кодировку, а чтобы знать кодировку, нужно начать читать страницу. К счастью, в начале файла HTML мы обычно используем "стандартные" символы до 127, а meta располагается в самом начале документа, так что её можно обработать без проблем, а потом уже использовать указанную кодировку. Если же meta нет, то некоторые браузеры могут пытаться "угадать" кодировку с помошью анализа встречающихся "нестандартных" символов, но иногда это может привести к тому, что вместо кириллических символов вылезут какие-нибудь китайские иероглифы. Поэтому, дамы и господа, мы с вами должны чтить кодировку.
👍16🔥10🆒2
В этом уроке мы поговорим о разнообразных кодировках, которые используются в программном обеспечении (ASCII, UTF8, UTF16, UCS4), а также о стандарте Unicode. Поговорим о том, зачем нам нужно столько разных кодировок, откуда они взялись, чем отличаются, и какие являются наиболее актуальными. https://www.youtube.com/watch?v=E5uQeik0tdc
👍17
Что ж, друзья, очередной год завершается. Его итоги мы подвели на недавнем стриме, было круто, благодарю всех за участие 😄 Надеюсь, сегодня вы проведёте этот вечер (ночь) со своими близкими и/или друзьями в приятной обстановке, и что будущий год будет куда лучше уходящего.

Я не очень верю в историю про "как встретишь - так проведёшь", но пока у меня проходит вечер за книгой по C 😂 Впрочем, потом сделаю что-нибудь вкусное, может, сумеем сегодня дозаписать одну композицию.

Отличных всем праздников, очень скоро увидимся!

P.S. Желающие сделать небольшой подарок каналу могут забустить его вот тут: https://t.iss.one/dev_in_ruby_colors?boost

P.P.S. Ну, и одна из наших старых композиций, в относительно "новогоднем" стиле 🤪 https://youtu.be/EwKNZ0dXC9E
🎄184🎉2🍾2👍1
Подвели итоги, а теперь я понимаю, что Габриэль Гарсия Маркес был прав...

"Все ещё продолжается понедельник, который был вчера. Погляди на небо, погляди на стены, погляди на бегонии. Сегодня опять понедельник". Привыкший к его чудачествам, Аурелиано не обратил на эти слова внимания. На следующий день, в среду, Хосе Аркадио Буэндиа снова появился в мастерской. "Просто несчастье какое-то, — сказал он. — Погляди на воздух, послушай, как звенит солнце, все в точности как вчера и позавчера. Сегодня опять понедельник".

Да, похоже, что из понедельника мы так и не выбрались, и не выберемся ещё очень долго. Есть желание покричать вместе с прекрасной дамой из Исландии.

https://www.youtube.com/watch?v=ImyPz4tqPFU
6😁1🤯1
Закон Амдала

Есть такой интересный закон, придуманный Джином Амдалом несколько десятилетий назад, его время от времени упоминают в контексте информационных систем, так что знать его суть не помешает.

Вообще, смысл данного закона очень простой: если мы "улучшаем" (ускоряем) *часть* некой системы, то общий эффект на быстродействие *всей* системы будет зависеть от того, насколько этот компонент был важен (то есть какой процент времени работа с ним занимает) и насколько мы его улучшили. Звучит как пассаж от капитана очевидность, но для оценки есть конкретная формула.

S = 1 / ( (1 - a) + (a / k) )


S - это то, насколько система в общем будет работать (в полтора, два раза и тд)

a - процент времени, который занимает работа конкретного компонента (выражается от 0.0 до 1.0)

k - фактор "улучшения"

Ну, к примеру, если мы знаем, что в нашей системе определённый компонент затрачивает 60% всего времени, необходимого для выполнения задачи, и мы улучшаем (ускоряем) этот компонент аж в 3 раза, то по факту S будет равно 1.67, то есть вся система ускорится чуть более, чем в полтора раза. То есть, казалось бы, мы очень серьёзно улучшили немалую часть системы, но окончательная выгода оказалась не такой уж большой.

Вообще говоря, этот закон может применяться далеко не только в информационных системах. К примеру, водитель грузовика знает, что ему нужно проехать 2500 километров, а скорость будет составлять условные 100 км/ч. Значит, вся поездка займёт 25 часов без учёта остановок.

Тогда вопрос - если мы предположим, что на участке пути в 1500 км можно ехать со скоростью в 150 км/ч, то насколько быстрее мы реально доедем? Ответить на этот вопрос несложно.

Участок в 1500 километров из всей дороги в 2500 км - это 60%, то есть a = 0.6.

Если мы едем на этом участке 150 км/ч, то k = 1.5, то есть в полтора раза быстрее.

Следовательно, финальное S равно 1.25, иными словами, приедем мы на 5 часов раньше.

Любопытно, что будет, если одну из частей системы мы сделаем настолько быстрой, что она практически не затрачивает никакого времени (это время настолько мало, что им можно пренебречь).

То есть что будет, если k равно бесконечности? В этом случае кусок a / k в формуле уходит, потому что значение будет стремиться к нулю. Тогда вся формула превращается в S = 1 / ( 1 - a ). Следовательно, даже если мы возьмём 60% всей системы и ускорим её так, что она будет выполнять необходимые операции мгновенно, всё равно общий рост производительности будет только 2.5 (т.к 1 / 0.4). Вот такая, понимаешь, загогулина.
11🔥7👍2
Коллеги из sitepoint прислали новый глобальный опрос для разработчиков (проводится каждый год). Думаю, снова поучаствовать, и вообще everyone is welcome - там вроде какие-то подарки обещают. Не реклама, просто хороший повод составить какую-никакую картину о мире it https://www.developereconomics.net/?member_id=sitepoint
2👌1
Big-endian и little-endian: порядок следования байтов и причём тут Гулливер

Частенько в руководствах и документации можно встретить термины big-endian и little-endian - да хотя бы в предыдущей записи про кодировки. Но что они вообще значат? На самом деле, всё довольно просто - это буквально война тупоконечников и остроконечников (я серьёзно).

Когда-то давно Джонатан Свифт написал роман "Путешествия Гулливера" - помните такой? Там была история, что, дескать, изначально варёные яйца лилипуты разбивали с тупого конца, но потом один из императоров умудрился себе порезать руку за завтраком, очищая яичко, после чего был издан указ: разбивать яйца только с острого конца, иначе будут применяться санкции.

Это привело к кровавым бунтам, тысячи человек пошли на казнь, так как, следуя заветам предков, всё равно разбивали яйца с тупого конца. По этой теме писались книги (хотя тупоконечную литературу запрещали), устраивались диспуты, и так далее, хотя в главном трактате некоего пророка было написано просто "разбивайте так, как вам больше нравится". Скажем прямо, в реальной жизни подобный идиотизм тоже встречается частенько. Это, конечно, едкая сатира, но она напрямую связана с нашим вопросом.

В оригинале "тупоконечники" - это big endians, а другая сторона - little endians. Отсюда и пошли эти термины - впервые в далёком 80-м году их употребил Дэнни Коэн, один из участников проекта ARPANet. Это была во многом шутка, но в итоге словечки прижились.

Предположим, у нас есть шестнадцатеричное число 0x12345678, и оно размещается в памяти, начиная с адреса 0x100 (про организацию память можно глянуть стрим https://youtube.com/live/1B6lBJUQ5q8?feature=share). Каждая двойка шестнадцатеричных чисел (12, 34 и тд) занимает по 8 бит (потому что каждое число hex требует 4 бит, 0x0 = 0b0000, 0xf = 0b1111).

Следовательно, на всё это число потребуется 4 байта или 32 бита (это называется "слово", "word"). Если принять, что каждый адрес в памяти указывает на 8 бит информации, то наше число займёт адреса с 0x100 по 0x103. Но вопрос в том, как их там разместить: справа налево или слева направо?

Можно сделать так:

100  101  102  103

12 34 56 78


но можно сделать и так:

100  101  102  103

78 56 34 12


То есть сначала может идти "самая значимая" пара (это 0x12), а может, наоборот, наименее значимая (0x78). Казалось бы, выбор очевиден, но можно вспомнить хотя бы о том, что некоторые народы читают справа налево, а кто-то вообще пишет столбиком. Так что, на самом деле, всё не так однозначно.

Поэтому подход, когда наиболее значимый байт идёт на первой позиции, называется "big endian" или BE (большинству из нас это куда привычнее), а "little endian" или LE - это когда сначала идёт наименее значимый байт. В разных машинах может задействоваться один из двух подходов, к примеру, многие устройства (но не все) от Oracle используют режим big endian. Впрочем, есть чипы вида bi-endian, которые умеют работать в обоих режимах.

Кроме того, многое зависит от самой операционной системы. Так, Android и iOS используют именно little-endian, с Windows в целом аналогичная история. В сетевых же протоколах, напротив, используется вариант big endian (принятую по сети информацию может потребоваться переставить обратно в вариант little endian, в зависимости от машины).

Дело же в том, что с технической точки зрения нет особенной разницы, какой вариант представления использовать, равно как и нет разницы, с какой стороны разбивать яйцо. Для обычных программистов знать, какой режим использует ОС, особенно не нужно, потому что это должны учитывать компиляторы.

Однако этот термин может встречаться в других местах - например, в документации Solidity и Ethereum, где рассказывается о хранении данных в слотах (к примеру, uint хранится в виде big endian, равно как и селектор функции в calldata).

Интересно, что сказал бы по этому поводу Свифт...
🔥8👍51