Qbasicnews.com
October 17, 2018, 06:07:55 AM *
Welcome, Guest. Please login or register.

Login with username, password and session length
News: Back to Qbasicnews.com | QB Online Help | FAQ | Chat | All Basic Code | QB Knowledge Base
 
   Home   Help Search Login Register  
Pages: [1] 2 3 4
  Print  
Author Topic: Write a bulletproof date validation routine.  (Read 33053 times)
Moneo
Na_th_an
*****
Posts: 1971


« on: October 10, 2004, 10:00:45 PM »

Bulletproof date validation routine, subroutine or function.

GIVEN: An input string in the format of: YYYYMMDD

where:
YYYY  is the year (past, present or future: from 1600 to 3999)
MM is the month
DD is the day

CHALLENGE: Read the date input string and make absolutely sure that the represented date is valid.

VALIDITY: To be valid:
* The year must be from 1600 to 3999)
* The month must be from 01 to 12
* The day must be from 01 to the last day of the above month, considering leap year.
* Obviously, if the date input string is not 8 bytes long, the date is invalid.

OUTPUT: The message "VALID" or "INVALID".

If you haven't needed this routine yet, for sure you will need it someday.

I will test most entries, and tell you if it works, or on what date it fails.
*****
Logged
Meg
Ancient QBer
****
Posts: 483


« Reply #1 on: October 11, 2004, 01:03:08 PM »

Here's my entry. It's pretty straightforward. The DinM% formula looks pretty beastly, but I've triple-checked it, and it does the same thing as several lines of code that check the requirements for leap year. Lemme know if it needs further explanation.

*peace*

Meg.

Code:
FUNCTION Validate$ (d$)

'ASSUME TRUE UNLESS PROVEN FALSE
'%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Validate$ = "VALID"

'CHECK STRING LENGTH
'%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
IF LEN(d$) <> 8 THEN Validate$ = "INVALID": EXIT FUNCTION

'CHECK NUMERIC, LEADING ZERO, "-", "D" NOT A PROB
'%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
IF INSTR(UCASE$(d$), "D") OR INSTR(d$, "-") THEN Validate$ = "INVALID": EXIT FUNCTION
IF LTRIM$(RTRIM$(STR$(VAL(d$)))) <> d$ THEN Validate$ = "INVALID": EXIT FUNCTION

'EXTRACT YEAR, MONTH, DAY
'%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
y% = VAL(LEFT$(d$, 4))
m% = VAL(MID$(d$, 5, 2))
d% = VAL(RIGHT$(d$, 2))

'GET DAYS IN MONTH. IF FEB, CHECK LEAP YEAR
'%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
DInM% = 31 + (m% MOD 2) * (m% > 7) + (m% MOD 2 XOR 1) * (m% < 8)
IF m% = 2 THEN DInM% = 28 - ((y% MOD 4 = 0) + (y% MOD 100 = 0) * NOT (y% MOD 400 = 0))

'CHECK THE RANGES
'%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
IF y% < 1800 OR y% > 3999 THEN Validate$ = "INVALID": EXIT FUNCTION
IF m% < 1 OR m% > 12 THEN Validate$ = "INVALID": EXIT FUNCTION
IF d% < 1 OR d% > DInM% THEN Validate$ = "INVALID": EXIT FUNCTION

END FUNCTION


-=-EDITS-=-
10/12/2004: included check for "-" and "D". Changed date from 1600 to 1800.
Logged
Z!re
*/-\*
*****
Posts: 4599


« Reply #2 on: October 11, 2004, 04:40:14 PM »

Shocked

wow... Thats... short... I'd imagine lots of more code, then again, i didn't even try to do it Tongue
Logged
Meg
Ancient QBer
****
Posts: 483


« Reply #3 on: October 11, 2004, 05:09:30 PM »

Moneo made the challenge a lot easier by only allowing the years 1600-3999.  If he included earlier dates, the challenge gets harder because you have to take into account all the calendar shifts with respect to leap years.
Logged
Antoni Gual
Na_th_an
*****
Posts: 1434



WWW
« Reply #4 on: October 11, 2004, 08:11:42 PM »

In thta case you must consider the country you are on, as the calendar shifts did'nt take place in ths same day.

For example UNESCO has declared 23 April the World Book day,  because in that day of 1616 both William Shakespeare and Miguel de Cervantes died. In fact they died  with  twenty-one days of interval. Spain  was using then the Gregorian (modern) calender and England was using the Julian calender....
Logged

Antoni
Moneo
Na_th_an
*****
Posts: 1971


« Reply #5 on: October 12, 2004, 12:21:42 AM »

Antoni has a valid point. My first choice of the "from" date was 1800. Maybe we should change the 1600 to 1800 to cover the differences mentioned by Antoni.

MEG, you're solution looks pretty good except for the VAL statement. My experience has been that certain values containing a "D" make QB think it is an exponential value. I have to look up an old validation routine where the "D" made it fail.
Give me a few days to thoroughly check out your routine. I intend putting it inside a loop to check all the date combinations against a routine that has been working for me for years.
*****
Logged
Z!re
*/-\*
*****
Posts: 4599


« Reply #6 on: October 12, 2004, 06:52:56 AM »

Code:
If INSTR(d$, "D") Then Validate$ = "INVALID"
If INSTR(d$, "-") Then Validate$ = "INVALID"
Logged
Meg
Ancient QBer
****
Posts: 483


« Reply #7 on: October 12, 2004, 12:01:14 PM »

Moneo:  Yes, you're right.  The program snags on "200D1010" with an overflow error.  I added a line to cover this, and changed the date from 1600 to 1800, as noted.
Logged
Moneo
Na_th_an
*****
Posts: 1971


« Reply #8 on: October 14, 2004, 11:53:59 PM »

Meg, I ran your validation logic side by side with mine in the same program, in a loop from years 1800 to 3999, months from 1 to 12, and days from, 1 to 31. The days going to 31 will generate "invalid" dates.

Well, your logic and mine agreed 100% with invalids and valids.

Excellent work, Meg. :bounce:
*****
Logged
Moneo
Na_th_an
*****
Posts: 1971


« Reply #9 on: October 16, 2004, 10:55:46 PM »

Meg,

I can't understand the DinM logic. Could you explain it? Did you derive this or did you "lift" it from somewhere?

I get the number of days in a month from a little table which has 28 for February, month 2. Then if month 2 and leap year, I add one. Not elegant, but easy to understand.

A suggestion on the next line that tests if month 2 and adds 1 if it's leap year. The leap year logic here is embedded and specifically tailored to adding 1 if leap year. I suggest that you have one and only one function for determining leap year, and that you always use it regardless of the program's needs. Otherwise, you have different implementations of the leap year logic depending on the program, which increases the risk of error.

The following is the leap year function I've been using for about 15 years. The result is -1 (true) or 0 (false).
Code:
' ====================== ISLEAPYEAR ==========================
'         Determines if a year is a leap year or not.
' ============================================================
'
FUNCTION IsLeapYear (Z) STATIC

   ' If the year is evenly divisible by 4 and not divisible
   ' by 100, or if the year is evenly divisible by 400, then
   ' it's a leap year:
   IsLeapYear = (Z MOD 4 = 0 AND Z MOD 100 <> 0) OR (Z MOD 400 = 0)
END FUNCTION

*****
Logged
Meg
Ancient QBer
****
Posts: 483


« Reply #10 on: October 17, 2004, 12:49:21 PM »

These are formulas I came up with.  I'll try to explain them.

ok when you test an equation, QB returns 0 (false) or -1 (true).

even numbered months before august (month#8) and odd numbered months after July (month#7) have 30 days (except feb, but that's covered in line 2)

So, line 1 is an equation that starts with 31 days and adds the validity of the above statement.  If it's true, it subtracts 1, leaving 30 days.

Code:
DInM% = 31 + (m% MOD 2) * (m% > 7) + (m% MOD 2 XOR 1) * (m% < 8)


I'll break it apart:

Code:
31                      default value
m% MOD 2                returns 0 if date is even, 1 if date is odd
m% > 7                  returns -1 if date is > 7, 0 if date < 8
m% MOD 2 XOR 1          returns 0 if date is odd, 1 if date is even
m% < 8                  returns -1 if date is < 8, 0 if date > 7


now, we multiply the first two and the second two together:

Code:
(m% MOD 2) * (m% > 7)                returns -1 if date is odd and > 7
(m% MOD 2 XOR 1) * (m% < 8)          returns -1 if date is even and < 8


so if either one of these is true, 31 gets added to -1 and 0 for a result of 30
otherwise, 31 gets added to 0 and 0 for a result of 31
since both products can't be -1, you never end up with 31+ -1 + -1 = 29.

Code:
IF m% = 2 THEN DInM% = 28 - ((y% MOD 4 = 0) + (y% MOD 100 = 0) * NOT (y% MOD 400 = 0))


This line does pretty much the same thing, only it starts with 28 as the default value and adds 1 if:

1. the year is evenly divisible by 4, unless the year is evenly divisible by 100 and not evenly divisible by 400.

Code:
28                      default value
y% MOD 4 = 0            returns -1 if year is divisible by 4, otherwise 0
y% MOD 100 = 0          returns -1 if year is divisible by 100, otherwise 0
y% MOD 400 = 0          returns -1 if year is divisible by 400, otherwise 0


I hope this clears it up!

*peace*

Meg.

edit:  another alternative to line #1 is:

Code:
DinM% = VAL(MID$("312831303130313130313031", m% * 2 - 1, 2))


so using your bit of code (which i like!) it would be this:

Code:
DinM% = VAL(MID$("312831303130313130313031", m% * 2 - 1, 2))
IF DinM% = 2 then DinM% = 28 - ((m% MOD 4 = 0 AND m% MOD 100 <> 0) OR (m% MOD 400 = 0))
Logged
Moneo
Na_th_an
*****
Posts: 1971


« Reply #11 on: October 17, 2004, 09:16:23 PM »

Meg,

Thanks for breaking down the explanation of your DinM logic. It's very clever, perhaps too clever since it takes more than 10 lines to explain it. I prefer the table-of-days-per-month approach, since it requires no explanation.

I have some similarly clever boolean logic on one line of code in one of my utility programs. It's been working for over 10 years, but every time I look at it, I vow to change it to something simple the next time I update it. This line of code is followed by about 8 lines of comments to explain it, and I myself still have trouble understanding it. I wonder who said KISS (Keep It Simple Stupid). There's a lot of truth in this.

Back when I was an assembly language programmer, we would go  running to a colleague and say "look, I was able to code that logic in only 5 instructions". Normally, the less instructions, the more efficient it was. The thinking was: the shorter the program, the less bugs it should have. But, of course, you break the barrier of precise, efficient code when your code gets so clever that others can't figure it out --- not even yourself in time.

I'm glad you liked my leap year function. It also is on the borderline of being too clever. I'm sure you're aware that the leap year logic has been expanded for some time now to include an exception for years that are a multiple of 4000, similiar to the current logic for multiples of 400. I chose not to worry about it, so I limit my years up to 3999.
*****
Logged
Meg
Ancient QBer
****
Posts: 483


« Reply #12 on: October 18, 2004, 02:13:58 AM »

I think I like this one best:

Code:
DinM% = VAL(MID$("312831303130313130313031", m% * 2 - 1, 2))


I wish I'd thought of that when I wrote the original.
Logged
ToohTooh
New Member

Posts: 24



« Reply #13 on: October 22, 2004, 04:18:03 PM »

Yes, some thoughts from the real world experience...
    NOTICE:
    Code below was **NEVER** tested! I'm bored at the office and compiled this one!.. If possible, debug it and, kindly ask me to edit the post (or, post the correct one)!..[/list]To catch your attention later, list starts off from 2, and this is not a bug -- keep reading! :-D

    Quote
    OUTPUT: The message "VALID" or "INVALID".
    (2) To me, this doesn't necessarily mean the function returning the actual word. If not needed, functions **DO NEVER RETURN STRINGS** (yes, they're like math functions)!..

    (3) You set beautiful traps for validating the date. Now, let's think: If traps are there, they want to end the function in error. If every trap means 'error,' you can assume FALSE at the beginning to shorten your code and make up a good logic (and this is a convention for this type of code -- altered version goes like the one below).

    You'll think: Doesn't it do the job? Well, it might... But I can't figure out how it does. So...

    (4) Break it into the most granule form. If you cannot at first (which is natural), study on paper before typing it out. I remember spending days on how to simplify and globalize a drag-and-drop support. It wasn't easy: Not only should it have worked flawlessly on icons, but on "every thing." (Like some piece of text you'll select and drag to an input box).

    You might have this:
    Code:
    ' ***
    '-- Moneo's Date Validation Challenge: An Alternative Approach
    '-- Explore under QB IDE.
    ' ***

    '-- by ToohTooh.


    FUNCTION IsAValidDate(Da$)

    IsAValidDate = 0  '>> Assume false

    '-- Break Da$ into 'day,' 'month,' and 'year.'
    IF NOT (Tokenize(Da$, day, month, year)) THEN EXIT FUNCTION

    '-- Sanity checks.
    IF (day < 1) OR (day > MaxDayForMonth(month, year)) THEN EXIT FUNCTION
    IF (month < 1) OR (month > 12) THEN EXIT FUNCTION
    IF (year < 1600) OR (year > 3999) THEN EXIT FUNCTION

    '-- If we got here, then all traps were defeated. Send success.
    IsAValidDate = -1

    END FUNCTION  '>> IsAValidDate()


    FUNCTION MaxDayForMonth(Mo, Ye)

    SELECT CASE Mo
    CASE 1, 3, 5, 7, 8, 10, 12: MaxDayForMonth = 31
    CASE 4, 6, 9, 11: MaxDayForMonth = 30
    '-- Sniff the leap year case for February.
    CASE 2: IF (IsALeapYear(Ye)) THEN MaxDayForMonth = 29 ELSE MaxDayForMonth = 28
    '-- Think about below:
    '    Q: Why 100 but not 99?  A: Tokenize() will have trapped it.
    '    Q: Do we need it?       A: Both yes and no. Think.
    CASE ELSE: MaxDayForMonth = 100  '>> Just being safe...
    END SELECT

    END FUNCTION  '>> MaxDayForMonth()


    FUNCTION IsALeapYear(Ye)

    '-- Based on re-interpreting Moneo's info.

    IF (Ye MOD 4 = 0) THEN
        '-- Divisible by 4. Probably a leap year. Now see if it has to do with
        '    some rare exceptions.
        IF (Ye MOD 100 = 0) THEN
            IsALeapYear = (Ye MOD 400 = 0)
        ELSE
            IsALeapYear = -1
        END IF
    ELSE
        IsALeapYear = 0
    END IF

    END FUNCTION  '>> IsALeapYear()


    FUNCTION Tokenize(Da$, day, month, year)

    Tokenize = 0

    Da$ = RTRIM$(LTRIM$(Da$))  '>> Idea from Meg.
    IF (LEN(Da$)) <> 8 THEN EXIT FUNCTION

    day = VAL(MID$(Da$, 1, 2))    '>> Move rover once, and...
    month = VAL(MID$(Da$, 2, 2))  '>> ...twice, and...
    year = VAL(MID$(Da$, 4, 4))   '>> ...thrice to extract.

    Tokenize = -1  '>> All done.

    END FUNCTION  '>> Tokenize()
    You are trying to show off some kind of binary and conventional math skills, but the explanation is longer than the code itself... Your mentor wouldn't like it...

    (5) Take every situation into account. You already did: Your R/LTRIMming was awesome... I never thought it...

    (6) TEST, TEST, TEST, and TEST... You needn't for a small one like this, but remember: In daily builds, we are using automatas which can use applications just like humans. Have that in mind.

    (7) No hacks. This means re-organizing code so that exceptional cases are minimal. This often requires good planning. Some are inevitable and must be clearly commented: 0! = 1 (Factorial 0 is an exception and equals to 1 -- was it?).

    (8) Minimal hard-coding! You should centralize data to retrieve it from a minimum number of places! (In fragments of Win2K source, they hard-coded file extensions into actual source code in some places! Say it changed! What then? Touch the code base, find the source, update it, inform everyone, update API docs, RECOMPILE, blah blah...
      NOTICE: I'm not judging Microsoft's expertise here, and as of yet, I am not skilled enough to.[/list]And...

      (1) DO A RESEARCH! Find the most acceptable, efficient ways of completing tasks!.. Are you retrieving files? May be dynamic hashing is for you. Go and look for some articles. Are you developing a word processing app? Tries are just the ticket for you!.. Are you sniffing for the presence of some text, some file name in your linked list? Why aren't you keeping the 128-bit signature of the complete list in array[0] to guess it on-the-fly? Ways are infinite!..

      For violation of (7), (8) and (1), thread http://forum.qbasicnews.com/viewtopic.php?t=7054 codes are great examples.

      If I am to tweak a saying in the famous book, 'Language, Proof and Logic,' it should go like
        'Computer languages are just like our every day languages... The more we practice, the more we talk among native speakers, it'll get better and better.'
      However, we should watch ourselves carefully while talking: I once read in a CV writing how-to book which said,
        "Avoid this in your CV:
      Quote
      'Please find those listed hereunder enclosed within.'
      Do we talk that way?"[/list]
      So, do we? We don't talk that way in real life. This post won't be a magic wand to correct BASICers' programming practices, but know one thing all that is: We don't talk that way.

      I know, I know... I can hear mumbles of 'but this is a challengeeeee...' Well, do challenges need to encourage poor programming practices?

      Last but not least, considering the ages of QBasicers, I think their ways of doing tasks should be accepted extra-ordinary. Having written this, I must send anyway!.. :,-(

      Anyway, some thoughts...

      NOTICE: BASIC is a great lab for us, this is true!.. But, you know... We should change labs when we advance. This doesn't mean hating the old one, or forgetting about it. Just leaving it...
      Logged

      Don't interrupt me while I'm interrupting." - Winston S. Churchill
      Meg
      Ancient QBer
      ****
      Posts: 483


      « Reply #14 on: October 22, 2004, 05:34:18 PM »

      I agree with almost everything ToohTooh says here.

      #2 - If this were a real-world piece of code I was writing, I'd definitely make it numeric instead of text function.  This was text because the challenge said it was supposed to be.

      #3 - I agree that the default value should be false.  It would lead to more efficient code.

      #4 - I'm a bit iffy on this one.  Generally, I'm in favor of splitting code apart into tiny routines.  However, if I have a routine that's short--like this one--and easy to follow, I find that splitting it into more routines can make it unnecessarily difficult to follow if I have to go back to for understanding later.  In my opinion, some routines are fine to have as self-contained code, particularly ones that contain no loops which can be followed easily from top to bottom.

      #5,6,7,8,9 - Agreed.

      Quote
      You are trying to show off some kind of binary and conventional math skills, but the explanation is longer than the code itself... Your mentor wouldn't like it...


      In all honesty, I was being a bit lazy.  I lifted the days-in-a-month code from an earlier challenge I entered:

      http://forum.qbasicnews.com/viewtopic.php?t=3402

      Moneo's been good about kicking me for writing code that's too complex for the problem.  In this instance, I feel that this code would have been better for calculating base days in a month:

      Code:
      DinM% = VAL(MID$("312831303130313130313031", m% * 2 - 1, 2))


      and Moneo's code was far simpler than mine for doing the leap-year adjustment.

      Thanks for the feedback and real-world spin!

      *peace*

      Meg.
      Logged
      Pages: [1] 2 3 4
        Print  
       
      Jump to:  

      Powered by MySQL Powered by PHP Powered by SMF 1.1.21 | SMF © 2015, Simple Machines Valid XHTML 1.0! Valid CSS!