14.3 Conversion Function ExamplesThis section shows how you can use the conversion functions we've described in actual PL/SQL examples. 14.3.1 FM: Suppressing Blanks and ZerosPL/SQL offers the FM element as a modifier to a format mask. FM (fill mode) controls the suppression of padded blanks and leading zeros in values returned by the TO_CHAR function. By default, the following format mask results in both padded blanks and leading zeros (there are five spaces between the month name and the day number): TO_CHAR (SYSDATE, 'Month DD, YYYY') ==> 'April 05, 1994' With the FM modifier at the beginning of the format mask, however, both the extra blank and the leading zeros disappear: TO_CHAR (SYSDATE, 'FMMonth DD, YYYY') ==> April 5, 1994' The modifier can be specified in upper-, lower-, or mixed-case; the effect is the same. The FM modifier is a toggle, and can appear more than once in a format model. Each time it appears in the format, it changes the effect of the modifier. By default (that is, if FM is not specified anywhere in a format mask), blanks are not suppressed and leading zeros are included in the result value. So the first time that FM appears in the format it indicates that blanks and leading zeros are suppressed for any following elements. The second time that FM appears in the format, it indicates that blanks and leading zeros are not suppressed for any following elements, and so on. In the following example I suppress the padded blank at the end of the month name, but preserve the leading zero on the day number with a second specification of FM: TO_CHAR (SYSDATE, 'fmMonth FMDD, YYYY') ==> April 05, 1994' If you do not use FM in your mask, a converted date value is always right-padded with blanks to a fixed length (that length is dependent on the different format elements you use). When you do use FM, on the other hand, the length of your return value may vary depending on the actual values returned by the different format elements. When you do not use FM to convert a number to a character string, the resulting value is always left-padded with blanks so that the number is right-justified to the length specified by the format (or declaration of the variable). When you do use FM, the left-padded blanks are suppressed and the resulting value is left-justified. Here are some examples of the impact of FM on numbers converted with TO_CHAR: TO_CHAR (8889.77, 'L9999D99') ==> ' $8889.77' TO_CHAR (8889.77, 'fmL9999D99') ==> '$8889.77' The FM modifier can also be used in the format model of a call to the TO_DATE function to fill a string with blanks or zeros to match the format model. This variation of FM is explored in the discussion of FX. 14.3.2 FX: Matching Formats ExactlyPL/SQL offers the FX element as a modifier to a format mask. FX (format exact) specifies that an exact match must be performed for a character argument and date format mask in a call to the TO_DATE function. If FX is not specified, the TO_DATE function does not require that the character string match the format precisely. It makes the following allowances:
TO_DATE ('JANUARY^1^ the year of 94', 'Month-dd-"WhatIsaynotdo"yy') ==> 01-JAN-1994
This kind of flexibility is great -- until you want to actually restrict a user or even a batch process from entering data in a nonstandard format. In some cases, it simply is not a reflection of everything being OK when a date string has a pound sign ( With FX, there is no flexibility for interpretation of the string. It cannot have extra blanks if none are found in the model. Its numeric values must include leading zeros if the format model specifies additional digits. And the punctuation and literals must exactly match the punctuation and quoted text of the format mask (except for case, which is always ignored). In all of the following examples, PL/SQL raises one of the following errors: ORA-01861: literal does not match format string ORA-01862: wrong number of digits for this format item TO_DATE ('Jan 15 1994', 'fxMON DD YYYY') TO_DATE ('1-1-4', 'fxDD-MM-YYYY') TO_DATE ('7/16/94', 'FXMM/DD/YY') TO_DATE ('JANUARY^1^ the year of 94', 'FXMonth-dd-"WhatIsaynotdo"yy') The FX modifier can be specified in upper-, lower-, or mixed-case; the effect is the same. The FX modifier is a toggle, and can appear more than once in a format model. Each time it appears in the format, it changes the effect of the modifier. By default (that is, if FX is not specified anywhere in a format mask), an exact match is not required in any part of the string (as described above). So the first time that FX appears in the format it turns on exact matching for any following elements. The second time that FX appears in the format it indicates that an exact match is not required for any following elements, and so on. In the following example I specify FX three times. As a result, an exact match is required for the day number and the year number, but not the month number: TO_DATE ('07-1-1994', 'FXDD-FXMM-FXYYYY') ==> 07-JUL-1994 This next attempt at date conversion will raise ORA-01862 because the year number is not fully specified: TO_DATE ('07-1-94', 'FXDD-FXMM-FXYYYY') -- Invalid string for format! You saw in the previous section how the FM modifier would strip leading blanks and zeros from the output of a call to TO_CHAR. You can also use FM in the format model of a call to the TO_DATE function to fill a string with blanks or zeros. This action matches the format model (the opposite of the suppression action). You can, in other words, use FM to guarantee that a format exact match required by FX will succeed. The following call to TO_DATE will return a date because the fm at the beginning of the format mask turns on fill mode for the entire string, thus changing the 1 to 01 and 94 to 1994: TO_DATE ('07-1-94', 'FXfmDD-FXMM-FXYYYY') You can also include multiple references to both FM and FX in the same format string, to toggle both or either of these modifiers. 14.3.3 RR: Changing MilleniaWe are coming up fast on the end of the 20th century. How many of your programs will still work when the clock ticks over midnight on December 31, 1999? Many of your Oracle-based applications should be well protected since you have been able to take advantage of a true date datatype. In other words, you haven't had to write any special programs to manually manipulate dates, thereby leaving yourself vulnerable. On the other hand, most everyone has been using a two-digit year in their date format masks, either inherited from the default DD-MON-YY or with common substitutes like MM/DD/YY. The two-digit year format elements could give you problems when the century and millenium are close to changing. The YY format element always defaults to the current century. So when it is November 1999 and your user enters 1/1/1 or 1-JAN-1, they will enter into the database the date of January 1, 1901 -- not January 1, 2001, as they might have been thinking. What's an IS manager to do? One solution is to go into all your screens and change or add trigger logic so that if the user enters a year number less than ten (or whatever you decide the cutoff to be), then the next century will be assumed. That will work, but it surely must be a most undesirable prospect. Fortunately, Oracle7 provides a new format element to take care of this problem: the RR format model. With RR you can enter dates from the 21st century before the year 2000 and you can enter dates from the 20th century after the year 2000 (like the birthdays of employees and customers). Here is how RR works: If the current year is in the first half of the century (years through 49) then:
Here are some examples of the impact of RR. Notice that the same year numbers are returned for Year 88 and Year 18, even though SYSDATE returns a current date in the 20th and 21st centuries, respectively: SELECT TO_CHAR (SYSDATE, 'MM/DD/YYYY') "Current Date", TO_CHAR (TO_DATE ('14-OCT-88', 'DD-MON-RR'), 'YYYY') "Year 88", TO_CHAR (TO_DATE ('14-OCT-18', 'DD-MON-RR'), 'YYYY') "Year 18" FROM dual; Current Date Year 88 Year 18 ------------ ------- ------- 11/14/1994 1988 2018 SELECT TO_CHAR (SYSDATE, 'MM/DD/YYYY') "Current Date", TO_CHAR (TO_DATE ('10/14/88', 'MM/DD/RR'), 'YYYY') "Year 88", TO_CHAR (TO_DATE ('10/14/18', 'MM/DD/RR'), 'YYYY') "Year 18" FROM dual; Current Date Year 88 Year 18 ------------ ------- ------- 11/14/2001 1988 2018 Of course, if you use the RR format after the year 2000 and want to enter a date that falls in the latter half of the 21st century, you will need to add special logic. Masks with the RR format model will always convert such two-digit years into the previous century. There are a number of ways you can activate the RR logic in your current applications. The cleanest and simplest way is to change the default format mask for dates in your database instance(s). You can do this by changing the NLS_DATE_FORMAT initialization parameter as follows: NLS_DATE_FORMAT = 'MM/DD/RR' or: NLS_DATE_FORMAT = 'DD-MON-RR' depending on what the previous format was. Then, if you have not hardcoded the date format mask anywhere else in your screens or reports, you are done. Bring down and restart the database and then your application will allow users to enter dates in the 21st century. If you do have date format masks in the format property for an Oracle Forms item or in an Oracle Reports query or field, you will need to change those modules to reflect the new approach embodied by RR. 14.3.4 Using TO_CHAR to Create a Date RangeAt times, users want information about activity on a specific date. In other situations, however, their interest lies in a range of dates. The user might enter the two dates and then expect to view all data that falls between them. Suppose, for example, that in an Oracle Forms application the user enters 12/4/93 in the start date field and 4/8/96 in the end date field. The query that Oracle Forms executes against the database would need to have logic in it as follows: hire_date BETWEEN '04-DEC-93' AND '08-APR-96' or, more generally: hire_date BETWEEN :criteria.start_date AND :criteria.end_date where criteria is the name of the block containing the start_date and end_date fields. The colons (:) in front of the field names indicate to PL/SQL that these are bind variables from the host environment. Sometimes this general logic can be passed directly to the SQL layer. In other situations, programmers must use the Pre-Query trigger or the SET_BLOCK_PROPERTY built-in to alter the SQL statement directly. In this case, they will need to create a string date range from the input dates. Rather than write the application-specific code to handle this each time, you can build a generic utility, using TO_CHAR and TO_DATE conversion functions. I offer below the date_range function. Its specification is as follows: FUNCTION date_range (start_date_in IN DATE, end_date_in IN DATE, check_time_in IN VARCHAR2 := 'NOTIME') RETURN VARCHAR2 The arguments to date_range are:
If the start date is NULL or the end date is NULL, the string returned by date_range uses the <= and >= operators rather than the BETWEEN operator. Here are some examples of the output from date_range. Note that date_range places two contiguous quote marks around each date because these are actually string literals. At runtime these two quotes are resolved into a single quote by PL/SQL: date_range ('04-DEC-93', '08-APR-96') ==> BETWEEN TO_DATE (''04-DEC-93'') AND TO_DATE (''08-APR-96'') date_range ('04-DEC-93', NULL) ==> >= TO_DATE (''04-DEC-93'') date_range (NULL, '04-DEC-93') ==> <= TO_DATE (''04-DEC-93'') If you do want to include the time component, then date_range will generate a string in this form: date_range ('04-DEC-93', '08-APR-96', 'time') ==> BETWEEN TO_DATE (''04-DEC-93'', ''HHMMYYYY HHMISS'') AND TO_DATE (''08-APR-96'', ''HHMMYYYY HHMISS'') Let's take a look at how you might use date_range in query processing in Oracle Forms. The Pre-Query trigger modifies the Default Where clause of a base table block. In Pre-Query, an assignment actually results in the addition of a WHERE clause in the SQL query for that block. Let's look at a simple example first. If Pre-Query contains a statement like this: :customer.contact_date := '12-JAN-95'; then the query executed by the forms tool to fill the block contains a WHERE clause that looks like this: WHERE <other clauses> AND (contact_date = '12-JAN-95')
So a simple assignment results in a straightforward comparison/restriction in the WHERE clause. You can also place complex SQL statements in the WHERE clause by appending a pound sign ( :customer.customer_id := '# IN (SELECT customer_id FROM invoice WHERE invoice_total > ' || TO_CHAR (:invoice.amount); then the query executed by the forms tool to fill the block contains a WHERE clause that looks like this: WHERE <other-where-clauses> AND customer_id IN (SELECT customer_id FROM invoice WHERE invoice_total > 1000)
This very useful feature is documented in the
Advanced Oracle Forms Techniques
manual from Oracle Corporation. The syntax of a :employee.hire_date := '# BETWEEN ' || TO_CHAR (:criteria.start_date) || ' AND ' || TO_CHAR (:criteria.end_date); If I were to place literal dates inside this string assignment, then I would need to put two single quotes together in the string wherever I needed one single quote to appear in the actual value placed in that field, as follows: :employee.hire_date := '# BETWEEN ''01-JAN-93'' AND ''01-DEC-94'''; Now if I apply the date_range function to this Pre-Query context, I have one of the following: :customer.contact_date := '# ' || date_range (:criteria.start_date, :criteria.end_date); or: :customer.contact_date := '# ' || date_range (:criteria.start_date, :criteria.end_date, 'TIME'); In the first call to date_range, I rely on the default value of the check_time_in parameter of NOTIME to ignore the time component of the dates. In the second call, I explicitly request that the time component be included. The SET_BLOCK_PROPERTY built-in in Oracle Forms offers another method of modifying the DEFAULT WHERE clause of a base table block. It allows you to directly pass a string of SQL syntax, which then replaces the DEFAULT WHERE clause specified at design time in the form. It is a much more structured approach than using the # syntax. In the following two calls to the built-in, I call date_range to generate a date range and attach that date range syntax to the contact_date column. In the second example I apply TRUNC to the contact date so that the time at which the contact_date was entered does not become a factor in the range check: SET_BLOCK_PROPERTY ('customer', DEFAULT_WHERE, 'contact_date ' || date_range (:criteria.start_date, :criteria.end_date, 'TIME')); or: SET_BLOCK_PROPERTY ('customer', DEFAULT_WHERE, 'TRUNC (contact_date) ' || date_range (:criteria.start_date, :criteria.end_date)); Here is the code for the date_range function: /* Filename on companion disk: daternge.sf */ FUNCTION date_range (start_date_in IN DATE, end_date_in IN DATE, check_time_in IN VARCHAR2 := 'NOTIME') RETURN VARCHAR2 IS /* String versions of parameters to place in return value */ start_date_int VARCHAR2(30); end_date_int VARCHAR2(30); /* Date mask for date<->character conversions. */ mask_int VARCHAR2(15) := 'MMDDYYYY'; /* Version of date mask which fits right into date range string */ mask_string VARCHAR2(30) := NULL; /* The return value for the function. */ return_value VARCHAR2(1000) := NULL; BEGIN /* || Finalize the date mask. If user wants to use time, add that to || the mask. Then set the string version by embedding the mask || in single quotes and with a trailing paranthesis. */ IF UPPER (check_time_in) = 'TIME' THEN mask_int := mask_int || ' HHMISS'; END IF; /* || Convert mask. Example: || If mask is: MMDDYYYY HHMISS || then mask string is: ', 'MMDDYYYY HHMISS') */ mask_string := ''', ''' || mask_int || ''')'; /* Now convert the dates to character strings using format mask */ start_date_int := TO_CHAR (start_date_in, mask_int); end_date_int := TO_CHAR (end_date_in, mask_int); /* If both start and end are NULL, then return NULL. */ IF start_date_int IS NULL AND end_date_int IS NULL THEN return_value := NULL; /* If no start point then return "<=" format. */ ELSIF start_date_int IS NULL THEN return_value := '<= TO_DATE (''' || end_date_int || mask_string; /* If no end point then return ">=" format. */ ELSIF end_date_int IS NULL THEN return_value := '>= TO_DATE (''' || start_date_int || mask_string; /* Have start and end. A true range, so just put it together. */ ELSE return_value := 'BETWEEN TO_DATE (''' || start_date_int || mask_string || ' AND TO_DATE (''' || end_date_int || mask_string; END IF; RETURN return_value; END; 14.3.5 Building a Date ManagerThe Oracle Server offers the ability to set a default date format for each instance of a database with the NLS_DATE_FORMAT initialization parameter.[ 2 ] Oracle provides a ruthlessly efficient gatekeeper for its RDBMS: there is no way you will ever be able to enter an invalid date into the database. And there are lots of functions that enable you to perform arithmetic on dates once they are in the database. There are, however, obstacles to entering dates efficiently:
It is possible with PL/SQL to build a "date manager" that satisfies the above moral imperatives for our users. The rest of this section explores the implementation of a function, dm_convert, which converts a string entered by the user into an actual Oracle date value. It liberates the user from having to know the default/enforced format in the Oracle7 database, because the input can conform to any of a wide variety of formats. The function determines which format applies, and returns the date. 14.3.5.1 The dm_convert function date masksTable 14.4 shows the different date masks which dm_convert supports. The user can enter a string conforming to any of these masks and dm_convert will return a date value. It will not issue such errors as: ORA-01861 literal does not match format string ORA-01858 a non-numeric character found where a digit was expected In addition to supporting many different date formats, dm_convert also offers conversion of "shortcut" entries. Suppose the user wishes to enter the Monday (first day) of this week or the last day of the month. Rather than requiring that users figure out the dates for those days, dm_convert allows them to simply enter a shortcut like "ew" for "end of week." Table 14.5 shows the shortcuts supported by dm_convert. You can easily add your own! Here are some examples of the way dm_convert changes a string to a date (assuming today's date is December 15, 1994): dm_convert ('12') ==> 12-DEC-1994 dm_convert ('3/15') ==> 15-MAR-1994 dm_convert ('em') ==> 31-DEC-1994
14.3.5.2 Using exception handlers to find the right formatYou might be wondering how Steven gets around raising those nasty Oracle errors relating to invalid date formats when he tries to convert the string to a date. The answer is that I use both the procedurality and the exception handling of PL/SQL. In native SQL, I can use TO_DATE to convert a string to a date, but if the format doesn't match the string, I will get an error and the SQL statement will fail. In fact, this is exactly what Oracle Forms does when a user enters a value in a date item. In PL/SQL, I can use the EXCEPTION clause in my program to trap a conversion failure and handle that failure. Usually when you get such a failure you raise an error, as shown in the following trap conversion failure example: FUNCTION convert_date (string_in IN VARCHAR2) RETURN DATE IS BEGIN RETURN TO_DATE (string_in); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE (' Invalid format for date conversion of ' || string_in); END; Clearly, this is not the behavior I want in dm_convert. In dm_convert, a conversion failure does not result from a user's error in entry. It is simply the first step in a search for the right date format mask. The behavior I need to create in dm_convert is the following:
I can use nested exception handlers, as shown in the following example, to implement this multiple-pass technique: FUNCTION dm_convert (string_in IN VARCHAR2) RETURN DATE IS my_date DATE; BEGIN BEGIN my_date := TO_DATE (string_in, 'MM/DD'); EXCEPTION WHEN OTHERS THEN BEGIN my_date := TO_DATE (string_in, 'MM/DD/YY'); EXCEPTION WHEN OTHERS THEN BEGIN my_date := TO_DATE (string_in, 'MM/DD/YYYY'); .. and so on for all the formats... END; END; END; END; Here, the dm_convert function uses nested anonymous blocks, each with its own exception section, to trap a date conversion failure and pass it on to the next format. The sequence and variety of masks used dictate the range of valid user input and the precedence with which it is parsed. One problem you might notice with this approach is with indentation. When I use my indentation guidelines for these nested blocks and exception handlers, I quickly run out of room on my page! As a result, in the final version of dm_convert, you will see that I pointedly give up trying to properly indent the exception sections of the function. Instead, I structure the exception sections like a CASE statement: /* Filename on companion disk: dmcnvrt.sf */ FUNCTION dm_convert (value_in IN VARCHAR2) RETURN DATE /* || Summary: Validate and convert date input of most any format. || dm_convert stands for "date manager conversion". Accepts || a character string and returns a fully-parsed and validated || date. If the string does not specify a valid date, the function || returns NULL. */ IS /* Internal, upper-cased version of date string */ value_int VARCHAR2(100) := UPPER (value_in); /* The value returned by the function */ return_value DATE := NULL; /* Transfer SYSDATE to local variable to avoid repetitive calls */ today DATE := SYSDATE; BEGIN /* || Handle short-cut logic before checking for specific date formats. || Supported short-cuts include: || EW - end of week || BW - beginning of week || EM - end of month || BM - beginning of month || || Add shortcuts for quarters specific to your site. */ IF value_int = 'EW' THEN /* End of week in this case is Friday of the week */ return_value := NEXT_DAY (today, 'FRIDAY'); ELSIF value_int = 'BW' THEN /* Beginning of week in this case is Monday of the week */ return_value := NEXT_DAY (today, 'MONDAY') - 7; ELSIF value_int = 'BM' THEN return_value := TRUNC (today, 'MONTH'); ELSIF value_int = 'EM' THEN return_value := LAST_DAY (today); ELSIF value_int IS NOT NULL THEN /* No known short-cut. The user must have entered a date string for || conversion. Now attempt to convert the value using a sequence || of calls to TO_DATE. If one attempt fails, pass it to the next || TO_DATE and format mask within a (very) nested exception section. */ BEGIN return_value := TO_DATE (value_int, 'DD'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MM/DD'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MM/DD/YY'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MM/DD/YYYY'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'DD-MON'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'DD-MON-YY'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'DD-MON-YYYY'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MON'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MON-DD'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MON-DD-YY'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MON-DD-YYYY'); EXCEPTION WHEN OTHERS THEN BEGIN return_value := TO_DATE (value_int, 'MON-YYYY'); EXCEPTION WHEN OTHERS THEN return_value := NULL; END; END; END; END; END; END; END; END; END; END; END; END; END IF; /* Whether NULL or a real date, return the value */ RETURN (return_value); END; In the rest of this section I offer alternative implementations of dm_convert. These do not require nesting of exception handling sections and also avoid hardcoding the format masks into the function. 14.3.5.3 Table-driven date format masksOne drawback to the dm_convert procedure as implemented in the previous section is that everything is hardcoded. Sure, I offer lots of acceptable formats, but they still are all coded explicitly in the procedure. What if a new format needs to be added? In addition, the order of precedence of those formats in validating the date input is hardcoded. If a person enters a 1 and the system date is 12-FEB-95, then dm_convert will change the entry into 01-FEB-95. Suppose, however, that the user really wanted to enter 01-JAN-95 by entering a 1 and suppose further that such defaulted entry is a requirement of your application? Generally, I like to avoid hardcoding any literal values (like the specific formats and their order of execution) in my routines. Any changes (additions or deletions from the supported date formats, or a request in a specific application to move a format up in the batting order) necessitate a change to the program itself, and then the recompilation of all affected modules. I can avoid this scenario by making the date formats and the order in which they are used data-driven. Here's what I would need to do:
/* Filename on companion disk: dmcntab.sf */ FUNCTION dm_convert (value_in IN VARCHAR2) RETURN DATE /* I will only comment the new sections in the function */ IS value_int VARCHAR2(100) := UPPER (value_in); return_value DATE := NULL; today DATE := SYSDATE; /* Now set up a cursor to go through the table of formats */ CURSOR mask_cur IS SELECT date_mask FROM dm_date_mask ORDER BY date_mask_seq; mask_rec mask_cur%ROWTYPE; BEGIN /* Convert short-cut entry. Same as before. */ IF value_int = 'EW' THEN return_value := NEXT_DAY (today, 'FRIDAY'); ELSIF value_int = 'BW' THEN return_value := NEXT_DAY (today, 'MONDAY') - 7; ELSIF value_int = 'BM' THEN return_value := TRUNC (today, 'MONTH'); ELSIF value_int = 'EM' THEN return_value := LAST_DAY (today); ELSIF value_int IS NOT NULL THEN /* || Open the cursor and loop through the date masks until one || of them is used successfully in a TO_DATE conversion. */ OPEN mask_cur; LOOP /* Fetch a record. Exit loop if there aren't any more masks */ FETCH mask_cur INTO mask_rec; EXIT WHEN mask_cur%NOTFOUND; /* || Still need separate PL/SQL block in the function to trap || a conversion failure, but I only need ONE! */ BEGIN /* Try to convert the date */ return_value := TO_DATE (value_int, mask_rec.date_mask); /* || If I made it this far, I have converted the string, so || EXIT the loop. */ EXIT; EXCEPTION /* || Conversion failure. Reset value to make sure it is still || NULL and then keep going -- back to the loop! */ WHEN OTHERS THEN return_value := NULL; END; END LOOP; CLOSE mask_cur; END IF; /* Return the converted date */ RETURN return_value; END; This new version of dm_convert not only looks more elegant than the first version -- it is considerably more flexible in its approach. If you no longer want to accept Month YYYY DD as a date format, simply pull up the maintenance screen, delete that entry from the table, or execute a DELETE from the dm_date_mask table directly in SQL*Plus. Poof ! The users will no longer be able to enter January 1995 11 for a date value. There is, of course, a downside to this solution, and it is a familiar one. The price of making your code more flexible (for the developer) is almost always to make your code work less efficiently. Now, instead of executing a series of inline TO_DATE conversion functions, we fetch a series of records from a table. The dm_date_mask table could well be sitting on a remote server. This network traffic could well make the application performance unacceptable to precisely the people you are trying to help by providing flexible date formats. What's a developer to do? One answer is to transfer the table into a memory structure at the start of each user session, as we describe in the next section. 14.3.5.4 Stashing date masks in memorySay you moved the date masks to a table and that any changes made to the dm_date_mask table will not happen very often. For all intents and purposes, the date masks are constant for a particular user session. So it is not really necessary to read the masks from the table every time you need to validate a date entry. Instead, we can transfer the masks from the table into a memory structure at the start of the user session. With this approach you add a little more time to the startup execution of the form, but then speed up the validation and conversion for each date item. You have a number of options for storing these date masks in memory:
I provide the code for PL/SQL table-based alternatives in the following sections. The technique based on Oracle Forms GLOBAL variables may be found in the dmcnvrt.doc and dmcnvrt.fp files on the companion disk. In both cases, there are two steps involved in using the memory-resident, data-driven approach:
Notice that the specification of the call to dm_convert does not change with any of these new implementations. I am changing the engine under the hood without making any alterations to the body style, dashboard, or steering controls. None of the triggers that call dm_convert would have to be modified if you did change the storage implementation for the date masks. 14.3.5.5 Storing and accessing date masks in a PL/SQL tableYou can also store the date masks in a PL/SQL table (PL/SQL tables are explained fully in Chapter 10, PL/SQL Tables ). The PL/SQL table structure, available only with PL/SQL Version 2, is similar to both a database table and a single-dimensional array. There are two steps to using date masks from the PL/SQL table: Initialize the date masks in the PL/SQL table structure, and revamp dm_convert to rely on the PL/SQL table. These are described below:
Now I can call dm_convert just as I did in the good old days: my_date := dm_convert ('12'); Copyright (c) 2000 O'Reilly & Associates. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|