Write a bulletproof date validation routine. Meg Senior Member Posts: 480 Threads: 24 Joined: Mar 2003 10-17-2004, 08:49 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))``` Moneo Posting Freak Posts: 1,956 Threads: 65 Joined: Jun 2003 10-18-2004, 05:16 AM 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. ***** Meg Senior Member Posts: 480 Threads: 24 Joined: Mar 2003 10-18-2004, 10:13 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. ToohTooh Junior Member Posts: 24 Threads: 1 Joined: Jun 2004 10-23-2004, 12:18 AM 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... Don't interrupt me while I'm interrupting." - Winston S. Churchill Meg Senior Member Posts: 480 Threads: 24 Joined: Mar 2003 10-23-2004, 01:34 AM 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. oracle Posting Freak Posts: 3,343 Threads: 83 Joined: Mar 2003 10-23-2004, 08:06 AM 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). size=9]Oracle QBNZ | GeSHi - Generic Syntax Highlighter for PHP | PHPClasses[/size] Moneo Posting Freak Posts: 1,956 Threads: 65 Joined: Jun 2003 10-24-2004, 06:38 AM 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". ***** Neo Posting Freak Posts: 1,845 Threads: 44 Joined: Aug 2002 10-24-2004, 03:22 PM 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 (I've tested it numerous times and it seems to work ) 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 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 [/syntax] Also, if you like to see what the short version looks like (the unclear one): [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] ToohTooh Junior Member Posts: 24 Threads: 1 Joined: Jun 2004 10-25-2004, 01:15 PM 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 implementCode:`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. Don't interrupt me while I'm interrupting." - Winston S. Churchill oracle Posting Freak Posts: 3,343 Threads: 83 Joined: Mar 2003 10-26-2004, 04:35 AM 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! size=9]Oracle QBNZ | GeSHi - Generic Syntax Highlighter for PHP | PHPClasses[/size] « Next Oldest | Next Newest »