YeaHub Tech
412 subscribers
343 photos
15 videos
2 files
450 links
Новые технологии, советы и обучающие материалы

YeaHub — это платформа для IT-специалистов, объединяющая обучение, карьерный рост, развитие и сообщество единомышленников.

Платформа: https://yeahub.ru

Для связи: @ruslan_kuyanets
Download Telegram
🔖 C++: полное руководство по mutable

Ключевое слово, которое часто упускают из виду, но оно критически важно для элегантного кода!

Что такое mutable?

Позволяет изменять член данных константного объекта, не нарушая его логическую неизменяемость.

class Example {
public:
void setValue(int val) const {
value = val; // Работает благодаря mutable!
}
private:
mutable int value;
};


Четыре основных сценария использования:

1️⃣ Кэширование

Сохраняйте результаты дорогостоящих вычислений:

class DataProcessor {
public:
int getSum() const {
if (!cachedSum_) {
cachedSum_ = std::accumulate(data_.begin(),
data_.end(), 0);
}
return *cachedSum_;
}
private:
std::vector<int> data_;
mutable std::optional<int> cachedSum_;
};


Преимущество: Вычисляем один раз, используем многократно!

2️⃣ Отложенные вычисления (Lazy Evaluation)

class LazyString {
public:
const std::string& getUpperCase() const {
if (!upperCase_) {
upperCase_ = original_;
std::transform(upperCase_->begin(),
upperCase_->end(),
upperCase_->begin(),
::toupper);
}
return *upperCase_;
}
private:
std::string original_;
mutable std::optional<std::string> upperCase_;
};


Экономия: Вычисляем только когда действительно нужно!

3️⃣ Потокобезопасность

class ThreadSafeCounter {
public:
int getValue() const {
std::lock_guard<std::mutex> lock(mutex_);
return value_;
}
private:
int value_ = 0;
mutable std::mutex mutex_; // Можем блокировать в const методах
};


4️⃣ Логическая неизменяемость

class BigData {
public:
int get(size_t index) const {
++accessCount_; // Не меняет логическое состояние
return data_[index];
}

size_t getAccessCount() const {
return accessCount_;
}
private:
std::vector<int> data_;
mutable size_t accessCount_ = 0;
};


💡 Практический пример: отложенная загрузка конфигурации

class Configuration {
public:
Configuration(const std::string& filename)
: filename_(filename) {}

std::string getValue(const std::string& key) const {
loadConfigIfNeeded(); // Загружаем только при первом обращении
return config_[key];
}

private:
void loadConfigIfNeeded() const {
if (!isLoaded_) {
// Загружаем конфигурацию из файла
isLoaded_ = true;
}
}

std::string filename_;
mutable bool isLoaded_ = false;
mutable std::unordered_map<std::string, std::string> config_;
};


Использование:
const Configuration config("settings.conf");
// Файл загрузится только здесь ↓
std::cout << config.getValue("log_level");


❗️Потенциальные ошибки:

1. Потокобезопасность
// ❌ НЕ потокобезопасно!
mutable int count_ = 0;
void increment() const { ++count_; }

// ✅ Потокобезопасно
mutable int count_ = 0;
mutable std::mutex mutex_;
void increment() const {
std::lock_guard<std::mutex> lock(mutex_);
++count_;
}


2. Не нарушайте логическую неизменяемость
// ❌ Плохо - меняет логическое состояние
class Bad {
void setData(int d) const { data_ = d; }
mutable int data_;
};

// ✅ Хорошо - только внутренняя оптимизация
class Good {
int getData() const {
++accessCount_; // Метаданные, не логика
return data_;
}
int data_;
mutable size_t accessCount_ = 0;
};


3. Документируйте использование
class Example {
private:
// mutable используется для кэширования
// с целью повышения производительности
mutable std::optional<int> cachedValue_;
};



Итог ⬇️

mutable — мощный инструмент для:
✅ Кэширования результатов
✅ Отложенных вычислений
✅ Потокобезопасности в const методах
✅ Сохранения логической неизменяемости


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
🔖 Java: Работа с большими данными без боли

Обрабатываете миллионы записей и приложение тормозит? 11 проверенных техник оптимизации Java Spring для работы с большими данными!


Проблема:
Приложение должно обработать 10 000+ транзакций по каждому счету. Как не упасть в OutOfMemoryError?


✳️Топ-11 техник оптимизации:

1️⃣ Оптимизация циклов
// ❌ Плохо
for (int i = 0; i < list.size(); i++)

// ✅ Хорошо
int size = list.size();
for (int i = 0; i < size; i++)

Экономия: вызов size() выносится за цикл

2️⃣ Пакетная обработка
Делим данные на батчи по 100-1000 записей
for (int i = 0; i < size; i += 100) {
List<T> batch = data.subList(i, min(i+100, size));
repository.saveAll(batch);
repository.flush(); // освобождаем память
}


3️⃣ Stream API с умом
// ❌ Создает промежуточные коллекции
.collect(Collectors.toList())

// ✅ Прямая обработка
.forEach(System.out::println)


4️⃣ Правильные структуры данных
• ArrayList вместо LinkedList (меньше памяти)
• int вместо Integer (без автобоксинга)
• HashMap с начальной емкостью

5️⃣ Параллельные потоки
// Используем все ядра процессора
list.parallelStream().forEach(this::process);


6️⃣ Избегаем создания объектов
// ❌ В цикле
new String("Result")

// ✅ Переиспользование
String result = "Result";


7️⃣ Optional осторожно
Не используйте Optional в полях класса — это лишние объекты-обертки!

8️⃣ Настройка GC
java -Xms1g -Xmx2g -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200


9️⃣ Логи сборщика мусора
Включите на проде для мониторинга:
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps


1️⃣0️⃣ Слабые ссылки для кэша
// Предотвращаем утечки памяти
WeakHashMap<String, T> cache = new WeakHashMap<>();


1️⃣1️⃣ Transient для сериализации
Не сериализуйте лишние поля:
private transient int salary;



💡 Практический кейс:
Банковское приложение обрабатывало остатки по счетам с 10К+ транзакций. После применения этих техник:
🔸Память: -60%
🔸Скорость: +250%
🔸GC паузы: -80%


Частые ошибки:
❌ list.size() в условии цикла
❌ Создание объектов в циклах
❌ Неправильный начальный размер HashMap
❌ Переиспользование Optional
❌ Игнорирование настроек GC


🔸Главное правило:
Измеряйте производительность! Используйте профайлеры (JProfiler, VisualVM) перед оптимизацией.


Чек-лист перед продом ⬇️

✅ Пакетная обработка настроена
✅ Циклы оптимизированы
✅ GC параметры подобраны
✅ Логи GC включены
✅ Структуры данных выбраны правильно
✅ Утечки памяти проверены


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🔖 10 мощных трюков со строками в JavaScript

Строки в JS — это не просто текст, а швейцарский нож для разработчика! Вот 10 приемов, которые сделают ваш код чище и быстрее.


1️⃣ Переворот строки в одну строку
const reverse = str => str.split('').reverse().join('');
reverse("hello"); // "olleh"


2️⃣ Проверка вхождения подстроки
"hello world".includes("world"); // true

✳️Быстро и читаемо!

3️⃣ Capitalize каждого слова
const toTitleCase = str => str.split(' ')
.map(word => word[0].toUpperCase() + word.slice(1).toLowerCase())
.join(' ');

toTitleCase("javascript tricks");
// "Javascript Tricks"


4️⃣ Удаление пробелов
"  hello world  ".trim(); 
// "hello world"


5️⃣ Повторение строки
"hello ".repeat(3); 
// "hello hello hello "

✳️Забудьте про циклы!

6️⃣ Подсчет вхождений
const count = (str, sub) => str.split(sub).length - 1;
count("hello world, hello JS", "hello"); // 2


7️⃣ Дополнение до нужной длины
"42".padStart(5, "0"); // "00042"
"7".padEnd(3, "0"); // "700"

✳️Идеально для ID и номеров!

8️⃣ Замена всех вхождений
"banana".replaceAll("a", "o"); // "bonono"

// Или через regex:
"banana".replace(/a/g, "o");


9️⃣ Извлечение только букв
const onlyLetters = str => str.replace(/[^a-zA-Z]/g, '');
onlyLetters("a1b2c3"); // "abc"

✳️Regex — ваш друг!

1️⃣0️⃣ Строка в массив символов
[..."hello"]; 
// ['h', 'e', 'l', 'l', 'o']

✳️Spread operator творит магию!

🟧Бонус-комбо:
// Подсчет уникальных символов
const uniqueChars = str => [...new Set(str)].length;
uniqueChars("hello"); // 4

// Удаление дубликатов
const removeDupes = str => [...new Set(str)].join('');
removeDupes("hello"); // "helo"


Почему это важно⬇️

✅ Код становится короче и читабельнее
✅ Меньше багов и edge cases
✅ Лучшая производительность
✅ Проще поддержка и рефакторинг


💡 Практический совет:
Перед тем как писать свою функцию для работы со строками, проверьте — возможно, встроенный метод уже решает вашу задачу!


Частые ошибки:
❌ Забывают, что строки иммутабельны
❌ Используют циклы там, где есть методы
❌ Не знают про replaceAll и padStart
❌ Переусложняют regex там, где можно проще


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🔖 C++: Switch со строками — миссия выполнима!

В C++ нет нативной поддержки switch для строк, но есть 4 мощных способа это обойти! Разбираем от простого к продвинутому.


Проблема:
// ❌ Так нельзя в C++
switch(str) {
case "hello": ...
case "quit": ...
}


1️⃣ Решение: Хеширование через enum
Самый простой и быстрый способ:

enum class StringCode {
hello, quit, help, unknown
};

StringCode hashString(const string& str) {
if (str == "hello") return StringCode::hello;
if (str == "quit") return StringCode::quit;
if (str == "help") return StringCode::help;
return StringCode::unknown;
}

switch (hashString(command)) {
case StringCode::hello:
cout << "Hello there!" << endl;
break;
case StringCode::quit:
cout << "Goodbye!" << endl;
break;
}


✅ Плюсы: быстро, экономно по памяти
🔸 Когда: CLI интерфейсы, конфиги

2️⃣ Решение: unordered_map + функторы
Для динамической обработки:

class CommandProcessor {
unordered_map<string, function<void()>> commands;

public:
CommandProcessor() {
commands["help"] = []() {
cout << "Help info" << endl;
};
commands["add"] = []() {
cout << "Adding..." << endl;
};
}

void execute(const string& cmd) {
auto it = commands.find(cmd);
if (it != commands.end()) {
it->second();
}
}
};


✅ Плюсы: гибкость, можно менять во runtime
🔸 Когда: плагины, динамические команды

3️⃣Решение: Constexpr хеширование (C++17)
Хеширование на этапе компиляции:

constexpr uint64_t hash(string_view str) {
uint64_t hash = 0;
for (char c : str) {
hash = (hash * 131) + c;
}
return hash;
}

constexpr uint64_t operator""_hash(const char* str, size_t len) {
return hash(string_view(str, len));
}

switch (hash(action)) {
case "save"_hash:
cout << "Saving..." << endl;
break;
case "load"_hash:
cout << "Loading..." << endl;
break;
}


✅ Плюсы: производительность как у int switch
🔸 Когда: нужна скорость + compile-time проверки

4️⃣Решение: String Pool (advanced)
Для высоконагруженных систем:

class StringPool {
unordered_set<string> pool;
public:
const string* intern(const string& str) {
auto [it, inserted] = pool.insert(str);
return &(*it);
}
};

// Сравниваем указатели, а не строки!
if (str == pool.intern("start")) {
// быстрее обычного сравнения строк
}


✅ Плюсы: O(1) сравнение вместо O(n)
🔸 Когда: частые повторяющиеся строки

💡 Практический пример: Текстовый редактор
class TextEditor {
enum class CommandType {
insert, delete_line, move, print, unknown
};

CommandType getCommandType(const string& cmd) {
if (cmd == "insert" || cmd == "i")
return CommandType::insert;
if (cmd == "delete" || cmd == "d")
return CommandType::delete_line;
// ...
}

void executeCommand(const string& cmdLine) {
switch (getCommandType(cmd)) {
case CommandType::insert: /* ... */ break;
case CommandType::delete_line: /* ... */ break;
}
}
};


Сравнение производительности:
🔸 Enum хеширование: O(1) после предобработки
🔸 unordered_map: O(1) в среднем, гибко
🔸 Constexpr hash: O(1), compile-time
🔸 String Pool: O(1), лучшее для частых операций

Выбор метода:
Небольшой фикс. набор строк → Enum hash
Динамические команды → unordered_map
Критична скорость → Constexpr hash
Много повторений → String Pool


Важные моменты ⬇️

🔸Enum подход требует exhaustive списка
🔸unordered_map имеет overhead на lookup
🔸constexpr работает только с литералами
🔸String Pool нужен для действительно частых операций

Pro-tip:
Комбинируйте подходы! Например:
🔸constexpr hash для горячих путей
🔸 unordered_map для редких команд
🔸 String pool для логирования

Реальные кейсы применения:
✅Парсеры конфигов
✅CLI приложения
✅Обработка HTTP методов
✅Игровые движки (команды консоли)
✅Компиляторы (обработка токенов)


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🔖 Python: Модульное тестирование для чайников и профи

Пишете код без тестов? Готовьтесь часами искать баги! Разбираем, как unittest спасет ваше время и нервы.


❓Что такое тестирование?

Простыми словами: это проверка, что ваш код работает правильно. Большинство делает это через print() и запуск в терминале. Но есть способ лучше!

💡 Зачем вообще нужны тесты?

🔸 Экономия времени
Вместо часов отладки — мгновенное обнаружение проблем

🔸 Лучший код
Заставляет продумывать входы, выходы и граничные случаи

🔸 Переиспользование
Копируете функцию? Копируйте и тесты!

🔸 TDD возможен
Можно даже писать тесты ДО написания кода (test-driven development)


Типы тестов:

🔸 Модульное (Unit)
Проверка отдельных функций/компонентов

🔸 Интеграционное
Проверка взаимодействия всех частей

Сегодня фокус на модульном!

📝 Простейший пример:

def add(x, y):
return x + y

# Базовый тест
assert add(2, 4) == 6, "Should be 6"


Если результат не 6 → получите AssertionError ❌

Unittest: профессиональный подход

Встроенный в Python тестировщик. Без установки пакетов!

Структура проекта:
project/
├── functions.py
└── tests.py


functions.py:
def add(x, y):
return x + y


tests.py:
import unittest
from functions import add

class TestAdd(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 4), 6)
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(-1, -1), -2)

if __name__ == '__main__':
unittest.main()


Запуск:
python tests.py


Результат:
...
-------------------------------
Ran 1 test in 0.001s

OK


Каждая точка (.) = успешный тест ✅
E = ошибка ❌
F = провал теста ❌


🔸Основные assert методы:

🔸 assertEqual(a, b) — a == b
🔸 assertNotEqual(a, b) — a != b
🔸 assertTrue(x) — x is True
🔸 assertFalse(x) — x is False
🔸 assertIn(a, b) — a in b
🔸 assertIsNone(x) — x is None
🔸 assertRaises(Exception) — проверка исключений
🔸 assertAlmostEqual(a, b) — для float чисел

🔸Правила хороших тестов:

1️⃣ Правильные имена
# ✅ Работает
def test_add():
pass

# ❌ НЕ работает (не начинается с test_)
def add_test():
pass


Название ДОЛЖНО начинаться с test_!

2️⃣ От простого к сложному

Сначала очевидные случаи:
def test_add_positive():
self.assertEqual(add(2, 3), 5)


Потом граничные:
def test_add_zero():
self.assertEqual(add(0, 5), 5)
self.assertEqual(add(5, 0), 5)


3️⃣ Тестируйте edge cases

Что может сломать ваш код?
🔸Отрицательные числа
🔸Нули
🔸Очень большие значения
🔸 Пустые строки
🔸 None значения

def test_edge_cases():
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(0, 0), 0)
self.assertEqual(add(-5, -3), -8)


4️⃣ Независимость тестов

Каждый тест — отдельная вселенная!

class TestCalculator(unittest.TestCase):
def setUp(self):
# Выполняется ПЕРЕД каждым тестом
self.calc = Calculator()

def tearDown(self):
# Выполняется ПОСЛЕ каждого теста
self.calc = None


5️⃣ Избегайте assertTrue

❌ Плохо:
self.assertTrue(add(2, 3) == 5)
# Вывод: Expected True, but was False


✅ Хорошо:
self.assertEqual(add(2, 3), 5)
# Вывод: AssertionError: 5 != 6


Второй вариант даёт НАМНОГО больше информации!


🔸Реальный пример бага:

def round_plus_ten(x):
return round(x) + 10

class TestRound(unittest.TestCase):
def test_round(self):
self.assertEqual(round_plus_ten(4.3), 14) # ✅
self.assertEqual(round_plus_ten(4.7), 15) # ✅
self.assertEqual(round_plus_ten(4.5), 15) # ❌ FAIL!


Почему упало?
round(4.5) = 4, а не 5! Python округляет к ближайшему чётному.

🟧Урок: тесты находят неочевидные баги!


🔸 Продвинутые фишки:

Группировка тестов:
class TestAddition(unittest.TestCase):
def test_positive(self): ...
def test_negative(self): ...

class TestMultiplication(unittest.TestCase):
def test_basic(self): ...


Пропуск тестов:
@unittest.skip("Пока не работает")
def test_future_feature(self):
pass


Проверка исключений:
def test_divide_by_zero(self):
with self.assertRaises(ZeroDivisionError):
divide(10, 0)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Best Practices:
✅ Один тест = одна проверка
✅ Тесты должны быть быстрыми (<1 сек)
✅ Понятные названия: test_add_negative_numbers
✅ Не тестируйте сторонние библиотеки
✅ Покрытие кода 70-80% — отличный результат
✅ Коммитьте тесты вместе с кодом

❌ Не делайте тесты зависимыми друг от друга
❌ Не используйте случайные данные без seed
❌ Не игнорируйте упавшие тесты


🔸Workflow разработки:

1. Пишете функцию
2. Сразу пишете тест
3. Запускаете тест
4. Если упал → чините код
5. Повторяете до зелёных тестов
6. Добавляете edge cases
7. Профит! 💫

🔸 Альтернативы unittest:

🔸 pytest — более простой синтаксис
🔸 nose2 — расширение unittest
🔸 doctest — тесты в docstrings

Но unittest — это стандарт Python!

💡 Золотое правило:

«Если нашли баг — напишите тест, который его воспроизводит. Потом чините код. Так этот баг никогда не вернётся!»

Реальная статистика ⬇️

🔸 Проекты с тестами имеют на 40% меньше багов
🔸 Рефакторинг с тестами = безопасность
🔸 Тесты = документация вашего кода
🔸 Код с тестами легче поддерживать


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🔖 Тайм-трекер в Telegram за вечер

Забываете отмечать рабочее время? Делаем простой бот на Node.js + TypeScript!


🔍 История

Жене нужен был отчёт за 3 месяца работы, но инструмента для учёта времени не было. Сначала писала "начало"/"конец" в WhatsApp группу. Решил автоматизировать это через Telegram Bot!


💡 Что получилось:

✅ 4 простые команды
✅ Автоподсчёт времени
✅ Экспорт в CSV
✅ Работает везде (телефон всегда под рукой!)


🔸Команды:

🔸 /work [задача] — начать работу
🔸 /done — завершить задачу
🔸 /state — текущий статус
🔸 /print — вывести CSV


🔸Стек:

• Node.js + TypeScript
• node-telegram-bot-api
• CSV для хранения
• Docker для деплоя


🔸 Быстрый старт:

1️⃣ Установка:
npm i --save node-telegram-bot-api
npm i --save @types/node-telegram-bot-api


2️⃣ Создать бота:
Идём к @BotFather → /newbot → получаем токен

3️⃣ Базовый код:
import TelegramBot from 'node-telegram-bot-api';

const token = 'YOUR_TOKEN';
const bot = new TelegramBot(token, { polling: true });

// Начать задачу
bot.onText(/\/work (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const work = match[1];
const startDate = new Date();

const entries = await getEntries();
entries.push({
work,
startDate: startDate.toJSON(),
endDate: null,
duration: null,
});
writeFile(entries);

bot.sendMessage(chatId,
`Started ${work} at ${startDate.toLocaleString()}`);
});

// Завершить задачу
bot.onText(/\/done(.*)/, async (msg) => {
const chatId = msg.chat.id;
const endDate = new Date();

const entries = await getEntries();
const targetEntry = entries[entries.length - 1];
targetEntry.endDate = endDate.toJSON();

const startDate = new Date(targetEntry.startDate);
const diff = endDate - startDate;
targetEntry.duration = formatDuration(diff);

writeFile(entries);

bot.sendMessage(chatId,
`Finished ${targetEntry.work}. Took ${diff}!`);
});


🔸 Работа с CSV:
const getEntries = async () => {
const val = fs.readFileSync(csvFilePath).toString();
const entries = [];

val.split("\n").forEach((row, index) => {
if (row === "" || index == 0) return;
const [work, startDate, endDate, duration] =
row.split(';');
entries.push({ work, startDate, endDate, duration });
});

return entries;
}

const writeFile = (entries) => {
let str = "work;startDate;endDate;duration;\n";
entries.forEach(e => {
str += Object.keys(e).map(k => e[k]).join(';') + '\n';
});
fs.writeFileSync(csvFilePath, str);
};


🔸 Использование:

/work Написать статью
→ Started at 9:00 AM

/done
→ Finished. Took 3h 25m!

/print
→ work;startDate;endDate;duration;
Статья;2025-01-20T09:00...;3h 25m


🔸 Деплой:
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "build/main/index.js"]


docker build -t timetracker .
docker run -ti timetracker



🔸Преимущества:

✅ Просто — 4 команды
✅ Быстро — 2 секунды на команду
✅ Везде — Telegram на всех устройствах
✅ Бесплатно — без подписок
✅ CSV — легко открыть в Excel


🔸 Идеи для улучшения:

🔸 Категории задач (#важное, #срочное)
🔸 Статистика по дням/неделям
🔸 Напоминания о перерывах
🔸 Экспорт в JSON/PDF
🔸 Интеграция с календарём


❗️ Важно:

Не коммитьте токен! Используйте .env:
import dotenv from 'dotenv';
dotenv.config();

const token = process.env.TELEGRAM_BOT_TOKEN;



Итог ⬇️

За один вечер — полнофункциональный тайм-трекер! ~200 строк кода, работает везде, данные в удобном формате.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
🔖 Контейнеризация: почему это невозможно остановить

Изолированная среда для запуска приложений!


📎 Что такое контейнер?

Изолированная «песочница», где процессы работают отдельно от ОС.

Внутри: все зависимости, библиотеки, конфигурации.

Результат: запускаешь везде — ничего устанавливать не нужно.


🔸 Как работает?

Правило: 1 контейнер = 1 процесс

Цепочка:
Dockerfile → Image (шаблон) → Container (экземпляр)

Аналогия: Image = класс, Container = объект


💡 Контейнер vs ВМ

ВМ: гипервизор + полная ОС + много ресурсов
Контейнер: только нужное + разделяемая ОС + минимум ресурсов


🔸 Преимущества

✅ Легковесные (МБ vs ГБ)
✅ Портативные (работают везде)
✅ Воспроизводимые (1 образ = тысячи копий)
✅ Изолированные (не мешают друг другу)
✅ Быстрые (секунды vs минуты)


🔸 Применение

• Микросервисы
• CI/CD
• Масштабирование
• Разработка ("Works on my machine" решено)
• Облако (AWS, Google, Azure)


🔸 Docker

docker pull node:18-alpine
docker run -p 3000:3000 my-app
docker stop container_id



🔸 Без vs С контейнерами

Без: установка → зависимости → настройка → молитва
С: docker run my-app


🔸 Почему невозможно остановить?

✅ "Works on my machine" → Works everywhere
✅ Сложный деплой → docker run
✅ Долгая настройка → Уже готово
✅ Конфликты → Изоляция
✅ Медленно → Мгновенно


Итог ⬇️

Контейнеры: легкие, быстрые, портативные, эффективные, изолированные.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🔖 Spring Boot + Redis + PostgreSQL: кэширование за 15 минут

Ускорение API в 300 раз!


📎 Результат

Без кэша: 3000 мс
С Redis: 10 мс


🔸 Что нужно?

Зависимости:
spring-boot-starter-data-redis + jedis

Конфигурация:
RedisTemplate + CacheManager + @EnableCaching


📝 5 аннотаций

@Cacheable — кэширует результат (GET)
@CacheEvict — очищает кэш (POST/PUT/DELETE)
@CachePut — обновляет кэш
@Caching — группирует операции
@CacheConfig — настройки класса


🔸 Как работает?

1-й запрос: Redis пусто → БД → сохраняем в кэш → 3000 мс
2-й запрос: Redis → 10 мс
Изменение: @CacheEvict очищает кэш


💡 3 стратегии

Cache-Aside: @Cacheable (популярная)
Write-Through: @CachePut (актуальные данные)
Invalidation: @CacheEvict (сложная)


🔸 Best Practices

✅ Кэшируйте медленные запросы (> 100 мс)
✅ Очищайте при изменениях
✅ Используйте TTL
✅ Мониторьте hit rate (> 70%)

❌ Не забывайте инвалидацию


🔸 Подводные камни

Thundering Herd: все в БД → cache warming
Memory Limits: переполнение → eviction policies
Serialization: объекты Serializable


🔸 Результаты

Без кэша: 100 запросов = 300 сек, нагрузка 100%
С кэшем: 100 запросов = 4 сек, нагрузка 1%

Ускорение в 75 раз!


Итог ⬇️

5 аннотаций + простая настройка = прирост в 75-300 раз. Must-have для production.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
🔖 C++ циклы while: полное руководство

Разбираем самый важный цикл в программировании!

🔸 Что такое while?

Цикл с предусловием. Логика: "ПОКА условие верно — ДЕЛАЙ это"

💡 5 практических применений

1️⃣ Проверка ввода
Запрашиваем данные, пока не получим корректные

2️⃣ Обработка файлов
Читаем, пока есть данные (размер заранее неизвестен)

3️⃣ Интерактивное меню
while(true) → показываем меню → выполняем → "Выход" = break

4️⃣ Игровые циклы
Игра продолжается, пока не gameOver или не победа

5️⃣ Обработка данных
Собираем данные, пока не получим сигнал остановки


🔸 While vs Do-While

While: проверка ДО → может не выполниться
Do-While: проверка ПОСЛЕ → минимум 1 раз


💫 Золотые правила

🔸 Всегда предусмотрите выход
🔸 Обрабатывайте ошибки
🔸 Понятные условия
🔸 Осмысленные имена
🔸 Защита от бесконечных циклов


📎 Частые ошибки

❌ Забыли изменить условие
❌ while(x = 5) вместо while(x == 5)
❌ Нет способа выхода
❌ Игнорирование ошибок


Итог ⬇️

While — основа C++: гибкий, универсальный, для ввода, файлов, меню, игр и данных.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
Forwarded from YeaHub
👩‍💻 Android Разработчик | Полный разбор профессии

В этом видео мы подробно разбираем профессию Android-разработчик: кто это, чем занимается, какие навыки нужны и сколько можно зарабатывать. Наш гость — сеньор Android-разработчик с опытом работы в Яндекс, МТС и Wildberries, который делится личным опытом: как пройти путь от новичка до ведущего специалиста всего за 5 лет.

Вы узнаете:
— Что делает Android-разработчик и как выглядит его рабочий день
— Какие качества и навыки нужны для успешного старта
— Сколько времени потребуется на обучение и с чего лучше начинать
— Какие инструменты и технологии нужно знать
— Топ-3 ресурса для старта Android-разработки
— Советы для первого проекта и развития карьеры

Если вы хотите стать Android-разработчиком или просто интересуетесь IT-профессиями, это видео точно для вас!

Подпишитесь на канал, чтобы не пропустить новые видео про карьеру в IT и обучение программированию.

https://yeahub.ru/ - тренажер для подготовки к собеседованиям
https://t.me/yeahub_android_mobile - канал YeaHub в Android

https://t.me/gulyaev_it - телеграмм-канал Антона об Android и IT
https://www.youtube.com/@gulyaev_it - ютуб-канал Антона

#it #android #kotlin #программирование #разработка #собеседование #трудоустройство

📹 https://www.youtube.com/watch?v=ss5cjY7SPXY
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🔥1
🔖 Странные CSS-свойства: зачем они нужны?

Работают, но непонятно где применять. Разбираем 6 необычных свойств!


1️⃣ empty-cells
Прячет пустые ячейки в таблицах.
empty-cells: hide


2️⃣ text-align-last
Выравнивает только последнюю строку текста.


3️⃣ shape-outside
Текст обтекает фигуры (круг, эллипс, многоугольник), а не квадрат.
shape-outside: circle(50%)


4️⃣ hyphenate-character
Меняет дефис при переносе на любой символ.
hyphenate-character: "="


5️⃣ tab-size
Настраивает ширину табуляции в коде.
tab-size: 1rem


6️⃣ text-emphasis
Добавляет символы НАД текстом для ударений.
text-emphasis: sesame


💡 А вы использовали?

Встречали эти свойства в реальных проектах? Или они правда странные?


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
🔖 4 пользовательских хука React: must-know

Упрощаем работу с React через кастомные хуки.


1️⃣ useMount

Задача: выполнить код только при первом рендере

Проблема: useEffect(() => {}, []) — непонятно, что выполняется один раз

Решение: useMount(() => {}) — явная семантика


2️⃣ useUnmount

Задача: выполнить код при размонтировании

Проблема: useEffect(() => { return () => {} }, []) — неочевидный cleanup

Решение: useUnmount(() => {}) — явная семантика


3️⃣ useUpdateEffect

Задача: выполнить код только при изменении зависимостей (не при монтировании)

Проблема: useEffect(() => {}, [count]) — выполняется и при первом рендере

Решение: useUpdateEffect(() => {}, [count]) — пропускает первый рендер


4️⃣ useSetState

Задача: упростить работу с объектами (как в классовых компонентах)

Проблема:
setPerson({ ...person, name: 'medium' })

Постоянное копирование объекта

Решение:
setPerson({ name: 'medium' })

Автоматическое слияние состояния


💡 Преимущества

✅ Читаемый самодокументируемый код
✅ Переиспользуемая логика
✅ Меньше ошибок и boilerplate
✅ Явная семантика


Итог ⬇️

Кастомные хуки решают типовые задачи и делают код понятнее. Эти 4 хука — must-have для React-разработчика.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
#React

🔖 Почему ваш WebSocket переподключается при каждом рендеринге?

Вы создали WebSocket, но соединение постоянно разрывается и переподключается. Проблема: React создаёт новый экземпляр сервиса при каждом рендеринге. Без useCallback сервис теряет стабильную ссылку, что приводит к переподключениям и утечкам памяти.

// Плохо - сервис пересоздаётся при каждом рендеринге
function ChatComponent() {
const ws = new WebSocket('ws://server');
const sendMessage = (msg) => {
ws.send(msg); // Новый WebSocket при каждом рендере
};
return <button onClick={() => sendMessage('Hi')}>Send</button>;
}

// Хорошо - стабильная ссылка на сервис
function ChatComponent() {
const wsRef = React.useRef(new WebSocket('ws://server'));
const sendMessage = React.useCallback((msg) => {
wsRef.current.send(msg); // Один и тот же WebSocket всегда
}, []);
return <button onClick={() => sendMessage('Hi')}>Send</button>;
}


📎 Основные проблемы:

🔸 WebSocket переподключается, теряя состояние соединения
🔸 Память утекает: старые соединения не закрываются
🔸 Event listeners создают дубликаты
🔸 Производительность падает из-за пересоздания ресурсов

Когда компонент рендерится без useCallback, каждый раз создаётся новая функция. Для долгоживущих сервисов (WebSocket, Worker) это критично — вы разрываете всё, что было до этого, вместо переиспользования.

Ключевой момент: useRef гарантирует, что сервис существует на весь цикл компонента, а useCallback — что ссылка на функцию не меняется. Идеальное сочетание для постоянных ресурсов.

✅ Используйте useCallback + useRef для:
- WebSocket и долгоживущих соединений
- Web Workers, которые должны жить долго
- Сервисов, хранящих внутреннее состояние
- Optimization обработчиков событий

❌ Избегайте:
- Создания нового сервиса без ref
- Забывать cleanup в useEffect
- Использования useCallback без причины
- Передачи ref как пропс дочерним компонентам

Стабильная ссылка на сервис — это гарантия корректной работы!

Остальные продвинутые техники React - читай в статье ⬇️


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
#JavaScript

🔖 Почему ваш JavaScript-бандлер весит как три полнометражных фильма?

Вы загрузили приложение, добавили ещё библиотек, и вот уже бандлер весит 500KB вместо 100KB. Пользователи ждут загрузки 5 секунд вместо 1 секунды. Приложение работает, но медленно. Каждый килобайт JS — это время парсинга, выполнения, разборки браузером. На мобильнике это значит батарея сливается, страница зависает, пользователи уходят к конкурентам.

// Плохо - импортируете всё, используете мало
import _ from 'lodash'; // 70KB, нужна одна функция
import * as echarts from 'echarts'; // 1MB для одного графика
import UILibrary from 'heavy-ui-kit'; // 200KB со всеми компонентами

// Хорошо - импортируйте только нужное
import { debounce } from 'lodash-es'; // Tree-shaking, 2KB
import { BarChart } from 'lightweight-charts'; // 50KB, минимум
import { Button, Input } from '@headlessui/react'; // Tree-shaking, 10KB


Основные проблемы раздутого бандлера:

🔸 Дольше загружается на мобильных сетях (LTE, 3G)
🔸 Браузер парсит и выполняет больше кода — замораживает UI
🔸 На слабых устройствах всё работает медленнее, батарея быстрее сливается
🔸 Поисковики штрафуют за низкую производительность, рейтинг падает

Раздутый бандлер — это не просто цифра на диске. Это деньги компании (CDN трафик), разочарование пользователя (медленная загрузка), потерянные конверсии (люди ушли, не дождались).

✅ Как уменьшить бандлер:

1. Аудит зависимостей — используйте webpack-bundle-analyzer, посмотрите что там. Может быть, импортируете тяжелую библиотеку для одной функции?

2. Code splitting — разделите на чанки, загружайте по требованию. React.lazy(), динамический импорт для маршрутов.

3. Tree-shaking — удаляйте неиспользуемый код. ES модули помогают бандлеру определить, что не нужно.

4. Минификация — Terser, Gzip/Brotli. Просто сжимайте код, функциональность не меняется.

5. Server-side rendering — переложите вычисления на сервер вместо клиента. Меньше JS в браузере = быстрее загружается.

❌ Избегайте:

- Импорта целых библиотек для одной функции
- Неиспользуемого кода (закомментированные куски месячной давности)
- Отключенного tree-shaking в production сборке
- Только клиентского рендеринга для статического контента

Каждый килобайт имеет значение. Производительность = пользовательский опыт = конверсии.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
#C++

🔖 Почему memset с целыми числами дает странные результаты?

memset — это функция для заполнения памяти значениями побайтно. Многие думают, что memset(arr, 1, size) заполнит целые числа единицами. На самом деле это создаст странные значения, потому что memset работает с байтами, а не с типами. Вот почему нужно понимать, как она реально работает.

// Плохо - пытаетесь установить целым числам значение 1
int numbers[5];
std::memset(numbers, 1, sizeof(numbers));
std::cout << numbers[0]; // Странный результат: 16843009

// Хорошо - используйте memset только для 0 и -1
int numbers[5];
std::memset(numbers, 0, sizeof(numbers)); // Все нули
// ИЛИ
std::memset(numbers, -1, sizeof(numbers)); // Все биты = 1


🔸 Основные проблемы с memset:

🔸 memset работает побайтно, не зная о типе данных
🔸 С целыми числами только 0 и -1 работают правильно, остальное — мусор
🔸 С не-POD типами (std::string, классы) приводит к краху
🔸 Лишняя единица в size параметре — выход за границы памяти

memset принимает три параметра: указатель, значение (преобразуется в byte), размер в байтах. Проблема: для int это 4 байта. Если вы хотите установить int = 1, нужно 4 раза записать байт 1, что даст 0x01010101 = 16843009, а не 1.

🔸 Практический пример:

char str[50];
std::memset(str, '*', sizeof(str)); // Работает: заполнит звездами
str[sizeof(str) - 1] = '\0';

int matrix[3][3];
std::memset(matrix, 0, sizeof(matrix)); // Работает: матрица нулей
std::memset(matrix, -1, sizeof(matrix)); // Работает: все биты = 1

// Буфер для обработки изображений
unsigned char pixels[100][100];
std::memset(pixels, 0, sizeof(pixels)); // Чёрный (0)
std::memset(pixels, 255, sizeof(pixels)); // Белый (255)

// Очистка игрового состояния
struct GameState {
int score;
int lives;
bool gameOver;
} state;
std::memset(&state, 0, sizeof(GameState)); // Работает для POD
state.lives = 3; // Затем переустанавливаете нужные значения


✅ Используйте memset для:
- Обнуления массивов и буферов (0)
- Установки всех битов в 1 (-1)
- Работы с char массивами
- Инициализации POD типов (Plain Old Data)

❌ Не делайте:
- memset с целыми числами кроме 0 и -1
- memset с std::string и классами (краш!)
- memset на не-POD структурах с конструкторами
- Ошибки в параметре size (выход за границы)

Правило: memset = только для побайтных операций. Для сложных типов используйте std::fill или конструкторы.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
#Programming

🔖 Почему ваша библиотека никто не использует, хотя она работает?

Вы написали библиотеку, которая решает проблему, но она сложная в использовании и никто не хочет её применять. Проблема не в функциональности, а в проектировании. Хорошая библиотека предоставляет точки расширения, не навязывает свой контекст и остаётся простой. Плохая — заставляет пользователя прыгать через обручи.

// Плохо - функция навязывает свой контекст, нет гибкости
void sort(int& container) {
// Компаратор зафиксирован, нельзя поменять
// Способ свопинга элементов фиксирован
}

// Хорошо - точки расширения встроены в интерфейс
template <class R, class C = DefaultCompare, class S = DefaultSwap>
void sort(R&& range, C compare = C(), S swap = S()) {
// Пользователь может передать свой компаратор
// Пользователь может передать свой своп
// По умолчанию работает стандартно
}


📎 Основные проблемы плохих библиотек:

🔸 Отсутствие точек расширения — невозможно кастомизировать
🔸 Сложная архитектура — нужно понимать всю библиотеку, чтобы использовать часть
🔸 Конкретные типы вместо обобщений — работает только для одного случая
🔸 Навязанный контекст — вынуждает пользователя следовать вашему стилю

Хорошая библиотека предполагает точку зрения пользователя, а не разработчика. Она не предполагает, что пользователь сделает. Вместо этого она предоставляет инструменты и ступеньки, позволяя каждому использовать её по-своему.

Три столпа проектирования:

// 1. МОДУЛЬНОСТЬ - компоненты независимы
// Контейнеры отделены от алгоритмов, но работают вместе
std::vector<int> v = {3, 1, 2};
std::sort(v.begin(), v.end()); // Работает с любым контейнером

// 2. МНОГОРАЗОВОСТЬ - работает для многих типов
template <class T>
class Maybe {
T value; // Работает с любым типом T
};

// 3. РАСШИРЯЕМОСТЬ - встроены точки для кастомизации
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b; // Пользовательский компаратор
});


✅ Проектируйте библиотеку так, чтобы:

- Компоненты были независимы и использовались отдельно
- Работала с разными типами, не только одним конкретным
- Предоставляла точки расширения (callback, custom comparator, plugin)
- Была легка в понимании — минимум контекста для начала использования

❌ Не делайте:

- Сложные архитектуры, когда вся библиотека зависит друг от друга
- Конкретные типы (int, string) вместо шаблонов и обобщений
- Фиксированное поведение без возможности кастомизации
- Навязывание дизайн-решений пользователям

Пример хорошей библиотеки — C++ STL: контейнеры работают с алгоритмами, всё обобщено через шаблоны, есть кастомные компараторы и аллокаторы. Пример плохой — фреймворк, где всё зависит от всего.

Полезная библиотека думает о том, кто её будет использовать, и делает это легко.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
#Go

🔖 Почему Go-разработчик пишет код, который Java-девелопер считает "странным"?

Вы Java-разработчик, начали писать на Go и всё кажется странным: нет try-catch, нет наследования, строчные буквы для приватности, можно вызвать метод у nil. Go — это не Java, это другая парадигма. Явность вместо магии, композиция вместо наследования, ошибки как значения вместо исключений.

// Плохо (по Go стандартам) - Java способ обработки ошибок
try {
String raw = Files.readString(Path.of("cfg.json"));
Config cfg = mapper.readValue(raw, Config.class);
return cfg;
} catch (IOException ex) {
throw new IllegalStateException("cannot init config", ex);
}

// Хорошо (Go способ) - явная проверка каждой ошибки
raw, err := os.ReadFile("cfg.json")
if err != nil {
return nil, fmt.Errorf("read cfg: %w", err)
}
var cfg Config
if err := json.Unmarshal(raw, &cfg); err != nil {
return nil, fmt.Errorf("parse cfg: %w", err)
}
return &cfg, nil


📎 Основные различия Go от Java:

🔸 Нет исключений (try-catch), ошибки — это значения (if err != nil)
🔸 Приватность через заглавность букв (Public vs private), не через модификаторы
🔸 Нет наследования, только композиция и интерфейсы
🔸 Интерфейсы неявные — не нужно писать implements

Go философия: явность над неявностью, простота над магией. Кода больше, но всё понятно. Нет аннотаций, нет DI фреймворков, нет магических прокси и рефлексии за кулисами. Вы видите, что происходит.

Два ключевых отличия:

// 1. ЯВНАЯ ОБРАБОТКА ОШИБОК - видите каждый шаг
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()

// 2. ПРИВАТНОСТЬ ЧЕРЕЗ БУКВУ - заглавная = public, строчная = private
type PaymentGateway interface { // Public
Charge(ctx context.Context) error
}
func (s *stripe) charge(ctx context.Context) error { // private method
// ...
}


✅ Go хорош для:
- Микросервисов и бекенда (явность, производительность)
- Когда нужна скорость разработки и чистота кода
- Когда нет необходимости в сложных абстракциях на старте
- Когда важна читаемость и простота отладки

❌ Привыкайте к:
- Повторению проверок ошибок (if err != nil много раз)
- Коротким названиям переменных (ctx, err, db, u, p)
- Отсутствию try-catch блоков (это норма)
- Приватности через буквы, а не ключевые слова
- Явным транзакциям и управлению ресурсами

Go требует переучивания мышления. Java — "дайте мне абстракции и автоматику". Go — "вот инструменты, делайте сами, но явно". Один раз адаптируетесь — полюбите за простоту и понятность.


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🔖 CSS Grid: Вёрстка без DIV'ов

CSS Grid — один контейнер делится на строки и столбцы в CSS. Все элементы располагаются как на шахматной доске.

📎Суть

Вместо кучи DIV'ов для раскладки:

.page {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 80px 1fr 50px;
grid-template-areas:
'sidebar header'
'sidebar main'
'sidebar footer';
}

.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }


Вот так! Рисуешь структуру в CSS, как картинку.

🔸 Главные юниты

fr — "fraction", волшебный юнит Grid'а:
/* 1/4 и 3/4 пространства, автоматически пересчитывается */
grid-template-columns: 1fr 3fr;


repeat() — копирование столбцов:
/* 7 столбцов одинаковой ширины (календарь) */
grid-template-columns: repeat(7, 1fr);


Элементы занимают несколько ячеек

.child {
grid-column: 1 / 4; /* от столбца 1 до 4 */
grid-row: 1 / 3; /* от строки 1 до 3 */
}


✅ Когда использовать

🔸 Двумерные раскладки (строки + столбцы)
🔸 Сетки (карточки, галереи)
🔸 Страницы (header/sidebar/main/footer)

❌ Для навигации по одной оси → Flexbox проще


Главное ⬇️

Чистый HTML, вся вёрстка в CSS!
Интерактивный гайд по ссылке ниже


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🔖 Davia: Документация к коду за одну команду

Ненавидишь писать документацию? Davia её генерирует автоматически с диаграммами!

💡 Как это работает

1️⃣ Установи Davia
2️⃣ Запусти команду в папке проекта
3️⃣ Готово! Полная документация со структурой кода и визуальными диаграммами

davia generate


📎 Что получишь

✅ Чистая документация всего проекта
✅ Визуальные диаграммы архитектуры
✅ Структурированное описание модулей
✅ Легко просматривать и редактировать

🔸Для кого это полезно

- Онбординг новичков — они разберут проект за часы, а не за дни
- Возврат к старому коду — забыл, как работает? Есть документация!
- Крупные проекты — видишь всю архитектуру визуально
- Open source — пользователи быстрее разберутся
- Code review — проще объяснять изменения

🔸Пример

Вместо этого:
TODO: написать документацию...


Теперь это:
Автоматическая документация с диаграммами модулей, 
зависимостей, API и структуры проекта ✨


Почему это важно ⬇️

Документация часто отстаёт от кода. Davia генерирует её из самого кода, поэтому она всегда актуальна!

Забирай на GitHub и пошли документировать проекты!


📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
🔖 Java: Одна буква может отправить тебя на год в будущее!

Знаешь ли ты разницу между 'Y' и 'y' в паттерне даты Java?

🔸Проблема

Код:
Date date = new Date("2024/12/31");
var formatter = new SimpleDateFormat("dd-MM-YYYY");
System.out.println(formatter.format(date));


Результат:
31-12-2025  ← Уехали на год вперёд! 


А если использовать правильный:
var formatter = new SimpleDateFormat("dd-MM-yyyy");
// 31-12-2024 ✅



🔸 В чём разница?

`y` (lowercase) = обычный год → 2024, 2025

`Y` (UPPERCASE) = неделя-года (ISO 8601) → может быть на год больше/меньше!

🔸 Почему так?

Y использует ISO-8601 стандарт: первая неделя года — та, где четверг в январе.

31 декабря 2024 входит в первую неделю 2025, поэтому Y выдаёт 2025!

1 января 2027 входит в последнюю неделю 2026, поэтому Y выдаёт 2026!

🔸 Реальные баги

❌ Bouncy Castle — нашли несколько мест с 'Y', исправили на 'y'
❌ OpenGrok — то же самое


💡 Правило

🔸 Никогда не используй `Y` в паттерне даты, если не знаешь что это!
🔸 Используй `y` для обычного года



📎 Статья

🎙 Новости

📝 База вопросов
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🤯2❤1