Лицензия Creative Commons
Содержимое блога доступно по лицензии Creative Commons Атрибуция — С сохранением условий
(Attribution-ShareAlike) 3.0 Unported
, если не указано иное.

суббота, 12 ноября 2011 г.

Цена невнимательности, или как я подложил себе интересные грабли при настройке SSH

Доброго времени суток, случайные и неслучайные читатели.

Не так давно, а именно около месяца назад, появилась у меня идея - настроить аутентификацию по публичному ключу на домашний компьютер через SSH. Это более безопастно и более удобно, чем авторизация по паролям. Я не буду здесь объяснять, как настроить аутентификацию по публичным ключам, так как в интернете есть тьма-тьмущая всяких how-to по этой теме - от самых простых, до подробнейших руководств. К примеру, есть цикл статей от Дэниела Роббинса (создателя Gentoo Linux) на IBM DeveloperWorks:

А сейчас я лишь хочу упомянуть про одни грабли, на которые я наступил при настройке SSHD. Причём грабли я подложил сам себе и набил немало шишек, пытаясь найти причину, почему через локальную сеть с ПК на ноутбук аутентификация по ключам работает, а наоборот - нет. Будто бы для настройки SSH на моём ПК под Gentoo Linux требовалось какое-то сакральное знание, которым я не обладал.

Пару дней длился поиск проблемы, были испробованы разные комбинации настроек, двадцать раз проверены права на каталог ~/.ssh и файл authorized_keys (как известно, ssh трепетно относится к выставленным правам на его файлы), в общем - была потрачена уйма времени (я занимался этим в выходные :)). Ничего не помогало - источник проблемы ускальзал от меня с удивительной ловкостью. В конце-концов я принял волевое решение оставить решение этой проблемы до лучших времён.

И вот, сегодня я решил опять попробовать найти источник проблемы. Часто так бывает, что по прошествии некоторого времени проблема решается "за пять минут". Взял свой коммуникатор под управлением Android и установленным ConnectBot, проверил, занесён ли ключ, сгенерированный на коммуникаторе, в файл ~/.ssh/authorized_keys. Всё настроено, пытаюсь подключится - ничего.

$ tail -f /var/log/messages

тоже молчит.

Ну, это мы уже проходили. Неплохо бы получить больше информации в лог. Открываю конфигурационный файл /etc/ssh/sshd_config, нахожу переменную LogLevel - установлена в INFO. Открываю man-страницу на конфиг sshd_config, нахожу описание переменной LogLevel. Значение DEBUG2 включает режим подробного вывода в лог сообщений SSHD. Выставляю LogLevel DEBUG2, перезапускаю SSHD:

$ rc-config restart sshd

Пробую подключиться на ПК с телефона. Ага, вот и подсказка: файл /home/avp/.ssh/autorized_keys не найден. Стоп, мне кажется, или в имени файла ошибка? Пропущена буква "h" в слове "authorized". Переименовываю файл:

$ cd ~/.ssh/
$ mv autorized_keys authorized_keys


Пробую подключиться - работает!..

Вот так, казалось бы, такая мелочь, как одна пропущенная буква, приводит к серьёзным последствиям. Прямо как в стихотворении Самуила Маршака:

Не было гвоздя -
Подкова
Пропала.

Не было подковы -
Лошадь
Захромала.

Лошадь захромала -
Командир
Убит.

Конница разбита -
Армия
Бежит.

Враг вступает в город,
Пленных не щадя,
Оттого, что в кузнице
Не было гвоздя.

четверг, 25 августа 2011 г.

scroller.el - простое дополнение для GNU Emacs

Решил добавить в Emacs функцию, которую не нашёл во всём многообразии keybinding'ов (может, я просто плохо искал?)

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


;;; These simple functions allows scrolling with keeping
;;; position of cursor

(defun scroll-down-keeping-cursor (&optional arg)
  "Scrolling down one line while keep cursor position."
  (interactive "p")
  (if (interactive-p)
      (progn
        (scroll-down arg)
        (scroll-down 1))))

(defun scroll-up-keeping-cursor (&optional arg)
  "Scrolling up one line while keep cursor position."
  (interactive "p")
  (if (interactive-p)
      (progn
        (scroll-up arg)
        (scroll-up 1))))

Теперь неплохо бы привязать эти функции к клавишам:


(global-set-key (kbd "M-p") 'scroll-down-keeping-cursor)
(global-set-key (kbd "M-n") 'scroll-up-keeping-cursor)

Как и при использовании обычных комбинаций C-n и C-p, эти функции можно вызывать в интерактивном режиме, передавая количество строк, на которое следует прокрутить буфер:

C-u 10 M-n

Конечно, если прописать все функции, настройки и keybinding'и в custom.el, они загрузятся. Но IMHO пользовательские файлы для Emacs выглядят гораздо опрятнее, если их разложить по отдельным файлам/каталогам.

Например, пусть это дополнение будет в отдельном файле scroller.el и положим мы его в ~/.emacs.d/. Тогда в ~/.emacs.d/custom.el останется только загрузить этот файл:


(load-file "~/.emacs.d/scroller.el")


Или так:


(setq emacs-dotdir
      (expand-file-name ".emacs.d" "~"))
(load-file (expand-file-name "scroller.el" emacs-dotdir))

пятница, 22 апреля 2011 г.

Проект "Амёба"

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


Содержание

  1. Постановка задачи
  2. «Царство Протозоа, класс Лобозные амёбы, отряд...»
  3. Реализация методов

  4. Разделять и властвовать


Постановка задачи


В один прекрасный день на курсах мы начали изучать классы в C++.


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


Эта задача легко решается с помощью статических (static) полей класса.


Так как в работе с классами я чувствовал себя ещё недостаточно уверенно, то решил пойти дальше и усложнить задачу. Чтобы понять, как всё это работает, я начал создавать класс, который мог бы "делиться", знал бы своих "потомков" и их количество, а так же знал "предков". Но обо всём по-порядку.



«Царство Протозоа, класс Лобозные амёбы, отряд...»


Поскольку я хочу понять работу с классами, с конструирования класса и начнём. Из-за сходства поведения (доступных методов) этого класса с амёбой, я решил назвать мой класс в честь этих микроскопических организмов - clsAmoeba (по-русски, я думаю, это будет что-то вроде "КАмёба", т.е. "Класс амёба"). Название класса само собой стало названием этого проекта.


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


Тем не менее, стандартная КАмёба должна уметь как минимум три вещи:


  1. Делиться;
  2. Сообщать количество уже существующих КАмёб;
  3. Знать всех своих потомков.

Каждая КАмёба имеет свой IDентификационный номер, который тоже умеет сообщать (иначе как мы узнаем, с какой КАмёбой мы имеем дело?).


Я объявил класс в заголовочном файле main.h и включил этот файл с помощью директивы include в другие файлы проекта.


Листинг 1: Класс clsAmoeba.


class clsAmoeba
{
 public:
  clsAmoeba();
  clsAmoeba(const clsAmoeba& pt);
  ~clsAmoeba();

  /* This methods works with the counter */
  void incr()     { counter++;      };
  void decr()     { counter--;      };
  int  getCount() { return counter; };

  /* This method returns ID of an amoeba */
  int getID() { return id; };

  /* This method returns pointer to parent of an amoeba */
  clsAmoeba& getParent() { return *parent; };

  /* Get count of the descendants of an amoeba */
  int getDCount() { return dCount; }

  /* Get descendant of an amoeba */
  clsAmoeba& getDescendant(int i) { return *descendants[i]; }

  /* This method divide an amoeba */
  void divide();

 private:
  /* ID of the amoeba */
  int         id;
  
  /* Pointer to parent of an amoeba */
  clsAmoeba*  parent;
  
  /* Descendants of an amoeba */
  clsAmoeba** descendants;
  int         dCount;

  /* Counter of amoebas */
  static int counter;
};

Методами класса называются функции (некоторые действия), которые может выполнять данный класс.


Полями класса называются переменные этого класса.


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


Доступ к потомкам КАмёбы обеспечен через метод getDescendant(). Этот метод принимает номер потомка (не ID, а именно номер потомка в массиве descendants), и возвращает указатель на класс-потомок. Метод getDCount() возвращает количество потомков.


Самый интересный метод в классе clsAmoeba - метод divide(). При вызове этого метода КАмёба (т.е. класс) делится две части: класс создаёт свою копию в массиве потомков.


Часть методов - например, простые методы вроде getID(), которые просто возвращают значение какого-то поля класса - объявлены прямо в теле класса. Такие классы называются "инлайновыми" (англ. Inline), т.е. в переводе на русский язык, встраиваемыми. И это действительно так - компилятор заменяет в коде вызовы этих методов на код внутри самих методов. Такие методы работают быстрее, так как при работе программы не тратится время на вызов метода. Встраиваемые методы не обязательно объявлять в теле класса - вместо этого, можно использовать ключевое слово inline перед объявлением метода.



Реализация методов


Все не-инлайновые методы КАмёбы я вынес в отдельный модуль amoeba.cpp.


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



Конструкторы и деструкторы


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


Первый конструктор производит инициализацию полей класса, увеличивает счётчик КАмёб на 1 и присваивает текущее значение счётчика в качестве ID созданной КАмёбе. Этот конструктор вызывается при создании первой КАмёбы.


Листинг 2: Конструктор 1.


clsAmoeba::clsAmoeba() :
  parent(NULL),
  descendants(NULL),
  dCount(0)
{
  incr();
  id = counter;
}

Второй конструктор называется конструктором копирования и вызывается в случае деления КАмёбы.


Листинг 3: Конструктор 2.


clsAmoeba::clsAmoeba(const clsAmoeba& pt) :
  descendants(NULL),
  dCount(0)
{
  if(&pt != this)
    {
      incr();
      id = counter;
      parent = (clsAmoeba*) &pt;
    }
}

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


Деструктор вызывается при уничтожении экземпляра класса, и участвует в его уничтожении. В данном случае, он уменьшает на 1 количество "живых" КАмёб и уничтожает своих потомков (что, кстати, не совсем логично).


Листинг 4: Деструктор.


clsAmoeba::~clsAmoeba()
{
  decr();
  parent = NULL;
  /* I think here may be a memory leak
   *   because I don't call delete for the
   *   descendents of the every descendent...
   */
  delete [] descendants;
}


Метод divide()


Представим, что наш первый экземпляр КАмёбы, шевеля ложноножками, выполз из воды на сушу...


Листинг 5: Создаю первый экземпляр КАмёбы (файл main.cpp).


#include <iostream>
#include "main.h"

using namespace std;

int clsAmoeba::counter = 0; // Инициализирую счётчик КАмёб

int main()
{
  clsAmoeba amoeba1;
  
  //...

  return 0;
}

Вокруг - крайне недружелюбная атмосфера. Однако, других живых созданий крупнее нашей КАмёбы мы здесь вряд ли найдём. Поэтому хваталки и кусалки для нашего экземпляра не особенно и нужны (возможно, его потомки обзаведутся этими "фичами"). Что действительно важно, так это возможность создавать себе подобных. В случае с КАмёбой этот процесс достаточно прост - он сводится к делению оригинала надвое. Для этого предназначен метод divide().


Листинг 6: Метод divide() (файл amoeba.cpp)


void clsAmoeba::divide()
{
  if(!dCount)
    {
      descendants = new clsAmoeba*;
      descendants[dCount++] = new clsAmoeba(*this);
    }
  else
    {
      clsAmoeba** tmp = new clsAmoeba* [dCount];
      for(int i = 0; i < dCount; i++)
        tmp[i] = descendants[i];
      
      delete [] descendants;

      dCount++;
      descendants = new clsAmoeba* [dCount];
      for(int i = 0; i < dCount; i++)
        descendants[i] = tmp[i];      
      descendants[dCount-1] = new clsAmoeba(*this);

      delete [] tmp;
    }
}

Если КАмёба делится первый раз, то потомков 0. Следовательно, выделяю память под первого потомка и создаю его. Если потомков на момент вызова метода больше 0, то создаю временное хранилище tmp под существующих потомков, освобождаю память, выделенную под массив descendants. Увеличиваю количество потомков на 1, снова выделяю память под массив descendants, но уже для нового количества потомков. Далее копирую существующих потомков из tmp в descendants. В завершении, создаю нового потомка в конце массива и освобождаю память, выделенную под временный массив tmp.



Разделять и властвовать


Для тестирования программы я создал 24 КАмёбы в main.cpp путём деления amoeba1 в цикле, а потом - её потомков, и потомков потомков...

Листинг 7: Делим, делим, делим...


// ...
  /* I'm making heap of amoebas */
  for(int i = 0; i < 10; i++)
    amoeba1.divide();

  clsAmoeba& amoebaN = amoeba1.getDescendant(2);  
  for(int i = 0; i < 5; i++)
    amoebaN.divide();

  clsAmoeba& amoebaN2 = amoebaN.getDescendant(4);
  for(int i = 0; i < 4; i++)
    amoebaN2.divide();

  clsAmoeba& amoebaN3 = amoebaN2.getDescendant(2);
  for(int i = 0; i < 4; i++)
    amoebaN3.divide();
//...

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

вторник, 15 марта 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 2.

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

В предыдущих постах я:
* Рассказал о том, как как нарисовать равнобедренный треугольник в консоли с помощью звёздочек, и описал проблему создания рисунка, похожего на ель.
* Рассмотрел простой алгоритм отрисовки ёлки, заключающийся в удвоении каждой строки.

Постановка задачи


Последний (и самый, на мой взгляд, интересный) способ отрисовки ели заключается в следующем. Я рисую в строке количество звёздочек, равное номеру строки. Для того, чтобы ель сохраняла некоторую симметричность, я сдвигаю каждую чётную строку вправо на 1 символ.

В итоге, должно получиться следующее:


01      X
02      **
03     ***
04     ****
05    *****
06    ******
07   *******
08   ********
09  *********
10  **********
12      ##
13      ##

Реализация


Для начала, зададим цвет для звезды на верхушке ели, цвет для самой ели и цвет для ствола дерева. Несмотря на то, что программа будет работать в консоли, это не проблема - можно использовать управляющие символы. О них можно прочитать, например, здесь:
http://www.linuxjournal.com/article/8603

Чтобы каждый раз не писать управляющие последовательности для задания цвета строки, я решил поступить следующим образом:

#include <stdio.h>

#define N 15

int main() {
  char c_red[N]    = "\033[49;31;5m"; // Красный
  char c_green[N]  = "\033[49;32;5m"; // Зелёный
  char c_yellow[N] = "\033[49;33;5m"; // Жёлтый
  char c_reset[N]  = "\033[0;0;0m";   // Возврат к первоначальным
                                      //   значениям
  // ...
}

Теперь я могу использовать более осмысленные названия цветов в вызове функции printf(), например:


printf("%sHello %sWorld%s\n", c_yellow, c_green, c_reset); 

Дабы сократить количество вычислений в циклах, заведу дополнительную переменную half_base_width, в которой буду хранить результат деления ширины ели, заданной пользователем, на 2.


  int base_width, half_base_width;
  int i, j;

  printf("Please enter height of the fir-tree:\n> ");
  scanf("%d", &base_width);

  half_base_width = base_width / 2;

  for(i = 1; i <= base_width; i++) {
    /* Печатаю номер строки */
    printf("%s%02d\t", c_red, i);

    /* Печатаю одну линию */
    for(j = 1; j < (half_base_width + (i / 2) + 1); j++) {
      if(((i % 2) >  0) && (j <  (half_base_width - (i / 2))) ||
         ((i % 2) == 0) && (j <= (half_base_width - (i / 2))))
        putchar(' ');
      else
        if(i == 1)
          printf("%sX", c_red);  // Большая звезда на верхушке
        else
          printf("%s*", c_green); // Ель
    }
    
    putchar('\n');
  }

Для повышения реалистичности, в завершении отрисовки ёлки нарисую ещё ствол дерева.


  for(i = base_width + 1; 
      i < (base_width + base_width / 10 * 3); i++) {
    printf("%s%02d\t", c_red, i);
    for(j = 0; j <= half_base_width + (base_width / 10) / 2; j++)
      if(j < half_base_width - (base_width / 10))
        putchar(' ');
      else
        printf("%s#", c_yellow);
    putchar('\n');
  }

Значение (base_width + base_width / 10 * 3) используется для задания высоты ствола в зависимости от высоты ели. Значение half_base_width + (base_width / 10) / 2 и half_base_width - (base_width / 10) используется для задания ширины ствола.

В итоге, ель у меня выглядит вот так:

суббота, 12 февраля 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 1.

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

Теперь пришло время нарисовать первое подобие ёлки.

Постановка задачи


Должно получиться следующее:


   _
  |      X <-------- звезда
  |      *
  |     ***
  |     ***
 10    *****
  |    ***** <------ ёлка
  |   ******* 
  |   *******
  |  *********
  |_ *********
    |----9----|

       рис.1

Всё очень просто: нужно взять за основу алгоритм отрисовки треугольника и удвоить каждую строку: 1, 1, 3, 3, 5, 5...

Высота дерева задаётся пользователем.

Дабы повысить соответствие оригиналу, я хочу поместить на верхушку ёлки "звезду". Ну, вроде тех массивных стеклянных/пластмассовых штуковин, которые часто надевают на верхушку ёлки для усиления праздничного эффекта. За звезду сойдёт английская буква "X" (см. рис.1)

Реализация


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


for(i = 0; i < height; i++) {
  // ...
}

С ним не будет особых проблем. А вот над вложенным циклом нужно подумать. Каким образом реализовать удвоение каждой строки? Посмотрим, как будет расти количество звёздочек в строке, в зависимости от номера строки. Как видно из куска кода выше, переменная i изменяется в цикле от 0 до height - 1 (о чём говорит знак "меньше").


  i | кол. звёздочек в строке
  --|------------------------
  0 | 1        X
  1 | 1        *
  2 | 3       ***
  3 | 3       ***
  4 | 5      *****
  5 | 5      *****
  6 | 7     *******
  7 | 7     *******
  8 | 9    *********
  9 | 9    *********
  ...
        рис.2

Из рисунка выше видно, что количество звёздочек увеличивается на 2 на каждой чётной строке. Это даёт подсказку, как можно реализовать алгоритм.

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


for(i = 1; i <= h; i++)
  for(j = 1; j <= (h + i); j++) {
    if(j <= (h - i + 1))
      putchar(' ');
    else
      putchar('*');
    putchar('\n');
  }

Начальное значение счётчиков i, j равно 1, чтобы обеспечить отступ в один дополнительный символ перед каждой строкой.

Нарисуем 3 строку треугольника (i = 3). Пусть высота h равна 5.


h + i = 8;
h - i + 1 = 3;

 |<------------- 8 ------------->|
  _______________________________
 |   |   |   | * | * | * | * | * |
  -------------------------------
             |<----- j > 3 ----->|

             рис.3

Я думаю, из рисунка 3 можно понять работу этого алгоритма.

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

hw = h / 2 - 1          (1)


Создам новую переменную, и присвою ей результат вычисления половины ширины елки, согласно формуле 1.

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

Я просто разделю i на 2 во всех проверках условий во вложенном цикле. Вот так:

// ...
int hw;
// ...
hw = height / 2 - 1;

for(i = 0; i < height; i++)
  /* Печатаю номер строки в обратном порядке (снизу вверх) */
  printf("%02d\t", height-i);

  for(j = 0; j <= (hw + i / 2); j++) { // <-- здесь
    if(j < (hw - i / 2)) // <-- и здесь
      putchar(' ');
    else
      if(i == 0) // или можно написать: if(!i)
        /* Рисую большую звезду на верхушке ёлки */
        putchar('X');
      else
        putchar('*');

    putchar('\n');
  }
// ...

Если строка нечётная, то hw будет увеличиваться в проверке условия завершения цикла for() и уменьшаться в условии if() - на остаток от деления, т.е. на 1. В результате, количество звёздочек в нечётной строке увеличиться на 2. Если же номер строки - чётный, то никаких изменений не происходит, просто копируется предыдущая строка.

Мне кажется, это потрясающе просто и понятно.

суббота, 29 января 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 0.

Некоторые даже в предпраздничные дни забивают себе голову алгоритмами, программированием и прочими непраздничными вещами.

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

С чего всё началось


А началось всё со следующей задачи.

По условию задачи пользователь вводит число строк, а программа должна нарисовать равнобедренный треугольник с помощью звёздочек - "*". Треугольник должен выглядеть так:


    *
   ***
  *****

Для примера, положим, что я ввёл высоту, равную 5 строкам.


 _
|      *
|     ***
5    *****
|   *******
|_ *********
  |----9----|

Тогда, как видно на примере выше, ширина основания треугольника равна 9 звёздочкам. Если я возьму другую высоту - например, 10 строк - то получу треугольник шириной 19 звёздочек. Отсюда выводим формулу:

w = h / 2 - 1          (1)


где w - ширина, h - высота.

Рисую равнобедренный треугольник


Чтобы нарисовать ровный треугольник, мне нужно помещать определённое количество пробелов перед каждой строкой, в зависимости от номера строки.


 -Что есть-  -Что должно получиться-
1 *              1     *
2 ***            2    ***
3 *****      =>  3   *****
4 *******        4  *******
5 *********      5 *********
 \             /
  номер строки 


Чтобы это осуществить, надо знать номер центрального "столбца".


строки
  1     *
  2    ***
  3   *****
  4  *******
  5 *********
    123456789  <-- столбцы
        ^
   центральный
     столбец

Очевидно, в треугольнике высотой 5 строк, центральным будет 5-й столбец. И тут оказывается, что для нахождения центрального столбца нам нужно знать только высоту h треугольника, введённую пользователем - ведь, если верить формуле 1, то номер центрального столбца всегда будет совпадать с высотой. Это позволяет упростить программу.

Вот, кстати, код на Си:


#include <stdio.h>

int main() {
  int height;
  int i, j;

  printf("Please enter height:\n> ");
  scanf("%d", &height);

  for(i = 1; i <= height; i++) {
    for(j = 1; j <= (height + i); j++) {
      if(j <= (height - i + 1))
        putchar(' '); // Печатаем пробел
      else
        putchar('*'); // Печатаем звёздочку
    }
    putchar('\n'); // Переходим на новую строку
  }

  return 0;    
}

Этот код, несмотря на его простоту, нуждается в дополнительных комментариях для понимания.

Первый цикл работает, пока i меньше или равно половины ширины треугольника. А поскольку, как уже было сказано выше, высота треугольника совпадает с номером центрального столбца, т.е. его центра - то я могу использовать значение введённой пользователем высоты height. Что я и делаю.

Второй цикл работает, пока j меньше или равно height плюс значение i (т.е. номер текущей строки).

Далее я проверяю, если j меньше или равно height минус i плюс 1. Единица здесь задаёт смещение всего треугольника от левой границы экрана. Если условие выполняется, то я печатаю пробел, иначе - звёздочку.

Эта программа решает поставленную задачу - рисует треугольник из звёздочек высотой N строк. Но...

Что не так с этим алгоритмом?..


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

Как бы нарисовать что-то, похожее на ёлку - да ещё так, чтобы эта ёлка высотой N, будучи вписанной в квадрат размером NxN, в основании была так же равной N?

Я подошёл к решению этой проблемы со всей серьёзностью, и нарисовал следующее:


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

Алгоритм отрисовки треугольника я не случайно назвал "алгоритмом номер 0" - следующие два алгоритма отрисовки ёлки реализуются на основе идей, реализованных в программе, решающей задачу с треугольником.

fig. 2 показывает, как можно нарисовать нечто похожее на ёлку - путём удвоения каждой строки. Но ширина основания по-прежнему меньше высоты.

fig. 3 показывает алгоритм отрисовки ёлки. Это как раз то, что нужно. Как видно из рисунка, ширина ёлки при таком способе заполнения строк равна высоте. Причём, здесь можно заметить интересную особенность данного алгоритма - она показана на рисунке справа от fig. 3.

Пока это всё. Предлагаю читателям подумать над реализацией второго и третьего алгоритма отрисовки ёлки.

Реализацию второго алгоритма я рассмотрю в следующем посте.

четверг, 20 января 2011 г.

Нахождение последовательностей одинаковых символов в строке с помощью указателей

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


Я хочу написать функцию, которой можно было бы скормить передать строку, адрес массива указателей - и получить заполненный массив указателей, каждый элемент которого указывал бы на начало каждой последовательности в строке. Раз уж так, почему бы не посчитать заодно количество последовательностей в строке? Пусть эта функция возвращает значение типа integer - количество найденных последовательностей.


Отлично, теперь пора перейти к реализации.


Считываю строку в массив


Мне понадобится символьный массив размером N, массив указателей на тип char (тоже размером N), переменная для хранения количества последовательностей, счётчик для цикла вывода последовательностей и... всё.



#include <stdio.h>

#define N 256

/* Прототип будущей функции */
int findSequences(const char *arr, char **arrPtr);

int main() {
  char charray[N];
  char *sequence[N];
  int count = 0;
  int i;

  printf("Please enter string:\n> ");
  fgets(charray, N, stdin);

  count = findSequences(charray, sequence);

  // ...

  return 0;
}

/* Полное описание функции */
int findSequences(const char *arr, char **arrPtr) {
  // невидимый текст ;)
}

Кое-что о функциях и их прототипах


Прототип функции не является обязательным элементом программы. Он необходим, если нужно обратиться к функции до её полного объявления, и для проверки передаваемых функции параметров. Например, я мог бы написать полное объявление функции findSequences() до функции main() - в таком случае, мне не потребовался бы её прототип. Но у данного способа объявления функций есть два недостатка:

  • Во-первых, если функций много, то главная функция main(), с которой начинается выполнеие любой программы на Си, оказывается где-то далеко в конце файла исходника. Читать такой код стороннему человеку, да и самому тоже, неудобно.
  • Во-вторых,иногда нужно сделать так, чтобы функция f1 вызывала функцию f2, а дать полное описание функции f2 до её использования в функции f1 не представляется возможным. А если из функции f1 нужно вызвать f2, а из f2 - f1 (т.е. необходима косвенная рекурсия)? Тогда без прототипов никак не обойтись.


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


Разумеется, никто не требует давать полное описание всех функций до main(), или же после - и использовать прототипы. Можно комбинировать эти два метода, в зависимости от удобства и задачи.


Всё это, конечно, хорошо, но... что насчёт функции findSequences()?


В поисках последовательностей...


Посмотрим ещё раз на прототип функции findSequences():



int findSequences(const char *arr, char **arrPtr);


Ясно, что она возвращает значение типа integer. А в качестве параметров принимает ссылку на начало строки (т.е. на начало символьного массива charray, в котором хранится строка), и указатель на массив указателей. Причём, ключевое слово const перед типом параметра говорит, что этот параметр ни за что, ни при каких условиях не должен (и не будет) изменяться. Что мне и нужно - я хочу, чтобы функция только искала и считала последовательности в строке, а не портила её.


Пришло время заглянуть внутрь функции.



int findSequences(const char *arr, char **arrPtr) {
  char *p = arr;
  int count = 0;

  arrPtr[0] = p;
  while(*p && ('\n' != *p))
    if(*p != *(p+1)) 
      arrPtr[++count] = ++p;
    else
      p++;

  return count;
}

В начале, я создаю указатель на тип char и присваиваю ему адрес первого элемента массива arr. Соответственно, первый элемент массива указателей arrPtr должен содержать ссылку на первый элемент arr (т.к. начало массива - это начало последовательности).


В цикле я перемещаюсь по массиву arr слева направо, перемещая указатель вправо с каждой итерацией. То есть, я прибавляю к указателю единицу, и он смещается вправо по массиву... нет, не на единицу. Он смещается на одну ячейку массива, т.е. на длину типа char.


Цикл while() работает, пока символ по адресу p не равен 0 (т.е. не достигнут конец строки) и не равен '\n' (т.е. символу перевода строки). Звёздочка '*' перед указателем p говорит о том, что я работаю не самим указателем, а с данными, на которые он указывает. В цикле я делаю проверку на определённое событие - если символ *p не равен символу *(p+1), т.е. символу в следующей ячейке массива, то я увеличиваю счётчик найденных последовательностей на 1, передвигаю указатель вправо, чтобы он указывал на следующую ячейку массива, и сохраняю его адрес в массиве указателей. Поскольку знак инкремента '++' стоит до переменной, которую он увеличивает - то действие инкремента, т.е. увеличения значения переменной на единицу, происходит до того, как её значение будет использовано в выражении.


По завершению цикла, я возвращаю значение count, т.е. количество найденных последовательностей в строке.


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

понедельник, 17 января 2011 г.

Замечания по задаче удаления из строки лишних пробелов

Для решения задачи удаления из строки лишних пробелов я использовал конечный автомат в бесконечном цикле.

Я получил от Антона Александровича (преподавателя из НИИТа) несколько замечаний по решению этой задачи:


1. Бесконечный цикл while(1) можно заменить на цикл while(charray [i]).

Известно, что строка (в отличии от массива символов) всегда заканчивается нулевым символом, он же '\0'. Так же известно, что 0 (ноль) в языке Си (да и во многих других ЯП) означает "ложь" (false). Исходя из этих двух фактов становится ясно, что я могу использовать проверку кода i-того символа из строки в условии завершения цикла while().


while('\0' != charray[i]) {
  ...
  i++;
}

Проверку условия можно упростить следующим образом:

а) while('\0' != charray[i]) - цикл выполняется, пока i-й элемент строки не равен символу '\0'
б) while(0 != charray[i]) - цикл выполняется, пока i-й элемент строки не равен нулю
в) while(charray[i]) - цикл выполняется, пока проверка i-го элемента не возвращает значение "false".

Очевидно, что вышеперечисленные варианты равнозначны. Однако, вариант "в" работает не хуже варианта "а", а если нет разницы - то зачем писать больше? Поэтому, в конечном счёте я использовал вариант "в".

Использование вместо бесконечного цикла while(1) цикл с проверкой i-того элемента строки на соответствие нулевому символу позволило избавиться от двух строк в теле цикла:


if(i == len)
  break;

Понятно, что теперь эти строки не нужны, т.к. цикл завершится, когда проверка charray[i] вернёт "false".


2. Из функций можно выкинуть переменную len2.

Возьму для примера функцию удаления лишних пробелов.


int remove_n(char *arr, int len, int pos) {
  int i;
  int len2 = len-1;

  for(i = pos; i <= len2; i++)
    arr[i] = arr[i+1];
  
  return 0;
}

Я использовал дополнительную переменную len2 для сохранения результата вычисления len-1, чтобы не вычислять это значение при каждой итерации в цикле for(). Как я понимаю, в данном случае использование len2 излишне, и цикл можно переписать следующим образом:


for(i = pos; i <= len-1; i++)
  arr[i] = arr[i-1];

Использование промежуточной переменной для хранения результатов вычислений, которые используются в проверке условия завершения цикла или в теле цикла, оправдано в случае, если эти вычисления достаточно сложны. Или же результат вычисления используется несколько раз в теле цикла. Ещё одна цель использования промежуточных переменных - улучшение читаемости программ человеком (если формулы вычислений достаточно сложны).


3. Проверка на наличие лишнего пробела, после работы конечного автомата, не является "костылём"

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


    _______________________
1. | * | * | * | * |   | 0 |
    -----------------------
                     ^
    _______________________
2. | * | * | * |   |   | 0 |
    -----------------------
                 ^

Конечный автомат находится в состоянии STATE_1, т.е. ждёт появления в строке пробела. В первом случае, теоретически я могу модернизировать программу следующим образом:


while(charray[i])

  switch(state) {

  case STATE_1:
    if(' ' == charray[i]) {
      state = STATE_2;
      if((0 == i) || !charray[i+1])) // <-- здесь
        continue;
    }
    break;

  case STATE_2:
    ...
    break;

  }

  i++;
}
...

То есть, если символ, находящийся в строке на позиции i+1 является нулём, я перехожу к следующей итерации цикла while(charray[i]) без инкремента i. Таким образом, я могу удалить последний пробел тем же способом, что я удаляю все пробелы в начале строки.

Но если я попробую использовать приведённый выше код для второго варианта (когда пробелов в конце строки больше 1), то опять же получу один лишний пробел в конце строки после работы программы. Почему? А потому, что условие !charray[i+1] никогда не выполнится уже в том случае, если количество пробелов в конце строки будет хотя бы равно двум: элемент строки, находящийся на позиции i+1, так же будет являться пробелом.

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

Тем не менее, я понял, что можно обойтись без дополнительной проверки длины строки, чтобы удалить последний пробел в строке. Ведь переменная i в цикле while() увеличивается до тех пор, пока i-тый элемент в строке не будет равен нулю.

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


...
while(charray[i]) {
  ...
  i++;
}

if(' ' == charray[i-1])
  charray[i-1] = 0;
...

понедельник, 10 января 2011 г.

Заполнение массива равным количеством случайных положительных и отрицательных чисел

В процессе решения одной из задач столкнулся с интересной проблемой. Как заполнить массив размером N случайными положительными и отрицательными числами так, чтобы количество отрицательных чисел в массиве было равно количеству положительных? И не подряд (например, сначала отрицательные числа, потом - положительные), а, цитируя знаменитое произведение Льюиса Кэрролла в переводе Бориса Заходера, "строго как попало"?

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

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

Генерирую случайное число


Для генерации случайных чисел, мне потребуются библиотеки


#include <stdlib.h>
#include <time.h>


Разумным будет задание размера массива N и максимальное значение MAX для генератора случайных чисел с помощью define:


#define N 10
#define MAX 100


Далее нужно запустить генератор случайных чисел с помощью srand() на основе текущего времени, возвращаемого функцией time(). Внутрь функции я поместил цикл while(), и с каждой новой итерацией будет генерироваться случайное число с помощью функции rand() и сохраняться в переменную buf.

Чтобы генерировать числа от 0 до 99, я должен получить остаток от деления результата работы rand() на MAX.


srand(time(0)); // Завожу генератор случайных чисел
buf = rand() % MAX; // Генерирую случайное число


Отлично, но мне нужно, чтобы генератор случайных чисел генерировал и отрицательные числа. В таком случае, нужно вычесть из полученного случайного числа другое число. Например, если мне требуется получить случайные числа в диапазоне от -50 до 49, то я могу сделать следующее:


buf = rand % MAX - (MAX / 2);


Теперь самое интересное. Мне предстоит...

Заполнение массива


Как заполнить массив одинаковым количеством положительных и отрицательных чисел в случайном порядке? Судя по условию поставленной задачи, количество положительных и отрицательных чисел в массиве размером N должно быть равно N / 2.

Отсюда следует, что можно проследить за количеством накиданных в массив положительных и отрицательных чисел. Так же можно предварительно сохранить случайно сгенерированное число в некий буфер, и посмотреть его знак. На основании этого, принимать решение - заносить очередное сгенерированное число в массив, или нет?

Отлично. Завожу два счётчика, один из которых будет считать количество "положенных" в массив отрицательных чисел, другой - положительных. Цикл while() будет выполняться, пока rand() не сгенерирует достаточное количество случайных чисел для заполнения массива.


int array_fill(int *arr, int len, int max) {
  int max_h = max / 2;
  int len_h = len / 2;
  int buf;
  int p = 0; // Счётчик положительных чисел
  int n = 0; // Счётчик отрицательных чисел
  int i = 0;

  srand(time(0));
  
  while(i < len) {
    buf = rand() % max - max_h;
 
    if((buf < 0) && (n < len_h)) {
      arr[i] = buf;
      n++;
      i++;
    }

    if((buf >= 0) && (p < len_h)) {
      arr[i] = buf;
      p++;
      i++;
    }
    
  }

  return 0;
}

Итак, предположим, что "лимит" на отрицательные числа исчерпан (n == len_h). Цикл будет повторяться, пока в buf не окажется положительное число. То же произойдёт, если p == len_h, только в этом случае цикл будет повторяться до появления в buf отрицательного числа. Таким образом, мы получаем массив, заполненный в равном количестве как положительными, так и отрицательными числами. Задача решена.

Пример вызова функции:


array_fill(arr, N, MAX);



P.S. На самом деле, у этой функции есть один недостаток - при нечётном размере массива она зацикливается. Этот недостаток можно преодолеть, добавив в функцию проверку на чётность размера массива. Если проверка даёт положительный результат, то остаётся сгенерировать ещё одно число и записать его в начало (или конец) массива.

суббота, 8 января 2011 г.

Удаление из строки "лишних" пробелов

У меня есть программа, которая должна запросить у пользователя строку, и произвести с полученной строкой определённые действия. Но вот ведь незадача - некоторые очень неаккуратно работают с клавиатурой, и при вводе данных могут случайно нажать пробел несколько раз вместо одного, добавить совершенно ненужных пробелов в начале строки, или даже в конце. А представьте, что будет, если по клавиатуре пройдётся ваш любимый кот? Это будет катастрофа! Но спокойно, можно предусмотреть и это. Далее я хочу рассмотреть метод (несомненно, один из многих) удаления лишних пробелов из строки.

Для начала определим, какие пробелы считаются лишними. Лишними считаются
  1. все пробелы, которые стоят в начале строки (т. е. перед первым символом строки, не являющегося пробелом);
  2. пробелы между символами, если количество идущих подряд пробелов равно двум или более;
  3. все пробелы в конце строки.

По условию задачи, нельзя пользоваться дополнительными массивами, кроме как массивом для хранения введённой пользователем строки.

Копать или не копать?


Для решения этой задачи удобно использовать так называемый конечный автомат. То есть, некий абстрактный автомат, который может пребывать в конечном количестве состояний. Например, в двух: "копать" или "не копать". Этот автомат будет переключаться из одного состояния в другое, в зависимости от выполнения каких-то условий.

Итак, звучит неплохо. Осталось придумать, как это использовать для решения задачи удаления пробелов.

Но прежде, чем что-то удалять из строки, мне нужна сама строка.

Получаю строку c помощью функции fgets()


Напишем работающую программу, которая считывает строку.


#include <stdio.h>
#include <string.h>
#define N 256

int main() {
  int charray[N];

  printf("Please enter string:\n> ");
  fgets(charray, N, stdin);
  
  return 0;
}

Эта программа пока только считывает строку со стандартного ввода (stdin) с помощью функции fgets() в массив символов размером N.

У функции fgets() есть замечательное свойство - используя её, я никогда не выйду за границу массива, если попытаюсь ввести строку длиннее, чем размер массива. Однако, у неё есть одна особенность: стоит мне ввести строку и нажать [Enter], как в конец введённой строки добавиться знак "\n", т.е. символ перехода на новую строку. Эта проблема решается гениально просто. Нужно лишь узнать длину введённой строки с помощью strlen():


len = strlen(charray); // Определяем длину строки
if('\n' == charray[len-1]) // Если предпоследний символ в строке='\n'
  charray[len-1] = 0;      // то заменяем его на нулевой символ, 
                           // т.е. символ конца строки, он же '\0'

Теперь у меня есть строка, готовая к дальнейшей обработке.

Реализация конечного автомата


Для удаления одного лишнего пробела, нужно сдвинуть элементы массива влево на 1. То есть, если пробел находится на позиции S, то на его место запишется символ из ячейки S+1, вместо S+1 запишется S+2 и т.д.

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


int remove_spc(char *arr, int len, int pos) {
  int i;
  int len2 = len-1;

  for(i = pos; i <= len2; i++)
    arr[i] = arr[i + 1];

  return 0;
}

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

Самое забавное, что функции remove_spc() всё равно, какой элемент стоит после позиции pos - она так же будет сдвигать все пробелы, которые следуют за этой позицией. Более того, этой функции безразлично, какой элемент стоит на текущей позиции pos

Поэтому мне требуется что-то большее, чем эта функция. Лучше всего подходит для этой цели бесконечный цикл. В него я и помещу свой автомат. К слову, сейчас самое время подумать над его реализацией. Конечному автомату для решения этой задачи потребуется только два состояния, назову их STATE_1 и STATE_2. Находясь в состоянии STATE_1 автомат будет просто проверять, является ли текущий символ в просматриваемом массиве пробелом. Если пробел обнаруживается, автомат переключается в состояние STATE_2. В этом состоянии он будет копать удалять лишние пробелы.

Работу автомата удобно представить ввиде ленты и считывающей/записывающей головки над ней. Пустые ячейки - это пробелы, звёздочками отмечены занятые ячейки, т.е. в которых присутствуют символы, не являющиеся пробелами.

   _______________________________
1 | * | * |   |   |   | * | * | * |
   -------------------------------
        ^
   _______________________________
2 | * | * |   |   |   | * | * | * |
   -------------------------------
            ^
   _______________________________
3 | * | * |   |   |   | * | * | * |
   -------------------------------
                ^
   _______________________________
4 | * | * |   |   | * | * | * |   |
   -------------------------------
                ^
   _______________________________
5 | * | * |   | * | * | * |   |   |
   -------------------------------
                ^

На шаге 1 автомат находится в состоянии STATE_1. Ячейка занята, так что считывающая головка передвигается вправо на одну клетку.

На шаге 2 автомат всё ещё пребывает в состоянии STATE_1. Считывающая головка находится над пустой ячейкой, поэтому автомат перемещает считывающую головку вправо на одну ячейку и переключается в состояние STATE_2. Но здесь не всё так просто. Если мы хотим удалить пустую ячейку в начале строки, то нам не нужно перемещать считывающую головку на следующую ячейку перед переключением в STATE_2.

Теперь посмотрим, что же происходит, когда автомат находится в состоянии STATE_2. А происходит вот что: автомат начинает выкидывать пробелы из строки, вызывая функцию remove_spc (шаг 4). При этом, сама "считывающая головка" остаётся неподвижной, и после каждого вызова функции проверяет содержимое ячейки, над которой она находится.
Как только в ней оказывается какой-либо символ (шаг 5), не являющийся пробелом, автомат переключается вновь в состояние STATE_1.

Вот, как это выглядит в программном коде:


#define STATE_1 1
#define STATE_2 2

...

while(1) {
  switch(state) {
  
  case STATE_1:
    if(' ' == charray[i]) {
      state = STATE_2;
      if(0 == i)
        continue;
    }
    break;

  case STATE_2:
    if(' ' != charray[i])
      state = STATE_1;
    else {
      remove_spc(charray, len, i);
      continue;
    }
    break;

  } // end switch

  i++;

  if(i == len)
    break;

} // end while

А теперь плохая новость. После работы автомата, при количестве лишних пробелов в конце строки больше одного, после последнего символа всё равно оставался один пробел. Я не смог пока разобраться, почему так происходит. Поэтому, после завершения бесконечного цикла while(1), пришлось использовать костыль в виде следующих строчек:


len = strlen(charray);
if(' ' == charray[len-1])
  charray[len-1] = 0;

Этот кусок кода удаляет самый последний пробел, если он остаётся после работы автомата.

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

Исходник:
remove-spaces.c

пятница, 7 января 2011 г.

Вывод приветствия в зависимости от времени, введённого пользователем

Задача заключается в том, чтобы запросить у пользователя время в формате ЧЧ:ММ:СС и вывести приветствие на английском языке - "Good morning!", "Good evening!" etc.

Задача достаточно простая, для ввода данных используется функция scanf(), которая принимает время в заданном формате. Далее мы должны проверить часы (ЧЧ) на принадлежность определённому времени суток.


Основная идея заключается в том, чтобы сопоставить временные интервалы с приветствием:

  • Утро - с 6 часов до 12 - "Good moring!"
  • День - с 12 до 18 - "Good afternoon!"
  • Вечер - с 18 до 22 - "Good evening!"
  • Ночь - с 22 до 6 - "Good night!"

Попробую усовершенствовать задачу. Пусть программа не только приветствует нас, но и сообщает человеческим голосом по-английски текущее время. Например, "It's twenty to nine." Для этого создам новый тип:


typedef char TString[60];


Далее, создаю массив из 12 элементов типа TString, в которые сразу заношу части фраз, характеризующие положение минутной стрелки (т.е. текущую часть часа):


TString currenttime[12]={"o'clock",          // 0
                         "five past",        // 1
                         "ten past",         // 2
                         "quarter past",     // 3
                         "twenty past",      // 4
                         "twenty-five past", // 5
                         "half past",        // 6
                         "twenty-five to",   // 7
                         "twenty to",        // 8
                         "quarter to",       // 9
                         "ten to",           // 10
                         "five to"};         // 11

В отдельную функцию можно вынести операцию определения положения минутной стрелки на основе введённых пользователем минут (ММ).

Так же хорошей мыслью будет осуществить все проверки на корректность введённых данных до начала их обработки - до того, как мы будем определять часы и количество введённых минут.

Всё хорошо, но почему бы программе, раз уж я взялся её учить говорить на человеческом языке, не называть часы словами? Для этого мне потребуется ещё один массив, размером в 23 элемента (массивы нумеруются с нуля, поэтому 23, а не 24). Тип использую тот же, TString.

В моём варианте секунды не учитываются вообще. Но думаю, можно найти применение и им.

Пример работы программы:


Please enter a time:
> 21:40:55
Good evening!
It's twenty to twenty-two.


Исходник:
greetings.c

Об этом блоге

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