MovieMan – wrapping up

Now that we've covered all of the language and standard library features we're going to cover, it's time to add the finishing touches to MovieMan. There are two modules that need to be modified: movieman.db and movieman.menu.display.

The db module

Open up $MOVIEMAN/source/movieman/db.d. We'll start with fleshing out DBTable. At the top of its declaration, add the highlighted line as follows:

T[] _items;
bool _sortRequired;

In order for the movies to display in a sensible order, they'll need to be sorted. We could perform the sort every time a movie is added or every time the movies are displayed. The problem with this is that sorting is going to become an expensive operation as more and more movies are added. There are different solutions to make it more efficient, but to keep things simple, we're only going to sort when it's actually needed. This is tracked by _sortRequired. It should be set each time a new movie is added to the database, so the opOpAssign member function needs to be updated.

void opOpAssign(string op : "~")(auto ref T t) {
  _items ~= t;
  _sortRequired = true;
}

Previously, the getMovies overloads were all implemented simply to return the _items member of DBTable. It's time to implement the means to change that by giving DBTable the ability to produce a range. The range will be a simple wrapper of a slice, much like the ArrayRange we implemented in Chapter 6, Understanding Ranges. We'll use opIndex to get the range, allowing calling code to use the slice operator with no indexes. Before the range is returned, the function will check if sorting is required and, if so, call std.algorithm.sort on the _items array. Here's the function in its entirety:

auto opIndex() {
  import std.algorithm : sort;
  struct Range {
    T[] items;
    bool empty() {
      return items.length == 0;
    }
    ref T front() {
      return items[0];
    }
    void popFront() {
      items = items[1 .. $];
    }
    size_t length() {
      return items.length;
    }
  }
  if(_sortRequired) {
    _items.sort();
    _sortRequired = false;
  }
  return Range(_items);
}

In order for this to compile, the Movie type needs an opCmp implementation. We could, instead, use a custom predicate for the sort, but what we need is a bit more complex than would be feasible in that case. Movies should be displayed by case number, then by page number, and finally, for movies on the same page, in alphabetical order. So scroll up to the top of the file and add the following to the Movie declaration.

int opCmp(ref const(Movie) rhs) {
  import std.algorithm : cmp;
  if(this == rhs)
    return 0;
  else if(caseNumber == rhs.caseNumber && pageNumber == rhs.pageNumber)
    return title.cmp(rhs.title);
  else if(caseNumber == rhs.caseNumber)
    return pageNumber - rhs.pageNumber;
  else
    return caseNumber - rhs.caseNumber;
}

If all fields of both instances are equal, there's nothing to do but return 0. If the case and page numbers are the same, there's nothing to do but to sort by title (same case, same page). This can be done with std.algorithm.cmp and its result returned directly. Otherwise, if only the case numbers are the same, there's nothing left but to sort by page number. This is done with a subtraction: if this.pageNumber is higher, the return value will be positive; if it's lower, the return value is negative. If the case numbers are not equal, the page numbers don't need to be tested (the movies are in different cases), so a subtraction is performed on the case numbers to sort by case. This implementation of opCmp is compatible with the default opEquals for structs (it will never return 0 when opEquals returns false), so there's no need to implement our own.

Now let's turn back to DBTable, where we need to implement removeItems, which allows us to delete an array of movies from the database. Like sorting, this is also a potentially expensive operation. Using std.algorithm.remove with SwapStrategy.stable (the default) would require looping through the array of movies, calling remove for each one, and paying the price of copying some of the elements around, in the worst case, on each iteration. Another option would be to use SwapStrategy.unstable and set the _sortRequired flag. A third option would be to figure out how we could make use of some of the many tools available in std.algorithm.

void removeItems(T[] ts) {
  import std.algorithm : find, canFind, copy, filter;
  auto dirty = _items.find!(m => ts.canFind(m));
  auto tail = dirty.filter!(m => !ts.canFind(m)).copy(dirty);
  _items = _items[0 .. $ - tail.length];
}

This uses a function we haven't discussed yet, std.algorithm.searching.canFind, which, given a range as a haystack and an optional needle, returns true if the needle exists in the haystack.

If it's not immediately obvious what's happening here, don't worry. This sort of thing takes a lot of getting used to. I encourage you to work through it step-by-step. It helps to copy it out of the project into a separate source file and work with data that's easy to manipulate manually. Something like this:

import std.stdio;
int[] _items;
void removeItems(int[] ts) {
  import std.algorithm : find, canFind, copy, filter;
  auto dirty = _items.find!(m => ts.canFind(m));
  auto tail = dirty.filter!(m => !ts.canFind(m)).copy(dirty);
  _items = _items[0 .. $ - tail.length];
}
void main() {
  _items = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
  removeItems([20, 50, 90, 30]);
  writeln(_items);
}

This allows you to work with the function in isolation, adding writelns where necessary and experimenting as needed until you fully understand it.

That's it for DBTable. Now let's turn our attention to the deleteMovies function. The only thing that needs to be done here is to add the highlighted line.

void deleteMovies(Movie[] movies) {
  _movies.removeItems(movies);
  writeln();
  foreach(ref movie; movies)
    writefln("Movie '%s' deleted from the database.", movie.title);
}

Next are the overloaded versions of getMovies. First up is the overload that returns all movies. We'll change it from returning _movies._items to returning the new input range we added. It can provide the range by slicing.

auto getMovies() {
  return _movies[];
}

The second overload takes a movie title. Because it's possible for more than one movie with the same title to be in the database, we need to use an algorithm that allows us to build a range containing all movies with the given title. That sounds like a job for std.algorithm.filter.

auto getMovies(string title) {
  import std.algorithm : filter;
  return _movies[].filter!(m => m.title == title);
}

Next up is the overload that takes a case number. It also uses filter.

auto getMovies(uint caseNumber) {
  import std.algorithm : filter;
  return _movies[].filter!(m => m.caseNumber == caseNumber);
}

Finally, getMovies by case and page number:

auto getMovies(uint caseNumber, uint pageNumber) {
  import std.algorithm : filter;
  return _movies[].filter!(m => m.caseNumber == caseNumber && m.pageNumber == pageNumber);
}

That's all we're going to implement for the db module in the book. There is one other function skeleton that we added back in Chapter 5, Generic Programming Made Easy, and that is the movieExists function. It's intended be called from the main menu before adding a movie to the database. It should return true if a movie with that title already exists, and false if not. Currently, it always returns false, which is fine since it isn't actually called from anywhere. Here's a challenge for you: implement movieExists using one of the algorithms we've already used here in the db module. Then, update $MOVIEMAN/source/movieman/menu/main.d to call the function and, if a movie title does already exist, ask the user if he really wants to add it to the database.

The display menu

Very little has been implemented here, so we'll be making several additions. Open up $MOVIEMAN/source/movieman/menu/display.d. Add the highlighted import at the top of the module.

import std.stdio,
  std.array;

We need this because we're going to use std.array.Appender to keep track of movies that need to be deleted. We can add an instance of it to the DisplayMenu declaration, immediately following the Options enumeration.

enum Options : uint {
  byTitle = 1u,
  allOnPage,
  allInCase,
  all,
  exit,
}
Appender!(Movie[]) _pendingDelete;

Next, we have three member functions to implement in the private: section of DisplayMenu. The first, displayRange, takes a range of movies returned from the getMovies functions, iterates it, and prints each movie to the screen.

void displayRange(R)(R range) {
  import std.range : walkLength;
  if(range.empty) {
    write("
Sorry, there are no movies in the database that match your query.");
    return;
  }

  auto len = range.walkLength();
  writefln("
Found %s matches.", len);

  foreach(ref movie; range) {
    if(!displayMovie(movie, --len >= 1))
      break;
  }
  writeln("
That was the last movie in the list.");
}

First, note that this is implemented as a template. That's because the range is a Voldemort type, so there's no way to use its type directly in a function parameter list. Using a template lets the compiler deduce the type. The first line is an import of walkLength, which will be used to determine how many movies are available. Next, a check is made to see if the range is empty. If so, a message to that effect is printed and we abort. After that, the number of available movies is obtained and printed, then the range is iterated with a foreach loop, and displayMovie, which we'll implement next, is called for each element. As the second argument to the function, the local copy of the range length is decremented to determine if any movies follow the current one; displayMovie will use this in order to decide if the user should be given the option to display the next movie.

Note that the movie variable in the foreach is declared as ref. This is because the user will be able to edit movies from the display menu. Using a ref variable allows us to take a shortcut and edit the movie data directly through the instance, rather than adding a new function to update the database.

displayMovie prints the movie data, then shows a submenu that allows the user to choose additional actions: edit or delete the current movie, or show the next movie (if any are available).

bool displayMovie(ref Movie movie, bool showNext = false) {
  static choices = [
    "edit this movie",
    "delete this movie",
    "display the next movie"
  ];

  printMovie(movie);
  auto choiceText = showNext ? choices : choices[0 .. $-1];
  auto choice = readChoice(choiceText);

  if(choice == 1) {
    editMovie(movie);
    return displayMovie(movie, showNext);
  }
  else if(choice == 2) {
    _pendingDelete ~= movie;
    writefln("
Movie '%s' scheduled for deletion.", movie.title);
    return showNext;
  }
  else if(choice != 3) return false;
    return true;
}

The choices array is a list of options for the submenu, which is displayed using readChoice. If no movies follow the current one, the third option is not shown. If option one is selected, the editMovie function, coming up next, is called; after that, the updated movie info is displayed. If the second option is selected, the movie is appended to _pendingDelete. Later, when control returns to handleSelection, any movies added to _pendingDelete will be removed from the database. Finally, if option three is selected, the function returns false, causing control to go back to handleSelection; otherwise, the return value is true, indicating that displayRange should continue iterating its range of movies.

The next private function is editMovie. This function simply asks the user to enter the new information, then, after asking for verification, either updates the movie instance with the new info directly or aborts.

void editMovie(ref Movie movie) {
  enum skipIt = "skip it";
  auto title = "";
  auto msg = "No changes committed to the database.";
  uint caseNumber, pageNumber;

  scope(exit) {
    writeln("
", msg);
    writeln("Press 'Enter' to continue.");
    readUint();
  }

  if(readChoice("to edit the movie's title", skipIt)) {
    title = readTitle();
    if(!validateTitle(title))
      return;
  }

  if(readChoice("to edit the movie's case number", skipIt)) {
    caseNumber = readNumber("case");
    if(!validateNumber(caseNumber, "case"))
      return;
  }

  if(readChoice("to edit the movie's page number", skipIt)) {
    pageNumber = readNumber("page");
    if(!validateNumber(pageNumber, "page"))
      return;
  }

  if(title != "") movie.title = title;
    if(caseNumber > 0) movie.caseNumber = caseNumber;
      if(pageNumber > 0) movie.pageNumber = pageNumber;
        msg = "Database updated.";
}

A few points of note. Before asking for each item, readChoice is called with the option to skip entering that item. When an item is entered, it is checked for validity against one of the two validation functions in the base Menu class. At the end of the function, only the fields for which data has been entered are modified.

Finally, handleSelection needs to be updated to call the new functions. The highlighted lines show the changes.

override void handleSelection(uint selection) {
  final switch(cast(Options)selection) with(Options) {
    case byTitle:
      auto movies = readTitle.getMovies();
      displayRange(movies);
      break;
    case allOnPage:
      auto caseNumber = readNumber("case");
      auto pageNumber = readNumber("page");
      auto movies = getMovies(caseNumber, pageNumber);
      displayRange(movies);
      break;
    case allInCase:
      auto movies = readNumber("case").getMovies();
      displayRange(movies);
      break;
    case all:
      auto movies = getMovies();
      displayRange(movies);
      break;
    case exit:
      exitMenu();
      break;
  }

  if(_pendingDelete.data.length > 0) {
    deleteMovies(_pendingDelete.data);
    _pendingDelete.clear();
  }
}

The block at the end will remove any movies that are pending deletion.

Making it better

There are a number of ways to improve upon MovieMan in its current form. Perhaps the most important is adding the ability to write the database to file. The menu handling code introduced early in the book could be rewritten to use features that came later, like templates. It could perhaps even be converted to a range-based implementation to be more idiomatic. New features could be added, like a database of movie directors, actors and actresses. Support could be added to accommodate music CDs in addition to DVDs. I intend to do that with my own copy of the program, as I have hundreds of music CDs in cases as well.

MovieMan is intended to be your playground. Experiment with it, play with it, and see what you can do with it. Use it to practice your D skills. Anything goes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.144.65