[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C. График с координатными осями

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

Пример графика с координатными осями  
C.1 Список переменных print-graph  
C.2 Функция print-Y-axis  Градуируем вертикальную ось.
C.3 Функция print-X-axis  Печатаем горизонтальную ось.
C.4 Печать полного графика  

Пример графика с координатными осями

Так как вставляемые символы заполняют буфер вправо и вниз наша новая функция должна вначале печатать вертикальную ось (ось Y), затем тело графика и наконец горизонтальную ось (ось X). Работу будущей функции можно представить следующим образом:

  1. Инициализация необходимых переменных.

  2. Печать оси Y.

  3. Печать графика.

  4. Печать оси X.

Вот как будет выглядеть наш график:

 
    10 -          
                  *
                  *  *
                  *  **
                  *  ***
     5 -      *   *******
            * *** *******
            *************
          ***************
     1 - ****************
         |   |    |    |
         1   5   10   15

На этом графике координатные оси пронумерованы числами, однако иногда по горизонтальной оси откладывается время и тогда лучше градуировать единицами времени, например, месяцами, как в следующем графике:

 
     5 -      * 
            * ** *
            *******
          ********** **
     1 - **************
         |    ^      |
         Jan  June   Jan

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

Все эти рассуждения приводят нас к следующему шаблону для будущей функции print-graph:

 
(defun print-graph (numbers-list)
  "документация..."
  (let ((height  ...
        ...))
    (print-Y-axis height ... )
    (graph-body-print numbers-list)
    (print-X-axis ... )))

Теперь можно по очереди реализовывать каждую часть определения функции print-graph.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.1 Список переменных print-graph

При создании функции print-graph первая задача придумать список локальных переменных для выражения let. (Пока мы не будем задумываться о документировании функции и о том будет ли она интерактивной или нет.)

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

Аналогично и функции для печати оси X и функции print-graph-body необходимо знать ширину каждого символа, поэтому лучше произвести вычисление ширины символа при создании списка аргументов и так же немного изменить предыдущее определение print-graph-body.

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

Все это приводит нас к следующей форме списка переменных выражения let для функции print-graph:

 
(let ((height (apply 'max numbers-list)) ; First version.
      (symbol-width (length graph-blank)))

В последствии мы увидим, что это выражение не совсем правильно.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.2 Функция print-Y-axis

Задача функции print-Y-axis пронумеровать вертикальную ось, что будет выглядеть примерно следующим образом:
 
    10 -
        
        
        
        
     5 -
        
        
        
     1 -

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

Впрочем легко сказать и даже нарисовать, как должна выглядеть ось Y; но создать функцию это совсем другое дело. Будет не совсем правильно, если мы скажем, что нам нужен номер и метка каждые пять строк: ведь между `1' и `5' только три строки (строки 2, 3 и 4), а между `5' и 10 уже четыре строки (строки 6, 7, 8, 9). Лучше скажем, что нам нужна метка и номер в начале оси и потом метка с номером каждые следующие пять строк.

Теперь, надо обдумать чему будет равна последняя метка. Предположим, что максимальная высота колонки графика равна 7. Какой должна быть верхняя отметка по оси Y --- `5 -', но тогда график будет выше отметки? Или верхняя метка должна быть равна `7 -', чтобы мы знали чему равна вершина графика? А может быть верхняя метка должна быть равна 10 -, ведь 10 делится на пять, но тогда верхняя метка будет расположена выше чем пик графика? Лучше всего реализовать последний случай. В большинстве случаев координатные оси пронумерованы дискретно с шагом 5 --- 5, 10, 15, и так далее. Но как только мы решаем использовать дискретный шаг для вертикальной оси, мы обнаруживаем, что простое выражение для списка переменных вычисляющее высоту неправильно. Выражение (apply 'max numbers-list), вернет максимальную высоту столбца, а нам необходимо значение округленное к ближайшему большему числу кратному пяти. Значит необходимо придумать более сложное выражение.

Как обычно сложная задача станет проще, если ее разделить на несколько простых задач.

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

Самый простой способ определить кратность числа пяти это разделить число на пять и проанализировать остаток. Если остаток равен 0, значит число кратно пяти. Например семь при делении на пять дает в остатке 2, значит семь не кратно пяти. Можно сказать совсем просто, как обьясняют в школе, пять входит в семь один раз с остатком два. Однако пять входит в десять дважды без остатка: значит десять кратно пяти.

C.2.1 Обходной путь: Вычисление остатка  
C.2.2 Создание элементов оси Y  
C.2.3 Создание колонки оси Y  
C.2.4 Окончательная версия print-Y-axis  Печать веритикальной оси.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.2.1 Обходной путь: Вычисление остатка

В Лиспе есть функция для вычисления остатка это %. Эта функция возвращает остаток от деления первого аргумента на второй. Так случилось, что % функция Emacs Lisp, которую вы не сможете обнаружить с помощью apropos: поиск M-x apropos RET remainder RET ничего не выдаст. Единственный способ узнать о существовании % прочесть об этом в каком-нибудь учебнике или изучив исходные тексты Emacs Lisp. Функция % используется в определении функции rotate-yank-pointer, об этом можно прочесть в приложении (See section Тело функции rotate-yank-pointer.)

Вы можете понять работу функцию %, вычислив следующие два выражения:

 
(% 7 5)

(% 10 5)

Первое выражение вернет 2, а второе выражение вернет 0.

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

 
(zerop (% 7 5))
     => nil

(zerop (% 10 5))
     => t

Таким образом следующее выражение вернет t, если высота графика кратна пяти:

 
(zerop (% height 5))

(Значение height, мы найдем с помощью выражения (apply 'max numbers-list).)

С другой стороны, если значение height не кратно пяти, мы должны установить его в большее значение, которое кратно пяти. Это достаточно простое арифметическое действие, которое вполне можно выполнить уже знакомыми нам функциями. Сначала мы разделим значение height на пять для того чтобы определить сколько раз пять входит в это число. Например пять два раза входит в двенадцать, если мы добавим к частному единицу и умножим его на пять, то мы получим большее число кратное пяти. Пять два раза входит в двенадцать, добавив к двум один и умножив на пять мы получим пятнадцать, число кратное пяти и большее чем двенадцать. Соответствующее Лисп выражение приведено ниже:

 
(* (1+ (/ height 5)) 5)

После вычисления следующего выражения вы получите 15:

 
(* (1+ (/ 12 5)) 5)

Во время наших рассуждений мы использовали `пять', как значение дискретного шага по оси Y; но мы вполне можем выбрать и какое-нибудь другое значение, лучше всего, если мы заменим `пять' переменной, которой мы сможем присвоить значение. Самое лучшее имя, которое я смог придумать это Y-axis-label-spacing. Используя новоизобретенную переменную и выражение if, получаем следующее выражение:

 
(if (zerop (% height Y-axis-label-spacing))
    height
  ;; else
  (* (1+ (/ height Y-axis-label-spacing))
     Y-axis-label-spacing))

Это выражение возвращает значение height, если высота кратна значению Y-axis-label-spacing или вычисляет и присваивает переменной height следующее большее число, кратное Y-axis-label-spacing.

Мы можем теперь включить это выражение в список переменных формы let для функции print-graph (конечно после того, как присвоим значение переменной Y-axis-label-spacing):

 
(defvar Y-axis-label-spacing 5
  "Масштаб по вертикальной оси.")

...
(let* ((height (apply 'max numbers-list))
       (height-of-top-line
        (if (zerop (% height Y-axis-label-spacing))
            height
          ;; else
          (* (1+ (/ height Y-axis-label-spacing))
             Y-axis-label-spacing)))
       (symbol-width (length graph-blank))))
...

(Стоит отметить, что здесь мы используем функцию let*: ведь вначале вычисляется значение height с помощью (apply 'max numbers-list) и после этого получившееся значение используется для вычисления окончательного результата. See section The let* expression, за дополнительной информацией о let*.)


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.2.2 Создание элементов оси Y

Когда мы печатаем вертикальные оси, мы вставляем метки, такие как `5 -' и `10 - ' каждые пять строк. Кроме того хотелось бы, чтобы вертикальные метки находились на одной линии, так что более короткие номера придется выравнивать ведущими пробелами. Если некоторая метка это число, состоящее из двух цифр, тогда метка с числом, состоящим из одной цифры должна быть выровнена с помощью дополнительного пробела, расположенного перед числом.

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

 
(length (int-to-string 35))
     => 2

(length (int-to-string 100))
     => 3

Кроме этого в каждой метке за числом следует строка ` - ', которую мы будем называть маркером и назовем ее Y-axis-tic. Давайте зададим эту переменную с помощью defvar:

 
(defvar Y-axis-tic " - "
   "Строка следующая за числом по оси Y.")

Длина метки по оси Y это сумма длин Y-axis-tic и длины наибольшего числа отложенного по оси Y.

 
(length (concat (int-to-string height) Y-axis-tic))

Это значение будет вычисляться в списке локальных переменных функции print-graph и присваиваться переменной full-Y-label-width. (Стоит отметить, что вначале мы и не думали что появится эта переменная.)

Для того чтобы полностью напечатать метку по вертикальной оси, строку Y-axis-tic надо обьединить с номером; а перед ними может потребоваться вставить один или два пробела, в зависимости от того, какой длины текущее число. Таким образом координатная метка будет состоять из трех частей: (необязательные) ведущие пробелы, число и строка Y-axis-tic (в нашем случае это символ тире). В функцию, создающую координатную метку по оси Y передается число для текущей строки и значение ширины верхней метки, (ведь она получится самой широкой), которое вычисляется при создании локальных переменных в функции print-graph.

 
(defun Y-axis-element (number full-Y-label-width) 
  "Создает N-тую координатную метку по оси Y.
Координатная метка выглядит следующим образом `  5 - ',
для того, чтобы все метки были на одной линии
некоторые необходимо дополнить пробелами."
  (let* ((leading-spaces 
         (- full-Y-label-width
            (length
             (concat (int-to-string number)
                     Y-axis-tic)))))
    (concat
     (make-string leading-spaces ? )
     (int-to-string number)
     Y-axis-tic)))

В функции Y-axis-element обьединяются вместе пробелы, которые могут и отсутствовать; число, предварительно превращенное в строку; и строка Y-axis-tic в нашем случае это знак тире.

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

Пробелы вставляются с помощью функции make-string. Этой функции требуются два аргумента: первый задает, какой длины должна быть строка, а второй--- это символ, который нужно вставить, заданный в особом формате. В нашем случае формат это знак вопроса за которым следует пробел, например `? '. See section `Character Type' in The GNU Emacs Lisp Reference Manual, за подробным описанием синтаксиса для символов.

Функция int-to-string используется в выражении concat при создании результирующей строки, для того, чтобы конвертировать число в строку, которая будет слита с пробелами и знаком тире.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.2.3 Создание колонки оси Y

Теперь мы полностью готовы к созданию функции, которая выдаст нам список пронумерованных и пустых меток, который будет представлять собой вертикальную координатную ось:

 
(defun Y-axis-column (height width-of-label)
  "Создать список пронумерованых и пустых меток для оси of Y.
Высота оси равна HEIGHT, ширина метки равна WIDTH-OF-LABEL."
  (let (Y-axis)
    (while (> height 1)
      (if (zerop (% height Y-axis-label-spacing))
          ;; Вставляем числовую метку.
          (setq Y-axis
                (cons
                 (Y-axis-element height width-of-label)
                 Y-axis))
        ;; Иначе, вставляем пробелы.
        (setq Y-axis
              (cons
               (make-string width-of-label ? )
               Y-axis)))
      (setq height (1- height)))
    ;; Вставляем базовую метку для 1.
    (setq Y-axis
          (cons (Y-axis-element 1 width-of-label) Y-axis))
    (nreverse Y-axis)))

В этой функции в конце каждой итерации цикла while из начального значения height последовательно вычитается единица. Перед каждым вычитанием проверяется, является ли значение height кратным Y-axis-label-spacing, если да, то с помощью функции Y-axis-element создается нумерованная метка; если же текущее значение height не кратно Y-axis-label-spacing, то с помощью функции make-string создается пустая метка.

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


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.2.4 Окончательная версия print-Y-axis

Список созданный Y-axis-column передается в функцию print-Y-axis, которая и напечатает его с помощью insert-rectangle.

 
(defun print-Y-axis (height full-Y-label-width )
  "Вставляет ось Y заданой высоты HEIGHT, ширина метки FULL-Y-LABEL-WIDTH.
HEIGHT должна быть максимальной высотой графика.  FULL-Y-LABEL-WIDTH
ширина самой верхней метки."
;; Значения height  full-Y-label-width 
;; вычисляются в функции `print-graph'.
  (let ((start (point)))
    (insert-rectangle
     (Y-axis-column height full-Y-label-width))
    ;; Переместить точку назад, где будет вставлен график.
    (goto-char start)      
    ;; Переместить точку вперед на максимальную ширину метки
    (forward-char full-Y-label-width)))

Функция print-Y-axis использует insert-rectangle для вставки оси Y, которая создана с помощью функции Y-axis-column. Кроме этого, она устанавливает точку в ту позицию откуда мы начнем печатать график.

Давайте проверим работу print-Y-axis:

  1. Установите

     
    Y-axis-label-spacing
    Y-axis-tic
    Y-axis-element
    print-Y-axis
    

  2. Скопируйте следующее выражение:

     
    (print-Y-axis 12 5)
    

  3. Переключитесь в буфер `*scratch*' и поместите курсор в точку, где вы хотите вставить вертикальную ось.

  4. Наберите M-: (eval-expression). Вставьте в минибуфер выражение (print-Y-axis 12 5)

  5. Нажмите RET для вычисления выражения.

В буфере `*scratch*' будет напечатана следующая ось Y:

 
10 - 
     
     
     
     
 5 - 
     
     
     
 1 - 

Emacs напечатал метки вертикально, верхняя метка равна `10 -'. (Функция print-graph передаст более правильное значение, как вершину графика height-of-top-line, которое в этом случае должно быть равно 15.)


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.3 Функция print-X-axis

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

 
    |   |    |    |
    1   5   10   15

Первый маркер под первым столбцом графика, перед ним несколько пробелов, это резервирует пространство для оси Y. Второй, третий, четвертый и последующие маркеры все расположены на одинаковом расстоянии друг от друга, согласно значению X-axis-label-spacing.

Вторая строка оси X состоит из номеров, которые также разделены согласно значению переменной X-axis-label-spacing.

Значение переменной X-axis-label-spacing надо задавать кратным значению переменной symbol-width, ведь вполне возможно что вы захотите изменить ширину символа, который мы используем для создания тела графика и тогда ось X автоматически воспримет это изменение.

Функция print-X-axis будет очень похожа на функцию print-Y-axis, разве что она будет печатать две строки: строку маркеров и строку номеров. Мы создадим две разные функции для печати каждой строки и затем обьединим их в общей функции print-X-axis.

Получается трехшаговый процесс:

  1. Создать функцию для печати маркеров по оси X print-X-axis-tic-line.

  2. Создать функцию для печати номеров по оси X print-X-axis-numbered-line.

  3. Создать основную функцию для печати двух строк, функцию print-X-axis, которая будет использовать и print-X-axis-tic-line и print-X-axis-numbered-line

C.3.1 Маркеры оси X  Создаем метки для горизонтальной оси.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.3.1 Маркеры оси X

Первая функция должна напечатать маркеры для оси X. Нам надо будет задать сами маркеры и разделяющее их пространство.
 
(defvar X-axis-label-spacing 
  (if (boundp 'graph-blank)
      (* 5 (length graph-blank)) 5)
  "Количество символьных единиц от одной метки оси X до другой.")

(Напомним, что значение graph-blank инициализируется тоже с помощью выражения defvar. Предикат boundp проверяет инициализировани ли его аргумент; если нет то boundp возвращает nil. Если бы символ graph-blank был бы еще неинициализирован и мы бы не использовали условную конструкцию if, тогда бы мы получили следующее сообщение об ошибке `Symbol's value as variable is void'.)

 
(defvar X-axis-tic-symbol "|"
  "Строка для обозначения маркера по оси X.")

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

 
       |   |    |    |

Первый маркер выровнен так, чтобы находиться под первой колонкой графика и освобождать место для оси Y.

Один полный элемент маркера состоит из пробелов, которые продолжаются от одной метки до другой, плюс символ маркера. Необходимое число пробелов можно определить зная ширину символа маркера и X-axis-label-spacing.

Соответствующий фрагмент кода выглядит следующим образом:

 
;;; X-axis-tic-element
...
(concat  
 (make-string 
  ;; Создаем строку пробелов.
  (-  (* symbol-width X-axis-label-spacing)
      (length X-axis-tic-symbol))
  ? )
 ;; Соединяем пробелы с символом маркера.
 X-axis-tic-symbol)
...

Теперь давайте определим сколько пробелов понадобится для того, чтобы правильно выровнять первую метку с первым столбцом графика. Здесь мы будем использовать значение full-Y-label-width вычисляемое в функции print-graph.

Соответствующий код выглядит следующим образом:

 
;; X-axis-leading-spaces
...
(make-string full-Y-label-width ? )
...

Также нам надо определить длину горизонтальной оси, которая равна длине списка номеров и количеству меток на горизонтальной оси:

 
;; X-length 
...
(length numbers-list)              

;; tic-width
...
(* symbol-width X-axis-label-spacing)

;; number-of-X-tics 
(if (zerop (% (X-length tic-width)))
    (/ (X-length tic-width))
  (1+ (/ (X-length tic-width))))

Теперь все готово для создания функции, которая напечатает строку координатных маркеров по оси X:

 
(defun print-X-axis-tic-line
  (number-of-X-tics X-axis-leading-spaces X-axis-tic-element)
  "Печатает маркеры для ось X." 
    (insert X-axis-leading-spaces)
    (insert X-axis-tic-symbol)  ; Под первой колонкой.
    ;; Вставка второго маркера в правильное место.
    (insert (concat  
             (make-string 
              (-  (* symbol-width X-axis-label-spacing)
                  ;; Вставка пробелов до второго маркера.
                  (* 2 (length X-axis-tic-symbol)))
              ? )
             X-axis-tic-symbol))
    ;; Вставка остальных маркеров.
    (while (> number-of-X-tics 1)
      (insert X-axis-tic-element)
      (setq number-of-X-tics (1- number-of-X-tics))))

Вставка строки номеров очень похожа:

Вначале мы создаем пронумерованный элемент с необходимым числом пробелов перед каждым номером:

 
(defun X-axis-element (number)
  "Создаем пронумерованный элемент для оси X."
  (let ((leading-spaces 
         (-  (* symbol-width X-axis-label-spacing)
             (length (int-to-string number)))))
    (concat (make-string leading-spaces ? )
            (int-to-string number))))

Теперь можно написать функцию, которая напечатает строку номеров для координатной оси, при этом под первым столбцом графика мы напечатаем "1":

 
(defun print-X-axis-numbered-line
  (number-of-X-tics X-axis-leading-spaces)
  "Печатает строку номеров по оси  X"
  (let ((number X-axis-label-spacing))
    (insert X-axis-leading-spaces)
    (insert "1")
    (insert (concat  
             (make-string 
              ;; Вставка пробелов до следующего числа.
              (-  (* symbol-width X-axis-label-spacing) 2)
              ? )
             (int-to-string number)))
    ;; Вставка оставшихся чисел.
    (setq number (+ number X-axis-label-spacing))
    (while (> number-of-X-tics 1)
      (insert (X-axis-element number))
      (setq number (+ number X-axis-label-spacing))
      (setq number-of-X-tics (1- number-of-X-tics)))))

Наконец нам надо создать функцию print-X-axis, в которой мы будем использовать и print-X-axis-tic-line и print-X-axis-numbered-line.

Эта функция должна определить локальные значения переменных, используемые и print-X-axis-tic-line и print-X-axis-numbered-line, и вызвать эти две функции. Также она должна напечатать символ перевода строки, которая должна разделять строку маркеров и строку номеров.

Функция состоит из списка переменных, который описывает пять локальных переменных, и вызовов каждой из двух печатающих различные строки функций:

 
(defun print-X-axis (numbers-list)
  "Печатать метки для оси X длины NUMBERS-LIST."
  (let* ((leading-spaces 
          (make-string full-Y-label-width ? ))
       ;; ширина символа находится  graph-body-print
       (tic-width (* symbol-width X-axis-label-spacing))
       (X-length (length numbers-list))
       (X-tic
        (concat  
         (make-string 
          ;; Создаем строку пробелов.
          (-  (* symbol-width X-axis-label-spacing)
              (length X-axis-tic-symbol))
          ? )
         ;; Обьединяем пробелы с символом маркера.
         X-axis-tic-symbol))
       (tic-number
        (if (zerop (% X-length tic-width))
            (/ X-length tic-width)
          (1+ (/ X-length tic-width)))))
    (print-X-axis-tic-line tic-number leading-spaces X-tic)
    (insert "\n")
    (print-X-axis-numbered-line tic-number leading-spaces)))

Давайте проверим print-X-axis:

  1. Установите X-axis-tic-symbol, X-axis-label-spacing, print-X-axis-tic-line, так же как X-axis-element, print-X-axis-numbered-line, и print-X-axis.

  2. Скопируйте следующее выражение:

     
    (progn
     (let ((full-Y-label-width 5)
           (symbol-width 1))
       (print-X-axis
        '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16))))
    

  3. Переключитесь в буфер `*scratch*' и расположите курсор в то место, откуда вы будет печатать горизонтальную ось.

  4. Наберите M-: (eval-expression).

  5. Скопируйте проверочное выражение в минибуфер с помощью C-y (yank).

  6. Нажмите RET для того чтобы вычислит это выражение.

Emacs должен напечатать следующую горизонтальную ось:

 
     |   |    |    |    |
     1   5   10   15   20


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.4 Печать полного графика

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

Функция для печати графика с координатными осями будет в целом соответствовать шаблону, который мы уже создали ранее, (see section Пример графика с пометками) но с небольшими добавлениями.

Вот и шаблон:

 
(defun print-graph (numbers-list)
  "документация..."
  (let ((height  ...
        ...))
    (print-Y-axis height ... )
    (graph-body-print numbers-list)
    (print-X-axis ... )))

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

Эта новая возможность требует незначительной модификации функции Y-axis-column, надо добавить необязательный параметр vertical-step. Функция выглядит следующим образом:

 
;;; Последняя версия.
(defun Y-axis-column
  (height width-of-label &optional vertical-step)
  "Создает список меток для оси Y.
HEIGHT это максимальная высота графика.  
WIDTH-OF-LABEL это максимальная высота метки.
VERTICAL-STEP это положительное целое, 
которое задает масштаб по оси Y.  Например,
шаг равный 5 означает, что каждая строка содержит
пять единиц на графике."
  (let (Y-axis
        (number-per-line (or vertical-step 1)))
    (while (> height 1)
      (if (zerop (% height Y-axis-label-spacing))
          ;; Вставить метку.
          (setq Y-axis
                (cons
                 (Y-axis-element
                  (* height number-per-line)
                  width-of-label)
                 Y-axis))
        ;; Иначе, вставить пробелы.
        (setq Y-axis
              (cons
               (make-string width-of-label ? )
               Y-axis)))
      (setq height (1- height)))
    ;; Вставить основную линию.
    (setq Y-axis (cons (Y-axis-element 
                        (or vertical-step 1)
                        width-of-label)
                       Y-axis))
    (nreverse Y-axis)))

Значения для максимальной высоты графика и ширины символа вычисляются функцией print-graph в выражении let; поэтому функцию graph-body-print надо немного изменить.

 
;;; Последняя версия.
(defun graph-body-print (numbers-list height symbol-width)
  "Печатает столбцовый график для NUMBERS-LIST.
numbers-list состоит из значений по оси Y.
HEIGHT это максимальная высота графика.
SYMBOL-WIDTH это номер каждой колонки."
  (let (from-position)
    (while numbers-list
      (setq from-position (point))
      (insert-rectangle
       (column-of-graph height (car numbers-list)))
      (goto-char from-position)
      (forward-char symbol-width)
      ;; Рисует график столбец за столбцом.
      (sit-for 0)               
      (setq numbers-list (cdr numbers-list)))
    ;; Переместить точку для печати оси X.
    (forward-line height)
    (insert "\n")))

Наконец код функции print-graph:

 
;;; Последняя версия.
(defun print-graph
  (numbers-list &optional vertical-step)
  "Печатает график с координатными осями согласно NUMBERS-LIST.
NUMBERS-LIST состоит из значений по оси Y.

Необязательный параметр VERTICAL-STEP это положительное число
задает масштаб по оси Y. Например, шаг равный 5 означает,
что каждая строка содержит пять единиц на графике."
  (let* ((symbol-width (length graph-blank))
         ;; height это наибольшее число.
         (height (apply 'max numbers-list))
         (height-of-top-line
          (if (zerop (% height Y-axis-label-spacing))
              height
            ;; else
            (* (1+ (/ height Y-axis-label-spacing))
               Y-axis-label-spacing)))
         (vertical-step (or vertical-step 1))
         (full-Y-label-width
          (length
           (concat
            (int-to-string
             (* height-of-top-line vertical-step))
            Y-axis-tic))))

    (print-Y-axis
     height-of-top-line full-Y-label-width vertical-step)
    (graph-body-print
     numbers-list height-of-top-line symbol-width)
    (print-X-axis numbers-list)))

C.4.1 Проверка функции print-graph  Быстрая проверка.
C.4.2 График количества слов в функциях  
C.4.3 Окончательная печать графика  


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.4.1 Проверка функции print-graph

Мы можем вначале проверить функцию print-graph с помощью короткого списка чисел

  1. Установите последнии версии Y-axis-column, graph-body-print и print-graph (а также весь остальной код.)

  2. Скопируйте следующее выражение:

     
    (print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1))
    

  3. Переключитесь в буфер `*scratch*' и расположите курсор в то место, где вы хотите напечать график.

  4. Наберите M-: (eval-expression).

  5. Скопируйте проверочное выражение в минибуфер с помощью C-y (yank).

  6. Нажмите RET для того чтобы вычислит это выражение.

Emacs напечатает следующий график:

 
10 -              
                  
                  
         *        
        **   *    
 5 -   ****  *    
       **** ***   
     * *********  
     ************ 
 1 - *************

     |   |    |    |
     1   5   10   15

С другой стороны, если вы передадите в print-graph значение 2 для аргумента vertical-step, вычислив следующее выражение:

 
(print-graph '(3 2 5 6 7 5 3 4 6 4 3 2 1) 2)

График будет выглядеть следующим образом:

 
20 -              
                  
                  
         *        
        **   *    
10 -   ****  *    
       **** ***   
     * *********  
     ************ 
 2 - *************

     |   |    |    |
     1   5   10   15


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.4.2 График количества слов в функциях

Теперь мы сможем наконец полностью напечатать график, который покажет сколько определений функций содержит менее 10 слов и символов, сколько между 10 и 19, сколько между 20 и 29 и так далее.

Это многошаговый процесс. Сначала убедитесь, что вы загрузили весь требуемый код.

Неплохая мысль заново установить top-of-ranges в случае если вы уже установили ее в другое значение. Вы можете вычислить следующее:

 
(setq top-of-ranges
 '(10  20  30  40  50
   60  70  80  90 100
  110 120 130 140 150
  160 170 180 190 200
  210 220 230 240 250
  260 270 280 290 300)

Затем надо создать список номеров слов и символов в каждом диапазоне.

Вычислите следующее:

 
(setq list-for-graph
       (defuns-per-range
         (sort
          (recursive-lengths-list-many-files 
           (directory-files "/usr/local/emacs/lisp"
                            t ".+el$"))
          '<)
         top-of-ranges))

На моей машине это заняло около одного часа. Необходимо было просмотреть все 303 файла в моей версии Emacs. После всех вычислений переменная list-for-graph имела следующее значение:

 
(537 1027 955 785 594 483 349 292 224 199 166 120 116 99 
90 80 67 48 52 45 41 33 28 26 25 20 12 28 11 13 220)

Все это значило, что в моей версии Emacs было 537 определений функции, в которых было менее 10 слов, 1027 определений функции, в которых от 10 до 19 слов, 955 определений функции, в которых 20 от 29 слов, и так далее.

Ясно, что одного взгляда на этот список достаточно, чтобы понять что наибольшее число функции содержат от десяти до тридцати слов.

Теперь о печати. Мы не хотим напечатать график в котором около тысячи строк .... Вместо этого нам бы хотелось напечатать график в котором менее 25 строк. Это число строк можно отобразить почти на любом мониторе.

Это означает, что каждое значение в списке list-for-graph надо уменьшить в пятьдесят раз.

Ниже коротенькая функция, которая сможет сделать это правда с помощью двух еще незнакомых нам функций, mapcar и lambda.

 
(defun one-fiftieth (full-range)
  "Возвращает список, каждый элемент которого уменьшен в 50 раз."
 (mapcar '(lambda (arg) (/ arg 50)) full-range))

Выражение lambda  Создание безымянной функции.
Функция mapcar  Запуск функции для каждого элемента списка.
Последняя ошибка ... самая коварная  Последняя ошибка обычно самая коварная.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

Выражение lambda

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

Например,

 
(lambda (arg) (/ arg 50))

это определение функции, котороя возвращает результат от деления переданного ее аргумента arg на 50.

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

 
(lambda (number) (* 7 number))

(See section The defun Special Form.)

если нам понадобиться умножить 3 на 7, мы можем написать:

 
(умножить-на-семь 3)
 \_______________/ ^
         |         |
      функция  аргумент

Это выражение вернет 21.

С помощью безымянной функции, надо написать следующее:

 
((lambda (number) (* 7 number)) 3)
 \____________________________/ ^
               |                |
     безымянная функция     аргумент

Если нам надо разделить 100 на 50, тогда мы запишем:

 
((lambda (arg) (/ arg 50)) 100)
 \______________________/  \_/
             |              |
    безымянная функция     аргумент             

Это выражение вернет 2. В функцию передан аргумент 100, который будет разделен на 50.

Дополнительную информацию о безымянных функция можно найти как всегда в справочном руководстве по Emacs Lisp See section `Lambda Expressions' in The GNU Emacs Lisp Reference Manual.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

Функция mapcar

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

Например,

 
(mapcar '1+ '(2 4 6))
     => (3 5 7)

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

Сравните действие функции mapcar с функцией apply, которая применяет свой первый аргумент ко всем оставшимся сразу. (See section Изготовляем график, где описано использование apply.)

В определении one-fiftieth первый аргумент это безымянная функция:

 
(lambda (arg) (/ arg 50))

а второй аргумент это full-range, который будет связан с list-for-graph.

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

 
(mapcar '(lambda (arg) (/ arg 50)) full-range)

See section `Mapping Functions' in The GNU Emacs Lisp Reference Manual, можно найти дополнительную информацию о mapcar.

С помощью функции one-fiftieth мы можем создать список в котором каждый элемент будет в пятьдесят раз меньше чем соответствующий элемент в списке list-for-graph.

 
(setq fiftieth-list-for-graph
      (one-fiftieth list-for-graph))

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

 
(10 20 19 15 11 9 6 5 4 3 3 2 2 
1 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 0 4)

Вот этот список мы почти готовы напечатать!


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

Последняя ошибка ... самая коварная

Я сказал `почти готовы печатать'! Конечно же есть ошибка в функции print-graph .... В ней есть опция vertical-step, но нет опции horizontal-step. Переменная top-of-range принимает значения от 10 до 300 с шагом 10. Но функция print-graph напечатает только первые значения.

Это класический пример ошибки, которые некоторые считают наиболее коварным типом ошибки, ошибка упущения. Эту ошибку вы не сможете найти изучая программу, так как ее нет там; это упущенная возможность. Лучше всего если вы будете чаще переписывать свою программу; и попробуете писать программу, так чтобы ее было легче понимать и изменять. Попытайтесь осознать, очень простой факт, все что вы написали рано или поздно надо будет изменить. Это основной закон практического программирования.

Основному изменению подвергнется функция print-X-axis-numbered-line, после этого надо будет немного подправить функции print-X-axis и print-graph. Не так уж много, но есть одна тонкость числа должны быть под символами маркера. Придется немного подумать.

Ниже скорректированная функция print-X-axis-numbered-line:

 
(defun print-X-axis-numbered-line
  (number-of-X-tics X-axis-leading-spaces
   &optional horizontal-step)
  "Печать строки номеров по оси X"
  (let ((number X-axis-label-spacing)
        (horizontal-step (or horizontal-step 1)))
    (insert X-axis-leading-spaces)
    ;; Удаляем лишние ведущие пробелы.
    (delete-char
     (- (1-
         (length (int-to-string horizontal-step)))))
    (insert (concat  
             (make-string 
              ;; Вставка пробелов.
              (-  (* symbol-width
                     X-axis-label-spacing) 
                  (1-
                   (length
                    (int-to-string horizontal-step)))
                  2)
              ? )
             (int-to-string
              (* number horizontal-step))))
    ;; Вставка оставшися чисел.
    (setq number (+ number X-axis-label-spacing))
    (while (> number-of-X-tics 1)
      (insert (X-axis-element
               (* number horizontal-step)))
      (setq number (+ number X-axis-label-spacing))
      (setq number-of-X-tics (1- number-of-X-tics)))))

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

 
(defun print-X-axis (numbers-list horizontal-step)
  "Печатает ось X согласно длине NUMBERS-LIST.
Необязательный параметр, HORIZONTAL-STEP, это положительное число,
задающее масштаб по оси X."
;; Значение SYMBOL-WIDTH и FULL-Y-LABEL-WIDTH 
;; вычисляются в `print-graph'.
  (let* ((leading-spaces 
          (make-string full-Y-label-width ? ))
       ;; SYMBOL-WIDTH нужен graph-body-print
       (tic-width (* symbol-width X-axis-label-spacing))
       (X-length (length numbers-list))
       (X-tic
        (concat  
         (make-string 
          ;; Создаем строку из пробелов.
          (-  (* symbol-width X-axis-label-spacing)
              (length X-axis-tic-symbol))
          ? )
         ;; Соединяем пробелы с символом маркера.
         X-axis-tic-symbol))
       (tic-number
        (if (zerop (% X-length tic-width))
            (/ X-length tic-width)
          (1+ (/ X-length tic-width)))))

    (print-X-axis-tic-line
     tic-number leading-spaces X-tic)
    (insert "\n")
    (print-X-axis-numbered-line
     tic-number leading-spaces horizontal-step)))

 
(defun print-graph
  (numbers-list &optional vertical-step horizontal-step)
  "Печатает график с координатными осями согласно NUMBERS-LIST.
NUMBERS-LIST состоит из значений по оси Y.

Необязательный параметр, VERTICAL-STEP, это положительное число,
задающее масштаб по оси Y.  Например шаг равный 5 означает, что в
каждой строке содержится пять единиц.

Необязательный параметр, HORIZONTAL-STEP, это положительное число,
задающее масштаб по оси Y."
  (let* ((symbol-width (length graph-blank))
         ;; height это наибольшее число
         ;; and the number with the most digits.
         (height (apply 'max numbers-list))
         (height-of-top-line
          (if (zerop (% height Y-axis-label-spacing))
              height
            ;; else
            (* (1+ (/ height Y-axis-label-spacing))
               Y-axis-label-spacing)))
         (vertical-step (or vertical-step 1))
         (full-Y-label-width
          (length
           (concat
            (int-to-string
             (* height-of-top-line vertical-step))
            Y-axis-tic))))

    (print-Y-axis
     height-of-top-line full-Y-label-width vertical-step)
    (graph-body-print
        numbers-list height-of-top-line symbol-width)
    (print-X-axis numbers-list horizontal-step)))


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

C.4.3 Окончательная печать графика

Теперь когда вы все исправили и заново установили можно наконец вызвать функцию print-graph следующим образом:

 
(print-graph fiftieth-list-for-graph 50 10)

А вот и долгожданный график:

 
1000 -  *                             
        **                            
        **                            
        **                            
        **                            
 750 -  ***                           
        ***                           
        ***                           
        ***                           
        ****                          
 500 - *****                          
       ******                         
       ******                         
       ******                         
       *******                        
 250 - ********                       
       *********                     *
       ***********                   *
       *************                 *
  50 - ***************** *           *
       |   |    |    |    |    |    |    |
      10  50  100  150  200  250  300  350

Из графика видно, что больше всего функций, которые содержат от 10 до 19 слов.


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated on March, 10 2004 using texi2html