В тексте новости на сайте опубликована ссылка на документ UPGRADING, где описываются несовместимости «семерки». Помимо выкинутых, как обычно, DEPRECATED функций и расширений (исчезла масса SAPI модулей), заметные изменения произошли в самом языке. Ниже — о части из них.
По порядку из официального документа:
Изменения в обработке переменных
1. Косвенные ссылки на переменные, свойства и методы теперь разбираются слева направо. Восстановить прежний порядок можно фигурными скобками.
$$foo['bar']['baz'] // разбирается как ($$foo)['bar']['baz'] - ранее как ${$foo['bar']['baz']}
$foo->$bar['baz'] // разбирается как ($foo->$bar)['baz'] - ранее как $foo->{$bar['baz']}
$foo->$bar['baz']() // разбирается как ($foo->$bar)['baz']() - ранее как $foo->{$bar['baz']}()
Foo::$bar['baz']() // разбирается как (Foo::$bar)['baz']() - ранее как Foo::{$bar['baz']}()
2. Ключевое слово global принимает только простые переменные. Вместо global $$foo->bar следует писать global ${$foo->bar}
3. Скобки вокруг переменных или вызовов функций больше не влияют на поведение. Например, код, где результат функции передается по ссылке:
function getArray() { return [1, 2, 3]; }
$last = array_pop(getArray());
// Strict Standards: Only variables should be passed by reference
$last = array_pop((getArray()));
// Strict Standards: Only variables should be passed by reference
сейчас выбросит ошибку strict standards вне зависимости от скобок (ранее во втором вызове ее не было).
4. Элементы массива или свойства объекта, которые были автоматически созданы во время присвоений по ссылке сейчас будут иметь другой порядок. Код:
$array = [];
$array["a"] =& $array["b"];
$array["b"] = 1;
var_dump($array);
сейчас сгенерирует массив [«a» => 1, «b» => 1], тогда как ранее был [«b» => 1, «a» => 1].
Изменения в обработке list()
1. list() теперь присваивает переменные в прямом порядке (ранее — в обратном), например:
list($array[], $array[], $array[]) = [1, 2, 3];
var_dump($array);
сейчас выдаст $array == [1, 2, 3] вместо [3, 2, 1]. Изменился только порядок присвоения, т.е. нормальное использование list() не затронуто.
2. Присвоения с пустым списком list() стали запрещены, следующие выражения ошибочны:
list() = $a;
list(,,) = $a;
list($x, list(), $y) = $a;
3. list() больше не поддерживает распаковку строк (ранее поддерживалась в некоторых случаях). Код:
$string = "xy";
list($x, $y) = $string;
установит $x и $y в значение null (без предупреждений) вместо $x == «x» и $y == «y». Более того, list() теперь гарантированно работает с объектами, реализующими интерфейс ArrayAccess, т.е. вот так заработает:
list($a, $b) = (object) new ArrayObject([0, 1]);
Ранее в обе переменные был бы занесен null.
Изменения в foreach
1. Итерации в foreach() больше не влияют на внутренний указатель массива, который доступен через семейство функций current()/next()/…
$array = [0, 1, 2];
foreach ($array as &$val) {
var_dump(current($array));
}
сейчас напечатает int(0) три раза. Ранее — int(1), int(2), bool(false)
2. Во время итерирования массивов по значению, foreach теперь пользуется копией массива, и его изменения внутри цикла не повлияют на поведение цикла:
$array = [0, 1, 2];
$ref =& $array; // необходимо, чтобы включить старое поведение
foreach ($array as $val) {
var_dump($val);
unset($array[1]);
}
Код напечатает все значения (0 1 2), ранее второй элемент выкидывался — (0 2).
3. Когда итерируются массивы по ссылке, изменения в массиве будут влиять на цикл. Предполагается, что PHP лучше будет отрабатывать ряд случаев, например, добавление в конец массива:
$array = [0];
foreach ($array as &$val) {
var_dump($val);
$array[1] = 1;
}
проитерирует и добавленный элемент. Вывод будет «int(0) int(1)», ранее было только «int(0)».
4. Итерирование обычных (не Traversable) объектов по значению или по ссылке будет вести себя как итерирование по ссылке для массивов. Ранее — аналогично, за исключением более точного позиционирования из предыдущего пункта.
5. Итерирование Traversable объектов не изменилось.
Изменения в обработке аргументов функций
1. Больше нельзя использовать одинаковые имена для аргументов (будет ошибка компиляции):
public function foo($a, $b, $unused, $unused) {
// ...
}
2. Функции func_get_arg() и func_get_args() теперь вернут текущее значение (а не исходное). Например:
function foo($x) {
$x++;
var_dump(func_get_arg(0));
}
foo(1);
выведет «2» вместо «1».
3. Похожим образом трейсы в исключениях не будет выводить оригинальные значения, а уже измененные:
function foo($x) {
$x = 42;
throw new Exception;
}
foo("string");
теперь выдаст:
Stack trace:
#0 file.php(4): foo(42)
#1 {main}
Ранее было бы так:
Stack trace:
#0 file.php(4): foo('string')
#1 {main}
Хоть это и не влияет на исполнение, но следует иметь это в виду при отладке. То же ограничение теперь и в debug_backtrace() и прочих функциях, исследующих аргументы.
Изменения в обработке integer
1. Некорректные восьмеричные числа будут выдавать ошибку компиляции:
$i = 0781; // 8 - неверный разряд для восьмеричного числа
Ранее все после некорректного разряда отбрасывалось, и в $i была бы 7.
2. Побитовые сдвиги на отрицательные числа теперь бросают ArithmeticError:
var_dump(1 >> -1);
// ArithmeticError: Bit shift by negative number
3. Сдвиг влево на число, большее разрядности, вернет 0:
var_dump(1 << 64); // int(0)
Ранее поведение зависело от архитектуры, на x86 и x86-64 результат был == 1, т.к. сдвиг был циклическим.
4. Аналогично сдвиг вправо даст 0 или -1 (зависит от знака):
var_dump(1 >> 64); // int(0)
var_dump(-1 >> 64); // int(-1)
Изменения в обработке ошибок
1. Больше не парсятся в числа строки с шестнадцатиричными числами:
var_dump("0x123" == "291"); // bool(false) (ранее true)
var_dump(is_numeric("0x123")); // bool(false) (ранее true)
var_dump("0xe" + "0x1"); // int(0) (ранее 16)
var_dump(substr("foo", "0x1")); // string(3) "foo" (ранее "oo")
// Notice: A non well formed numeric value encountered
filter_var() может использоваться для проверки строки на содержание шестнадцатиричного числа или конвертации в обычное число:
$str = "0xffff";
$int = filter_var($str, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX);
if (false === $int) {
throw new Exception("Invalid integer!");
}
var_dump($int); // int(65535)
2. Из-за добавления эскейп-синтаксиса для юникода, строки в двойных кавычках и heredoc должны это учитывать:
$str = "\u{xyz}"; // Fatal error: Invalid UTF-8 codepoint escape sequence
Необходимо двойное экранирования слэша:
$str = "\\u{xyz}";
Хотя простое "\u" без последующей { не затронуто, и вот так заработает без изменений:
$str = "\u202e";
Про новую обработку ошибок уже здесь писали, не буду повторяться. Из интересного еще можно добавить, что конструкторы встроенных классов теперь всегда кидают исключения, тогда как ранее некоторые просто возвращали null.
Из других изменений документ отмечает теперь отсутствие $this для нестатических методов, вызванных статически (ранее метод использовал $this вызывающего контекста).
Пополнился, что логично, список недоступных для классов, трейтов и интерфейсов имен — добавлены bool, int, float, string, null, false, true, а также для будущего использования: resource, object, mixed, numeric.
Конструкт yield не требует больше скобок при использовании в выражениях. Он теперь право-ассоциативный оператор с приоритетом между «print» и "=>". Поэтому поведение может измениться:
echo yield -1;
// ранее интерпретировалось как
echo (yield) - 1;
// а сейчас как
echo yield (-1);
yield $foo or die;
// ранее интерпретировалось как
yield ($foo or die);
// а сейчас как
(yield $foo) or die;
Эти случаи рекомендуется принудительно уточнять скобками.
Из заметных изменений в стандартной библиотеке функций отмечу только удаление call_user_method() и call_user_method_array(), остальное не столь значительно (выкинули dl() в php-fpm, переименовали и оптимизировали zend_qsort -> zend_sort, добавили zend_insert_sort, немного поменяли поведение setcookie при пустом имени cookie и убрали фатальную ошибку ob_start внутри буферизации).
Комментарии (62)
resurection
12.07.2015 11:26Ура! Осталось подождать всего-то тыщу лет, пока эта версия докатиться до продакшн-серверов и виртуальных хостингов.
JSmitty Автор
12.07.2015 12:03+3Мне кажется, что переползание начнётся сразу по релизу, уж очень соблазнительно получить прирост производительности даром. Конечно, если не выплывет какого-нибудь крупного косяка. На мой взгляд, изменения менее существенны, чем при переходе с 5.2 на 5.3.
edogs
12.07.2015 12:24+2На мой взгляд, изменения менее существенны, чем при переходе с 5.2 на 5.3.
Менее существенны в смысле принципиальности и глобальности, но переписывать надо достаточно много кода, ряд проектов мы просто не будем переводить на 7-ку, ибо никто это не оплатит — слишком дорого.
Было бы неплохо, что бы появился какой-то анализатор кода, который будет указывать на то что надо поправить, но надежды на это немного.wapmorgan
12.07.2015 13:39+4Попробуйте www.reddit.com/r/PHP/comments/36vuxi/php_7_migration_assistant_report_mar или на гитхабе github.com/Alexia/php7mar
symbix
12.07.2015 19:41-1Да вроде особо нечего переписывать-то. Надо очень странный код писать, чтобы изменения его затронули. У меня все юнит-тесты прошли на альфе. Правда, opcache иногда сегфолтился, но это другая проблема.
guyfawkes
12.07.2015 23:46+2unset внутри цикла никогда не делали?
FractalizeR
13.07.2015 10:41Кстати, насколько я понимаю, unset(), сделанный внутри цикла, сработает все равно. Просто эти изменения будут не видны внутри цикла. А снаружи — пожалуйста. Или нет?
symbix
13.07.2015 13:47+2А это, как уже верно заметил FractalizeR, ложь и провокация. Все работает, как и прежде:
codepad.viper-7.com/gT7CyS
Только что проверил на свежем master, результат тот же. Копия используется «внутри».
OnYourLips
13.07.2015 00:45Осталось подождать всего несколько месяцев. Ведь сейчас все и так на 5.6 работает.
За исключением ультрадешевых проектов на shared hosting.
AreD
12.07.2015 16:32+1А в этой версии название функций привели к единому стилю, порядку аргументов?
bosha
12.07.2015 16:43-3Да вряд ли приведут. Это поломает огромное количество существующего кода, а тогда PHP7 нафиг никому не нужен будет.
От него и сейчас то все уходят.Corpsee
12.07.2015 16:56+2Есть на это RFC и совместимость он не ломает. Старые названия функций остаются и добавляются новые более однородные названия. В 7.0 он уже не войдет, но в какой-нибудь релизе 7.1+ вполне могут что-то такое сделать.
bosha
12.07.2015 17:23-3Т.е. они добавят ещё «стопицот» новых функций? Ужас. Читать чей нибудь PHP код вообще будет невозможно.
Corpsee
12.07.2015 17:32+3Добавят, а стопицот старых объявят устаревшими и выпилят через пару релизов. А вы как хотели поменять имена и сохранить обратную совместимость одновременно?
bosha
12.07.2015 17:40-9Никак. У них не получится, и никакие RFC не помогут.
Учитывая средний уровень PHP программистов, то никакие RFC и best-practices они читать не будут. Напишут код как нибудь, чтобы работало: насобирают кусков кода со stackoverflow и остального интернета. Это будет каша из старых функций, и новых. И такого кода будет не мало. И опять в кругах разработчиков PHP вспыхнут прения: ломать совместимость и удалять старые функции, или нет. И, в общем-то, я думаю Вы и сами понимаете какой выбор они сделают. Такой же, как на протяжении всех предыдущих лет.
Punk_UnDeaD
13.07.2015 00:40+1Плохие средние PHP программисты в состоянии перепроверить порядок $haystack, $needle, а вы нет?
bosha
13.07.2015 09:19Какая у Вас интересная логика. Во-первых, про себя я ничего не говорил. Во-вторых, почитайте хотя бы это. Кажется, на хабре даже где-то был её перевод. Сразу поймёте масштаб проблемы.
Cразу видно, что с крупными проектами Вы не работали. Если PHP комьюнити решится на такой шаг, то я с удовольствием посмотрю, насколько тяжело комьюнити дастся такой переход. За примерами далеко ходить не надо: Python'у, который чуточку менее популярный чем PHP, переход со второй версии на третью дался с большим трудом — до сих пор очень большое количество пакетов не поддерживают третью версию, а многие даже не собираются переписывать свои библиотеки.LastDragon
13.07.2015 10:59+1Так не надо переписывать старые проекты и проблем не будет. Всего-то нужен LTS* релиз на основе последнего PHP 5 и всё, года через три-четыре будем вспоминать как ужасно раньше было :) (популярные библиотеки обновятся рано или поздно, а даже если что-то умрет, то большая любовь комьюнити к велосипедостроению довольно быстро заполнит освободившиеся ниши).
* так то уже всё есть www.zend.com/en/support-center/support/php-long-term-support, но на платной основе.bosha
13.07.2015 15:26Про Python так же говорили. Как итог — я сейчас регулярно натыкаюсь на библиотеки, которые до сих пор не портированы на python3, и вряд ли будут. И ещё раз повторяю: таких библиотек очень много до сих пор. Есть люди, которые решили определённые задачи написав библиотеку, и выложили её в публичный доступ. Дальше они поддерживать их не будут, и вряд ли перепишут, а комьюнити, не факт что перепишет лучше. Пусть даже перепишет, вопрос во времени, которое им понадобится.
Давайте мы не будем гадать. Я Вам привёл пример языка, где подобный переход (хотя на самом деле, между python2 и python3 разница не такая большая, как планируемая между PHP5 и PHP7) длится уже 8 год, а в случае с PHP всё гораздо хуже, как из-за большего количества функций языка, так и из-за среднего уровня людей на нём пишущих приложения. Можете мне рассказать success story хоть одного языка, в котором подобное прокатило? Да, наверняка останется достаточное количество достойных программистов, фанатов языка которые всё напишут. Вопрос только в том, сколько времени у них это займёт, и сколько будет написано жуткого кода во время этого периода.
Так вот, отходя плавно от полемики в которую Вы меня пытаетесь увести: разговор был про переписывание функций, которое планируется к следующим релизам. Вопрос: сколько будет написано пахнущего кода во время этого перехода? А во время следующей «чистки»? И ведь кому-то это придётся поддерживать.Fedot
13.07.2015 15:47Вообще то с точки зрения библиотек, PHP5.6 и PHP7 не отличаются практически, потому что обратная совместимость сохранена на уровне языка. Обратная совместимость потеряна только на уровне внутренностей реализации интерпретатора. А в питоне2 и 3 потеряна обратная совместимость на уровне языка. Так что вы что-то не то сравниваете.
bosha
13.07.2015 18:20-1Подождите. Вы опять пытаетесь меня увести в сторону. Я изначально написал:
Т.е. они добавят ещё «стопицот» новых функций? Ужас. Читать чей нибудь PHP код вообще будет невозможно.
Т.е. планируется добавить пачку новых функций, которые повторяют функционал других. Меняется только название и порядок/количество аргументов. Будет снова гора кода, где часть функций deprecated, а часть «новые».
Вообще этот ужас с количеством функций языка удручает.
В python особо не потеряна обратная совместимость. Не так много на самом деле изменилось. Ну, это так по мне. Для меня переход с 2 на 3 версию дался легко.Fedot
13.07.2015 18:56+1Не очень понимаю чем может удручать поэтапное эволюционирование языка?
Я же указал вам на что у сообщества PHP не будет проблем с переходом популярных(и не очень) библиотек на новые версии языка, так как нет потери обратной совместимости, в отличии от питона, пример которого вы привели.
Что касается кода, не высокого качества, то его количество не измениться. Но хороший код станет писать приятнее и удобней.Fesor
13.07.2015 20:12+2революция vs эволюция всегда было холиварной темой.
Что до функций — на самом деле не проблема нынче написать тулзу для автоматической миграции кода. Благо инструментарий для основы имеется.
LastDragon
13.07.2015 16:05+2> между python2 и python3 разница не такая большая, как планируемая между PHP5 и PHP7
Python не знаю, к сожалению, но судя по первым ссылкам в гугле (например,http://pythonworld.ru/osnovy/python2-vs-python3-razlichiya-sintaksisa.html) там вообще всё сломали и сделали по другому. PHP7 же это просто эволюция при том больше самого движка нежели синтаксиса (из того что выше не так много по настоящему критичного). Планируемое переименование функций проблем тоже не создает ибо можно автоматизировать генерацию алиасов на старые функции (я вообще не думаю что кто-то будет руками сидеть и все это переносить). Да и вообще переход с PHP4 на PHP5 был гораздо хуже.
Punk_UnDeaD
13.07.2015 22:13+1Имею достаточно опыта в разработке и в руководстве личным составом разработчиков, чтобы сделать вывод.
Проблемы от $haystack, $needle встречаются чуть чаще, чем никогда. Чуть больше проблем с другими неоднозначностями и тонкостями языка. Но это даже не рядом с проблемами, когда программист плохо знает фреймворк, когда вытаскивает и перебирает всю таблицу из базы, когда забывает, что сделали аякс доставку части контента. Когда сваливает весь функционал в один обработчик события.
Главные проблемы не в языке, а в методичном следовании универсальным антипаттернам.
Статью же от белок истеричек оставим белкам истеричкам.
istem
14.07.2015 20:00-1> «ломать совместимость и удалять старые функции, или нет»
В таких случаях, так и напрашивается конструкция вида:
use PHP5.3;
berman
12.07.2015 20:59-4PHP, наверное, хороший язык и вы все его любите. Но, черт, какой-же он нечитабельный. Сейчас работаю с одним проектом, у которого сервер написан полностью на PHP (программистами, которым пофиг на хорошие практики) и разобраться с ходу ни в чем не получается. Вообще интересно было бы почитать какой-нибудь материал про то, как писать maintainable PHP. Может кто-нибудь что-нибудь посоветует? Чем короче публикация — тем лучше. :)
AterCattus
12.07.2015 21:06+5Казалось бы, дело не в языке, а людях, пишущих код. Тем более вы сами про это же и пишете.
bosha
12.07.2015 21:13-6Когда в языке есть 10+ способов сделать одно и тоже, и выстрелить себе в ногу — это конечно способствует написанию хорошего кода. :)
bosha
13.07.2015 15:32-1Господа минусующие: у Вас есть что возразить по делу, или продолжите сливать мне карму, просто от обиды из-за фактов? Факты штука такая, против них не попрёшь.
AterCattus
13.07.2015 15:48+2Я вам не минусовал, но все же комментарий получается однобоким: если выкинуть все ЯП, позволяющие выстрелить в ногу и сделать что-то более, чем одним способом, то много ли останется чего-то приемлемого?
Понятно, что если код пишут люди, которым плевать на стандарты, практики и подходы к разработке на конкретном ЯП — то тут уже ничего не поможет (включая возможности и инструментарий самого ЯП).
bosha
12.07.2015 21:16Вы ошиблись: этот вопрос надо не мне задавать. У меня после написания кода на PHP остались сугубо негативные чувства, и я расскажу больше плохого, чем хорошего.
Fesor
13.07.2015 23:20А знаете, расскажите. Только постарайтесь объективно. + уточните что и когда вы писали, насколько хорошо его знаете и на чем пишите в основном. Так, для статистики. Хотя бы на примере вот этого.
Fesor
13.07.2015 23:26Заодно расскажите (я может плохо знаком с telnet или были другие особенности), почему вы из сокета читаете по одному чару?
stychos
14.07.2015 14:23+1Ну так можно любой язык объявить нечитабельным с колокольни «знания» другого.
Arepo
12.07.2015 19:54+2В целом отличные изменения, кроме вот этих двух вещей, который вызывает только вопрос «ШТА?!»
трейсы в исключениях не будет выводить оригинальные значения, а уже измененные
Господа, как с этим жить? Теперь трейсы перестанут быть трейсами т.к. вызов функции с аргументами, показанными в трейсе, не всегда будет приводить к оригинальному результату.
Как итог, трейсам нельзя будет доверять. Как могли на такое пойти? Это же просто суицид-фича какая-то.
Далее
Во время итерирования массивов по значению, foreach теперь пользуется копией массива, и его изменения внутри цикла не повлияют на поведение цикла
Когда итерируются массивы по ссылке, изменения в массиве будут влиять на цикл
Тоже неоднозначное изменение, которое в корне меняет концепцию foreach — если раньше это был обычный обход элементов массива либо по значению, либо по ссылке, то теперь это 2 совершенно разных вида циклов: один read-only, второй read-write.
В принципе, ничего не имею против, но стоило ли ради этого ломать обратную совместимость, даже если это касается такой странной практики, как изменение массива в момент его обхода?guyfawkes
12.07.2015 23:46+1А что странного в этой практике? Конечно, есть альтернативные способы написания, но удаление ненужных по какой-то логике элементов из массива при итерации вроде бы ничем не странное
FractalizeR
13.07.2015 10:47+1foreach не делает исходный массив read-only. Изменения, произведенные над массивом внутри цикла будут видны после выхода из цикла. Можно пробовать, например, здесь: codepad.viper-7.com/pJ6KCf
<?php $array = [1, 2, 3]; foreach($array as $arg) { unset($array[0]); } var_dump($array);
Выведет массив из двух элементов, а не из трех.
gro
14.07.2015 10:57Strict Standards: Only variables should be passed by reference
Я, конечно, за максимальный стрикт, но зачем он здесь нужен?
Ну передали результат «по-ссылке», не сохранится он нигде после вызова, ну и что?
Нет, надо городить промежуточные переменные.AterCattus
14.07.2015 11:29Это указание, что, вероятно, в коде написано не то, что хотел написать разработчик. Это как присвоение в if: можно (в некоторых ЯП), но часто не то, что нужно.
gro
14.07.2015 11:39Ну почему, в примере вполне реальный случай. Получить из функции массив, а из него только последний элемент.
AterCattus
14.07.2015 11:52Мы же сейчас про array_pop? Но ведь это не получение последнего элемента, это забирание элемента с вершины стека. Передавая по значению, теряется вообще смысл операции.
gro
14.07.2015 11:56Это: 1. удаление элемента из стека, 2. получение этого элемента.
Меня может интересовать только этот элемент, а состояние остального стека нет.AterCattus
14.07.2015 12:08В таком случае да, это не то, что нужно. Подошел бы end, но и он по ссылке хочет значение. Тут скорее вопрос к полноте и функциональности стандартного набора функций и ЯП, раз приходится писать свой вариант arr[count(arr) — 1] за его неимением.
Fesor
14.07.2015 13:08по ссылке end хочет массив только потому что меняет указатель на текущий элемент списка (массивы же в php все еще списки). Не вижу в этом ничего такого, из-за чего нужно писать свою функцию, тем более покрывающую только часть сценариев. В то же время end влияет только на случаи когда код зависит от указателя на текущий элемент. Не забывайте что может быть и такое:
$arr = [1, 2, 3]; $arr[7] = 4; // мало ли, бывает $end = end($arr); //4 $arr[count($arr)-1]; // Notice: Undefined offset
AterCattus
14.07.2015 13:22Так я про end к тому, что он тоже позволяет получить элемент, но тоже с тем же подходом со Strict Standards. Т.е. в php есть две более-менее подходящие функции, но обе хотят ссылку. И ни одной, которым ссылка не нужна.
Fesor
14.07.2015 13:50ясно, тут соглашусь. Хотя опять же, основное предназначение функции end — двигать указатель. Если вам так хочется получить последний элемент массива и не хочется париться из-за побочных эффектов то можно воспользоватьяся тем что пых копирует аргументы при передаче в функции (при записи, или в нашем случае, создании референса).
function last(array $arr) { // слава Copy-on-write return end($arr); } // лучше вообще юзать какие-нибудь коллекции function first(array $arr) { return reset($arr); } function foo() { return [1, 2, 3, 4]; } last(foo()); // 4 first(foo()); //1
kentastik
Ура, товарищи! Хотелось бы увидеть тесты производительности на популярных cms и фреймворках.
bolk
Вордпресс сойдёт? wiki.php.net/phpng#performance_evaluation
Bzzz
docs.google.com/spreadsheets/d/1qW0avj2eRvPVxj_5V4BBNrOP1ULK7AaXTFsxcffFxT8/edit#gid=1334306309
kentastik
это жирный плюс :) спасибище!
kidar2
Накидал пару графиков
https://slemma.com/share/b3f5803b7e60295c56321e042588c9a33ffb4846
NorthDakota
Тут ещё больше docs.google.com/spreadsheets/d/1qW0avj2eRvPVxj_5V4BBNrOP1ULK7AaXTFsxcffFxT8/edit#gid=1334306309