Этюд: работа с char-строками

arduinec
Offline
Зарегистрирован: 01.09.2015

Для работы со строками обычно используются функции и методы из класса String, несмотря на то, что они неэкономно используют память.
Строки из массивов элементов char позволяют контролировать практически каждый байт, а для работы с ними существует достаточное количество функций, которые нужно только правильно применить.

В данном этюде разбирается строка с использованием основных строковых функций.

Сначала скетч (в нём есть некоторые лишние действия, но новичкам так должно быть понятнее):

void setup()
{
  long dnum1, dnum2;
  int god, god1;
  byte den, mes, mes1, dned, num;
  char *uk1, *uk2, stemp[15];

  char stroka[] = "22.06.1941 - 9.5.1945";
  
  Serial.begin(9600);
  
  uk1 = strchr(stroka, '.');
  if (uk1 == NULL) { Serial.println("error"); return; }
  
  num = uk1 - stroka;
  strncpy(stemp, stroka, num);
  stemp[num] = 0;
  den = atoi(stemp);
  
  uk2 = strchr(uk1+1, '.');
  if (uk2 == NULL) { Serial.println("error"); return; }
  
  num = uk2 - uk1 - 1;
  strncpy(stemp, uk1+1, num);
  stemp[num] = 0;
  mes1 = atoi(stemp);
  
  uk1 = uk2;
  uk2 = strstr(uk1+1, " - ");
  if (uk2 == NULL) { Serial.println("error"); return; }
  
  num = uk2 - uk1 - 1;
  strncpy(stemp, uk1+1, num);
  stemp[num] = 0;
  god1 = atoi(stemp);
  
  if (mes1 < 3) { mes = mes1 + 9; god = god1 - 1; }
  else { mes = mes1 - 3; god = god1; }
  
  dnum1 = 64 + den + (153*mes+2)/5 + 365L*god + god/4 - god/100 + god/400;
  dned = dnum1 % 7 + 1;
  
  sprintf(stemp, "%02i.%02i.%04i = %i", den, mes1, god1, dned);
  Serial.println(stemp);
  
  uk1 = uk2;
  uk2 = strchr(uk1+3, '.');
  if (uk2 == NULL) { Serial.println("error"); return; }
  
  num = uk2 - uk1 - 3;
  strncpy(stemp, uk1+3, num);
  stemp[num] = 0;
  den = atoi(stemp);
  
  uk1 = uk2;
  uk2 = strchr(uk1+1, '.');
  if (uk2 == NULL) { Serial.println("error"); return; }
  
  num = uk2 - uk1 - 1;
  strncpy(stemp, uk1+1, num);
  stemp[num] = 0;
  mes1 = atoi(stemp);
  
  strcpy(stemp, uk2+1);
  god1 = atoi(stemp);
  
  if (mes1 < 3) { mes = mes1 + 9; god = god1 - 1; }
  else { mes = mes1 - 3; god = god1; }
  
  dnum2 = 64 + den + (153*mes+2)/5 + 365L*god + god/4 - god/100 + god/400;
  dned = dnum2 % 7 + 1;
  
  sprintf(stemp, "%02i.%02i.%04i = %i", den, mes1, god1, dned);
  Serial.println(stemp);
  
  Serial.print("data2 - data1 = "); Serial.println(dnum2 - dnum1);
}

void loop() {}

/*
rezultat:

22.06.1941 = 7
09.05.1945 = 3
data2 - data1 = 1417
*/

Теперь расшифровка (слева приводятся номера объясняемых строк):

08. Задача: вычислить количество дней между датами из прилагаемой строки. Решение простое: переводим строку в числа, вычисляем количество дней от нулевого года до каждой даты и считаем разность между ними.

06. Для работы потребуются два указателя и одна вспомогательная строка (её длина рассчитана впритык).

12. Ищем с помощью функции strchr() в строке первую точку.

13. Если ничего не найдено, то печатаем "ошибка" и завершаем работу - в данном случае выходим из setup().

15. Вычисляем количество символов, которые нужно скопировать во вспомогательную строку.

16. Копируем с помощью функции strncpy() нужное количество символов.

17. Так как функция strncpy() не ставит после копирования нулевой символ завершения строки, то ставим его сами.

18. Получаем из вспомогательной строки числовое значение.

20. Ищем вторую точку в строке.

23-24. Копируем со следующего символа после указателя, поэтому uk1+1, из-за этого количество копируемых символов num уменьшается на 1.

28. Передача адреса от одного указателя к другому (необязательное действие, сделано для упрощения понимания скетча).

29. С помощью функции strstr() ищем в строке подстроку " - ", указатель получит адрес первого элемента, с которого подстрока входит в строку - в нашем случае это элемент сразу после 1941.

32-35. Копируем нужное количество символов и получаем из них число.

37-38. Вспомогательные манипуляции для вычислений.

40. Формула для вычисления количества дней от первой даты нулевого года взята из давних времён программируемых калькуляторов и немного преобразована для целочисленной арифметики микроконтроллера.

41. Заодно вычисляем день недели для полученной даты: 1 соответствует понедельнику, 7 - воскресенью.

43. Используя sprintf(), во временную строку делаем форматированный вывод полученной даты и дня недели.

47. Ищем третью точку в строке.

56. Ищем четвёртую точку в строке. Аналогичный результат можно получить с помощью функции strrchr(stroka, '.') найдя последнюю точку в строке.

64. С помощью функции strcpy() просто копируем остаток строки. В нашем случае можно и не копировать, а сразу использовать atoi(uk2+1).

76. Выводим интервал между датами. Задача решена!

Ещё некоторые строковые функции:

strcmp(stroka1, stroka2) сравнивает строки и возвращает значение больше, меньше или равно нулю, аналогично тому как stroka1 относится к stroka2.
Применять можно например так:
if (strcmp(stroka1, stroka2) == 0) { /* строки равны */ }

strncmp(stroka1, stroka2, num) аналогична strcmp(), но сравнивает только первые num символов.

strcat(stroka1, stroka2) добавляет stroka2 к stroka1, при этом stroka1 должна иметь достаточный размер, чтобы обе строки в неё поместились.

strlen(stroka) вычисляет длину строки (без нулевого символа).

strrev(stroka) переворачивает (обращает) строку: "1234" => "4321".

Полный список и краткое описание строковых функций можно посмотреть здесь:
http://www.nongnu.org/avr-libc/user-manual/group__avr__string.html

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Щас набигут и начнётся ограбление корованов.

b707
Offline
Зарегистрирован: 26.05.2017

arduinec, что-то длинно - не находите? 6 одинаковых конструкций извлечения чисел из строки? Используйте циклы...

Вот этот код извлечет из Вашей строки все нужные числа, преобразует в int и сложит в массив. Календарную математику копировать не стал. Раз уж мы о разборе строк - сосредоточимся только на нем.

void setup()
{
  char *uk1;
  int den_mes_god[5];
  byte i=0;

  char stroka[] = "22.06.1941 - 9.5.1945";
  
  uk1 = strtok (stroka," .-");
  while (uk1 != NULL)
  {
    den_mes_god[i] = atoi(uk1);
    uk1 = strtok (NULL, " .-");
    i++;
  }

  
}

update: сорри за многократное редактирование. Учусь выражать мысли более связно. Суть сообщения при правках не менял, только комменты.

arduinec
Offline
Зарегистрирован: 01.09.2015

b707 пишет:

arduinec, что-то длинно - не находите?

Старался показать работу разных строковых функций.
Перед скетчем кстати написал, что там есть лишние действия :)

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

arduinec
Offline
Зарегистрирован: 01.09.2015

Продолжу этюд с теми же действиями в цикле:

void setup()
{
  int den_mes_god[6];
  byte i=0, num;
  char *uk1, *uk2, stemp[5];

  char stroka[] = "22.06.1941 - 9.5.1945";

  Serial.begin(9600);

  uk1 = stroka;
  uk2 = strpbrk(stroka, " .-");

  while (uk2 != NULL)
  {
    num = uk2 - uk1;
    if (num > 0) {
      strncpy(stemp, uk1, num);
      stemp[num] = 0;
      den_mes_god[i] = atoi(stemp);
      i++;
    }
    uk1 = uk2 + 1;
    uk2 = strpbrk(uk1, " .-");
  }

  if ((stroka + strlen(stroka) - uk1) > 0) {
    den_mes_god[i] = atoi(uk1);
  }

  for(i=0; i<6; i++) Serial.println(den_mes_god[i]);
}

void loop() {}

Функция strpbrk(stroka1, stroka2) возвращает указатель на первое появление в stroka1 любого символа из stroka2; если таких символов в stroka1 нет, то возвращается NULL.

arduinec
Offline
Зарегистрирован: 01.09.2015

Так как b707 дал код с ошибкой и не смог описать как работает strtok(), то восполню этот пробел.

Функция strtok(stroka1, stroka2) рассматривает stroka1, как состоящую из последовательности из нуля или более лексем, выделенных с помощью символов из stroka2. Первый вызов функции strtok возвращает указатель на первый символ первой лексемы в строке stroka1 и записывает нулевой символ в stroka1 непосредственно  сразу за выделенной лексемой. Последующие вызовы со значением NULL в качестве первого аргумента будут обрабатывать stroka1 таким же образом, пока не кончатся все лексемы (strtok возвратит NULL).
Особенности:
Функция strtok модифицирует исходную строку stroka1, поэтому не может применяться к const строкам.
Между вызовами strtok хранит где-то промежуточную информацию.

b707
Offline
Зарегистрирован: 26.05.2017

Из сравнения кода в сообщениях #2 и #4 хорошо видны премущества strtok() перед strpbrk()

Функция strtok(stroka1, stroka2) не только ищет в stroka1 любой символ из набора stroka2. но и заменяет найденные символы на нулевой терминатор, что позволяет извлекать части stroka1 без промежуточного копирования. Кроме этого,у strtok есть механизм повторного вызова - заменяя первый параметр на NULL -strtok(NULL, stroka2) -мы запускаем поиск в stroka1 c последней найденной позиции, что делает ненужным постоянное обновление указателя на строку поиска.

Как итог - код получается почти вдвое короче.

ЗЫ вообще говоря, strpbrk() - одна из десятков Си-шных строковых функций, далеко не самая "ходовая" и в парсинге почти не используется. Несколько странный выбор для этюда для новичков.

 

b707
Offline
Зарегистрирован: 26.05.2017

arduinec пишет:

Так как b707 дал код с ошибкой и не смог описать как работает strtok(), то восполню этот пробел.

Не вижу смысла описывать то, что уже описано в любои онлайн справочнике языка. Задача этюдов - дать новичкам понятия, что такие функции вообще существуют, иллюстрируя примерами. А конкретный синтаксис вызова пусть ищут сами.

Будьте добры увказать на ошибку.

arduinec
Offline
Зарегистрирован: 01.09.2015

b707 пишет:

Задача этюдов - дать новичкам понятия, что такие функции вообще существуют, иллюстрируя примерами.

Что я и сделал: продемонстрировал работу и описал 12 строковых функций (в том числе и strpbrk).

b707 пишет:

Будьте добры увказать на ошибку.

В строке 6 чисел, а массив рассчитан только на 5.

b707
Offline
Зарегистрирован: 26.05.2017

так, ошибку можете не показывать. Последнее число не будет найдено.

 

b707
Offline
Зарегистрирован: 26.05.2017

arduinec пишет:

В строке 6 чисел, а массив рассчитан только на 5.

Скорее опечатка. Настоящая ошибка в другом. Чисел будет извлечено только пять, так что массив как раз верный :)

В рамках использования только strtok() вытащить последнее число не так то просто, лучше к вашей строке добавить в конце пробел  :)

arduinec
Offline
Зарегистрирован: 01.09.2015

b707 пишет:

Скорее опечатка. Настоящая ошибка в другом. Чисел будет извлечено только пять, так что массив как раз верный :)

В рамках использования только strtok() вытащить последнее число не так то просто, лучше к вашей строке добавить в конце пробел  :)

Ну как обычно на данном форуме: выкладываешь код, затем появляется "специалист" который "знает код лучше", а потом выясняется, что он даже свой код не знает.

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

void setup()
{
  char *uk1;
  int den_mes_god[9];
  byte i=0, j, k;
 
  char stroka[] = "22.06.1941 - 9.5.1945";

  k = strlen(stroka);
   
  Serial.begin(9600);
  
  uk1 = strtok (stroka," .-");
  while (uk1 != NULL)
  {
    den_mes_god[i] = atoi(uk1);
    Serial.print(den_mes_god[i]);
    Serial.print(" ");
    uk1 = strtok (NULL, " .-");
    i++;
  }
  Serial.println();

  for(j=0; j<=k; j++) {
    Serial.print(stroka[j], HEX);
    Serial.print(" ");
  }
  Serial.println();
}

void loop() {}

Результат:
22 6 1941 9 5 1945
32 32 0 30 36 0 31 39 34 31 0 2D 20 39 0 35 0 31 39 34 35 0

Извлекаются все 6 чисел.

b707
Offline
Зарегистрирован: 26.05.2017

arduinec пишет:

Ну как обычно на данном форуме: выкладываешь код, затем появляется "специалист" который "знает код лучше", а потом выясняется, что он даже свой код не знает.

да ладно, совсем застыдили :) А это ведь вы меня сбили, когда  написали, что в коде ошибка - вот я и полез искать, хотя на самом деле код был почти правильный :)

Предлагаю продолжить. Выкладывайте что-нибудь посложнее. А я буду вставлять комментарии :) Из меня плохой рассказчик, зато хороший оппонент :)

arduinec
Offline
Зарегистрирован: 01.09.2015

b707 пишет:

да ладно, совсем застыдили :)

Это было высказывание про ситуацию на форуме. Во многих темах спорят из-за какой-нибудь ерунды, а через несколько десятков постов выясняется, что они совсем про разные вещи говорят.

b707 пишет:

А это ведь вы меня сбили, когда  написали, что в коде ошибка - вот я и полез искать, хотя на самом деле код был почти правильный :)

Функцию print пока никто не отменял :)

b707 пишет:

Предлагаю продолжить. Выкладывайте что-нибудь посложнее.

Изначально я планировал выложить посложнее пример с strtok с учётом того, что при повторных вызовах strtok(NULL, stroka2) может меняться stroka2. Но теперь думаю смысла нет, пусть новички с уже выложенным кодом сначала разберуться.

ЕвгенийП тоже хотел этюд про парсинг выложить, может он что-нибудь добавит.

b707 пишет:

А я буду вставлять комментарии :) Из меня плохой рассказчик, зато хороший оппонент :)

Комментаторов, плохих рассказчиков и оппонентов тут полфорума :)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

arduinec пишет:

ЕвгенийП тоже хотел этюд про парсинг выложить, может он что-нибудь добавит.

Нет, у меня была совсем другая задумка, с Вашей идеей тьюториала по строковым функциям не пересекается. Я хотел выложить атоматный разбор строк. Т.е. тьюториал не по функциям, а по построению лексическго анализатора. По сути, компактную и усечённую версию регулярных выражений (микро-RE) - типа как я тут как-то делал microAjacs. И кучу примеров использования.

При этом хотелось заодно показать технику надстраивания высокоуровнего функционала на разные низкоуровневые дела, т.е. создать один инструмент для работы как с готовыми строками (хоть char *, хоть String) так и с потоками  (Serial, SoftwareSerial и т.п.), а заодно и со всем остальным (SPI, I2C).

b707
Offline
Зарегистрирован: 26.05.2017

arduinec пишет:

Комментаторов, плохих рассказчиков и оппонентов тут полфорума :)

тем не менее, в этой теме их нет, так что цените вашего единственного активного читатетеля :)

arduinec
Offline
Зарегистрирован: 01.09.2015

ЕвгенийП пишет:

Нет, у меня была совсем другая задумка, с Вашей идеей тьюториала по строковым функциям не пересекается. Я хотел выложить атоматный разбор строк. Т.е. тьюториал не по функциям, а по построению лексическго анализатора. По сути, компактную и усечённую версию регулярных выражений (микро-RE) - типа как я тут как-то делал microAjacs. И кучу примеров использования.

При этом хотелось заодно показать технику надстраивания высокоуровнего функционала на разные низкоуровневые дела, т.е. создать один инструмент для работы как с готовыми строками (хоть char *, хоть String) так и с потоками  (Serial, SoftwareSerial и т.п.), а заодно и со всем остальным (SPI, I2C).

Мне было бы интересно посмотреть.