Qbasicnews.com

Full Version: Write a bulletproof date validation routine.
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Pages: 1 2 3 4 5 6
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))
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.
*****
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.
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)!..
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.
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?"
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...
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.
Quote:(5) Take every situation into account. You already did: Your R/LTRIMming was awesome... I never thought it...

In particular, it's a good idea to write as many test cases as possible before writing the actual function/method/class (in whatever language you're coding in). (Black box testing).
Quote: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)!..
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)!.....

Your remarks about the Valid Invalid strings are true. I used these specific strings to emphasize the result for the challenge. For "real world" as you say, they would return true (-1) or false (0).

Your remark of "code below was never tested!" completely turned me off regarding any appreciation of your suggested code "improvements".
*****
Here's what I made today (in 20 minutes):

[syntax="QBasic"]DECLARE FUNCTION IsValidDate% (TheDate AS STRING)

'$DYNAMIC
DEFINT A-Z

'+-----------------------------------------------------+
'| FUNCTION IsValidDate |
'| |
'| This function takes a string of 8 characters. This |
'| string indicates a specific date in the format |
'| YYYYMMDD. In which YYYY must be in the range 1800 |
'| to 3999. MM must be in the range 01 to 12. And DD |
'| must have a leading zero if DD < 10. |
'| |
'| The function returns -1 if the specific date is a |
'| valid date, otherwise it returns 0. |
'| |
'| Neo |
'+-----------------------------------------------------+
FUNCTION IsValidDate (TheDate AS STRING)
'first check for correct input (only numbers, and string must be 8 characters in length)
TD$ = LTRIM$(RTRIM$(TheDate))
Allowed$ = "0123456789"
FOR I = 1 TO LEN(TD$)
IF INSTR(Allowed$, MID$(TD$, I, 1)) = 0 THEN IsValidDate = 0: EXIT FUNCTION
NEXT I
IF LEN(TD$) <> 8 THEN IsValidDate = 0: EXIT FUNCTION

'check for the year (must be >= 1800 and <= 3999)
Year = VAL(LEFT$(TD$, 4))
IF NOT(Year >= 1800 AND Year <= 3999) THEN IsValidDate = 0: EXIT FUNCTION

'check for the month (must be >= 1 and <= 12)
Month = VAL(MID$(TD$, 5, 2))
IF NOT(Month >= 1 AND Month <= 12) THEN IsValidDate = 0: EXIT FUNCTION

'check if year Year is a leap year
IsLeapYear = ((Year MOD 4 = 0) AND (Year MOD 100 <> 0)) OR (Year MOD 400 = 0)

'prepare maxdays variable (numbers of days in the month)
I = Month
MaxDays = 30 + ((I + (I > 7)) MOD 2) + 2 * (I = 2) + (I = 2) * IsLeapYear

'check for the day (must be >= 1 and <= MaxDays)
Day = VAL(MID$(TD$, 7, 2))
IF NOT(Day >= 1 AND Day <= MaxDays) THEN IsValidDate = 0: EXIT FUNCTION

IsValidDate = -1
END FUNCTION[/syntax]

I hope it works Wink (I've tested it numerous times and it seems to work Wink)

Note that this entry was written for clarity. I.e., it could have been done shorter, but I think it's much more clear this way Smile

Another note: this entry was created in 20 minutes, without any help from online resources or previously submitted entries.

Btw, if you like this function to return "VALID" or "INVALID", you could easily make a wrapper:
[syntax="QBasic"]DECLARE FUNCTION IsValidDateWrapper$ (TheDate AS STRING)

FUNCTION IsValidDateWrapper$ (TheDate AS STRING)
IsValidDateWrapper$ = LTRIM$(MID$("IN ", 2 * ABS(IsValidDate(TheYear)) + 1, 2) + "VALID")
END FUNCTION
'note: wrapper not tested but should work Wink[/syntax]

Also, if you like to see what the short version looks like (the unclear one): Wink
[syntax="QBasic"]FUNCTION IsValidDate (TheDate AS STRING)
IsValidDate = 0
TD$ = LTRIM$(RTRIM$(TheDate))
FOR I = 1 TO LEN(TD$)
IF INSTR("0123456789", MID$(TD$, I, 1)) = 0 THEN EXIT FUNCTION
NEXT I
IF LEN(TD$) <> 8 THEN EXIT FUNCTION
IF VAL(LEFT$(TD$, 4)) < 1800 OR VAL(LEFT$(TD$, 4)) > 3999 OR VAL(MID$(TD$, 5, 2)) < 1 OR VAL(MID$(TD$, 5, 2)) > 12 THEN EXIT FUNCTION
I = VAL(MID$(TD$, 5, 2)) : J = VAL(LEFT$(TD$, 4))
IF VAL(MID$(TD$, 7, 2)) < 1 OR VAL(MID$(TD$, 7, 2)) > 30 + ((I + (I > 7)) MOD 2) + 2 * (I = 2) + (I = 2) * (((J MOD 4 = 0) AND (J MOD 100 <> 0)) OR (J MOD 400 = 0)) THEN EXIT FUNCTION
IsValidDate = -1
END FUNCTION[/syntax]
Hello, Moneo.

You're right on the discouraging header "note (!)" This was 22:30 coding, and, truly speaking, it went the way you saw to mean that I "didn't have a QBasic interpreter at my office computer and, was too lazy and tired to download one!" For the healthiest mind, this was too misleading!

The remark conflicts with those I have written, but have in mind: My experience doesn't rely on such a simple one.

Your appreciating "criterias" are another story, and I'm quite shocked that you have been much short sighted: Can a single line stand for the whole? Programming methodology and documenting are different. If you would have spotted some technical places you oppose, and shown relationships of them with your own, we could talk. But, as of yet, there's no point in that.

And, Meg. Hello!

I'd like to clarify your concerns about my suggestion numbered (4) by giving a brief understanding of Discrete Maths. This is the area of maths which deals with discrete, unconnected objects, events which we use in analyzing our programming methodology -- that is algorithms, artificial intelligence applications, and so forth. Let me divide this into two to make it more clear:

(a) Algorithmic thinking: This deals with the ways we use to build unique solutions to the unique problems. My IsAValidDate() is a good example of this, and it is pretty well written: You can subdivide it into discrete sets to understand or test separately [IsALeapYear(), Tokenize()].

(b) Discrete structures: This deals with the ways we include "others' solutions" to our projects. What are they? Binary trees they are, linked lists they are, hashing they are. On many of them, much discussion have been made, and they are 'de facto' standards in some cases. Please refer to my suggestion numbered (1) of my past post to understand what I mean.

What is common in (a), and (b)? They let us develop our programming methodologies in a way so that we can examine and analyze them, and represent them in some metrics (Big-O, anyone?).

So, my efforts on suggesting 'break them down!' aren't just a matter of obsession, but the Computer Science itself.

Let's say our IsAValidDate() went international to meet different sorting of Date, Month, Year trilogy. If we were to pack everything into a single function, testing and updating it would be tad more painful. Think of giant games, database systems, or operating systems functions!.. But in this case, we only need to update and test result = Tokenize(...) or better, just implement
Code:
result = Tokenize(FormatDa$(Da$), ...)
and only test FormatDa$() to see if it meets our criterias.

If you are interested, I may suggest you some books on Discrete Maths. But, better, search for them in your favorite searching engine. Even better, go to your local library and find the one which suits you best.

Glad if I was of some help.
_____

Edit history:
(i) Some spelling corrected.
Quote:(Big-O, anyone?).

Mememe!

Having said what you did however, creating a whole function to do some very simple algorithm may do nothing more than clutter up the namespace and confuse some people... not so much a problem in object orientated languages however!
Pages: 1 2 3 4 5 6