Skip to main content

Exercise 6: Second Lookup Class

Click here to return to the exercise description in the main part of the tutorial.

  1. Using VS Code - ObjectScript, change the top line of ObjectScript.Lookup1 to ObjectScript.Lookup2, and save it as ObjectScript.Lookup2.

  2. The CurrentCount() method makes use of the BitMap-ID index. Since it contains a 1 bit for each person, a loop through the chunks and $BitCount is all you need.

    
    Class ObjectScript.Lookup2
    {
    
    /// count the "1" bits from the chunks of the Bitmap-ID index
    ClassMethod CurrentCount()
    {
        set records = 0, chunk = ""
        for {
            // use the 3-argument $order to get the next chunk and the bits stored there
            set chunk = $order(^PersonI("Bitmap-ID", chunk), 1, bits)
            quit:(chunk = "")
            // add the "1" bits to the count
            set records = records + $bitcount(bits, 1)
        }
        write !, "There are ", records, " records in the database."
    }
    }
  3. Modify Main() so that it calls CurrentCount(). The new ElseIf for phone lookups can go anywhere; the example below places it above the ElseIf for date of birth lookups.

    
    Class ObjectScript.Lookup2
    {
    
    /// main loop section, dispatch to different methods based on user input
    ClassMethod Main()     
    {
        do ..CurrentCount()
        while ..GetInput(.type, .search) {  
            if (type = "help") {do ..Help()}
            elseif (type = "phone") {do ..Phone(search)}
            elseif (type = "dob") {do ..DOB(search)}
        }
    }
    }
  4. Modify GetInput() so that it also handles phone numbers. Since ValidDOB() writes an error message, date of birth lookups should be handled last. That's why the new ElseIf for phone lookups is placed above the ElseIf for date of birth lookups. The Else writes the final part of an error message (the comment explains the hack).

    
    Class ObjectScript.Lookup2
    {
    
    /// prompt user for a lookup string, return search type and search string
    ClassMethod GetInput(output type as %String, output search as %String) as %Boolean
    {
        read !, "Lookup: ", lookup
        return:(lookup = "") 0  // user entered nothing so return FALSE
        if (lookup = "?") {
            set type = "help", search = ""
        }
        // the RegEx accepts ###- or ###-###-#### only
        elseif $match(lookup, "\d{3}-(\d{3}-\d{4})?") {
            set type = "phone", search = lookup        
        }
        elseif (##class(ObjectScript.DataEntry4).ValidDOB(lookup, .convdate)) {
            set type = "dob", search = convdate
        }
        else {
            // this is a hack for invalid input
            // ValidDOB() writes an error message, and the text below gets added to that
            write ", name, or phone"
            set (type, search) = ""
        }
        return 1
    }
    }
  5. The new Phone() method handles exact matches first, and then area code matches.

    
    Class ObjectScript.Lookup2
    {
    
    /// lookup phone or area code
    ClassMethod Phone(phone as %String)
    {
        set count = 0
        // handle exact match first
        set id = $get(^PersonI("Phone", phone))
        if (id '= "") {
            set count = 1
            write !, "1) "
            do ..DisplayLine(id)
            quit
        }
        // handle area code matches next
        elseif (phone?3n1"-") {
            // use 3-argument $order to get first matching phone number and its ID number
            set ph = $order(^PersonI("Phone", phone), 1, id)
            // loop through matching phones, and number them
            while ($extract(ph, 1, $length(phone)) = phone) {
                write:(count = 0) "...finding area code matches"
                set count = count + 1
                write !, count, ") "
                do ..DisplayLine(id)
                // use 3-arg $order to get the next phone number and its ID number
                set ph = $order(^PersonI("Phone", ph), 1, id)
            }
        }
        if (count = 0) {write "...no matches"}
    }
    }
  6. Modify Help().

    
    Class ObjectScript.Lookup2
    {
    
    /// display lookup options
    ClassMethod Help()
    {
        write !, "You can enter:",
              !?10, "* date of birth",
              !?10, "* full phone number or area code only ""617-""", !
    }
    }
  7. Click the Save and Compile buttons.

  8. Start the Terminal, and run your method to test the new Phone lookups, by typing do ##class(ObjectScript.Lookup2).Main().

  9. Modify Main() again, adding another ElseIf for name lookups.

    
        do ..CurrentCount()
        while ..GetInput(.type, .search) {  
            if (type = "help") {do ..Help()}
            elseif (type = "phone") {do ..Phone(search)}
            elseif (type = "name") {do ..Name(search)}
            elseif (type = "dob") {do ..DOB(search)}
        }
    
  10. Modify GetInput() again to handle names. Again, the new ElseIf is placed above the ElseIf for date of birth lookups.

    
        if (lookup = "?") {
            set type = "help", search = ""
        }
        // the RegEx accepts ###- or ###-###-#### only
        elseif $match(lookup, "\d{3}-(\d{3}-\d{4})?") {
            set type = "phone", search = lookup        
        }
        /* the $zconvert converts the last name and first name entered to Last,First format
           the pattern match accepts Lastname only, or Lastname,Firstname */
        elseif ($zconvert(lookup, "W")?1U.L.1(1","1U.L)) {
            set type = "name", search = $zconvert(lookup, "W")
        }
        elseif (##class(ObjectScript.DataEntry4).ValidDOB(lookup, .convdate)) {
            set type = "dob", search = convdate
        }
        else {
            // this is a hack for invalid input
            // ValidDOB() writes an error message, and the text below gets added to that
            write ", name, or phone"
            set (type, search) = ""
        }
    
  11. The new Name() method is the longest method so far. It uses a focused $Order loop for last names. For each matching last name, a $Order loop either goes through all first names for that last name (if only last name was entered by the user), or it's a focused loop for the first names that match. For each last name and first name, a final $Order loop goes through all the ID numbers.

    
    Class ObjectScript.Lookup2
    {
    
    /// lookup names in these forms: Smith; Smith,John; Smith,J; Sm,John; Sm,J
    ClassMethod Name(name as %String)
    {
        set count = 0
        set last = $piece(name, ",", 1), first = $piece(name, ",", 2)
        // last may be an exact match, so find preceding last name
        set ln = $order(^PersonI("Name", last), -1)
        // loop through last names
        for {
            set ln = $order(^PersonI("Name", ln))
            // quit as soon as last name doesn't match original
            quit:($extract(ln, 1, $length(last)) '= last)
            // first may be "". Otherwise, it may be an exact match, so find preceding first name
            if (first = "") {set fn = ""}
            else { set fn = $order(^PersonI("Name", ln, first), -1)}
            // loop through first names
            for {
                set fn = $order(^PersonI("Name", ln, fn))
                // quit as soon as first name doesn't match original, or is ""
                quit:(($extract(fn, 1, $length(first)) '= first) || (fn = ""))
                set id = ""
                // loop through all IDs
                for {
                    set id = $order(^PersonI("Name", ln, fn, id))
                    quit:(id = "")
                    write:(count = 0) "...finding name matches"
                    set count = count + 1
                    write !, count, ") "
                    do ..DisplayLine(id)
                }
            }
        }
        if (count = 0) {write "...no matches"}
     }
    }
  12. Modify Help() again.

    
    Class ObjectScript.Lookup2
    {
    
    /// display lookup options
    ClassMethod Help()
    {
        write !, "You can enter:",
              !?10, "* date of birth",
              !?10, "* full phone number or area code only ""617-""",
              !?10, "* full name: Smith,John",
              !?10, "* last name: Smith",
              !?10, "* partial name: Sm,J or Smith,J or Sm,John", !
    }
    }
  13. Click the Save and Compile buttons.

  14. Start the Terminal, and run your method to test the new Name lookups, by typing do ##class(ObjectScript.Lookup2).Main().

  15. You'll use a matches array to keep track of the matches so that the user can select one for display. Remember that you don't have to declare anything about this array; you just use it. You need to add four lines to your code. In DOB(), add a line between Quit and Write.

    
        quit:(id = "")
        set matches(count) = id  // keep track of matches
        write !, count, ") "
    

    In Phone(), add a line in the "exact match" If, between Set and Write.

    
        set count = 1
        set matches(1) = id  // keep track of exact match
        write !, "1) "
    

    In Phone(), add a line in the "area code matches" ElseIf, between Set and Write. And in Name(), add the same line in the innermost nested For loop (ID numbers), between Set and Write.

    
        set count = count + 1
        set matches(count) = id  // keep track of matches
        write !, count, ") "
    
  16. Now that your code is building the matches array, you need to write a Select() method, that takes the array as an argument, and returns the ID number of the user's selection. Notice that the code doesn't check that the user enters an integer, nor does it check if the integer is in the range of 1 to count. It simply uses $Get to check if the user's choice is an existing subscript in the matches array, and returns the corresponding ID number if it is, or the empty string if not.

    
    Class ObjectScript.Lookup2
    {
    
    /// user makes a choice from the matches array, return the corresponding ID or ""
    ClassMethod Select(ByRef matches as %Integer, Output id as %Integer)
    {
        set id = ""
        for {
            read !!, "Choose by number: ", choice
            quit:(choice = "")
            set id = $get(matches(choice))
            quit:(id '= "")  // stop looping if user makes a valid choice
            write "...Invalid choice"
            }
    }
    }
  17. Now you have to add a call to Select() from DOB(), Phone(), and Name(). In DOB(), add this at the end of the method.

    
        do ..Select(.matches, .id)
    

    In Phone() and Name(), update the "no matches" If at the end of the method to look like this.

    
        if (count = 0) {write "...no matches"}
        else {do ..Select(.matches, .id)}
    

    Modify the signatures of DOB(), Phone(), and Name() to return the chosen ID number.

    
    Class ObjectScript.Lookup2
    { 
    ClassMethod DOB(intdob as %Date, output id as %Integer)
    { }
    ClassMethod Phone(phone as %String, output id as %Integer)
    { }
    ClassMethod Name(name as %String, output id as %Integer)
    { }
    }

    To ensure that DOB(), Phone(), and Name() always return the chosen ID number, initialize id at the top of each method.

    
        set id = ""
    

    Modify Main() again, passing id by reference to DOB(), Phone(), and Name().

    
        do ..CurrentCount()
        while ..GetInput(.type, .search) {  
            if (type = "help") {do ..Help()}
            elseif (type = "phone") {do ..Phone(search, .id)}
            elseif (type = "name") {do ..Name(search, .id)}
            elseif (type = "dob") {do ..DOB(search, .id)}
        }
    
  18. TakeAction() is very simple for now, just retrieving and displaying a record.

    
    Class ObjectScript.Lookup2
    {
    
    /// display chosen record
    ClassMethod TakeAction(id as %Integer)
    {
        set record = ^PersonD(id) 
        do ##class(ObjectScript.DataEntry4).Display(record)
    }
    }

    Modify Main() to call TakeAction() if the input is good. Also, since Help() doesn't return id like the other three methods, add set id = "" to the If.

    
        do ..CurrentCount()
        while ..GetInput(.type, .search) {  
            if (type = "help") {do ..Help() set id = ""}
            elseif (type = "phone") {do ..Phone(search, .id)}
            elseif (type = "name") {do ..Name(search, .id)}
            elseif (type = "dob") {do ..DOB(search, .id)}
            if ((type '= "") && (id '= "")) {do ..TakeAction(id)}
        }
    
  19. Click the Save and Compile buttons.

  20. Start the Terminal, and run your method to test the final changes, by typing do ##class(ObjectScript.Lookup2).Main().

FeedbackOpens in a new tab