2

I'm trying to create a program which asks for a 13-16 digit credit card number, and re-prompts if the user enters non-numerical values. So far my program works when I enter 16 digit values; however, it re-prompts if I enter anything less.

How do I get my program to accept a minimum of 13 digits and max of 16 digits?

#include <stdio.h>
#include <cs50.h>
#include <stdlib.h>

int main(void)
{
    long cn;
    char buf[18], *endptr;

    do
    {
        printf("card number please: ");
        fgets(buf, 17, stdin);
        cn = strtol(buf, &endptr, 0);
    } while (*endptr != '\0' || endptr == buf);

    printf("%ld\n", cn);
}
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Patrick Dankyi
  • 99
  • 1
  • 1
  • 7
  • 1
    The `fgets` function retains the new-line character `'\n'` at the end of the line. It also reads at most one character less as speicified, truncating the input if it doesn't fit. That means that for a short number, the chacater after the number is `'\n'`, not `'\0'`. The exception is a sixteen-digit number without surrounding spaces: Here, the `'\n'` doesn't fit and is not included in the string. – M Oehm Apr 13 '20 at 16:21
  • (The buffer length passed to `fgets` includes the space for the null terminator, so you could `fgets(buf, 18, stdin)` or, peraps even better `fgets(buf, sizeof(buf), stdin)`. You could also make the buffer much larger and accept additional spaces and hyphens, because people tend to format their cc numbers inti four-digit chunks.) – M Oehm Apr 13 '20 at 16:23
  • Aa an aside: If you use `strtol` to check whether the input is a valid number, you should use base 10 explicitly. Otherwise, you might be surprised if you have a cc number with a leading zero and with 8's and 9's in it ... – M Oehm Apr 13 '20 at 16:26
  • `ptrdiff_t d; ... while( (d = endptr - buf) < 17 && d > 12 ... )` – William Pursell Apr 13 '20 at 16:46

2 Answers2

2

You can use strcspn to locate the end of the char array in the while condition, you can return the size of the char array till it finds '\0' (or '\n' since you don't seem to be removing it from the array), you can then use this to set a minimum size for the input.

#include <string.h>
//...
do{
//...
while (strcspn(buf, "") < 14); //less than 13 chars will repeat the cycle
//...

You could also remove the '\n' from buf, in which case you can use strlen:

//...
do{
    fgets(buf, 17, stdin);
    buf[strcspn(buf, "\n")] = '\0';
while (strlen(buf) < 13); //by removing '\n' you can use strlen
//... 

The max size of buf will be 16 because you limit the size in fgets to 17, so it will store 16 characters plus the null-terminator, you can reduce the size of your array to 17, since the 18th element is useless.

Also be aware that long is not always 8 bytes, https://en.wikibooks.org/wiki/C_Programming/limits.h

This being said, there is an inherit problem with the code, it consumes everything, aphabetic caracters, spaces, etc., so if you have an input of 1234 1234 1234 1234, only 1234 will be converted by strtol.

In theexample below I'm removing everything that is not a digit and maintaining all your other specs:

Running sample

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

int main()
{
    long long cn; //long long is never smaller than 8 bytes
    int i, c;
    char buf[17];

    do {
        i = 0;
        printf("card number please: ");
        while ((c = fgetc(stdin)) != '\n' && c != EOF && strlen(buf) < 17) //max size 16
        {
            if (isdigit(c)) //only digits
                buf[i++] = c;
        }
        buf[i] = '\0';
    } while (strlen(buf) < 13); //min size 13

    cn = strtoll(buf, NULL, 10); //strtoll for long long

    printf("%lld\n", cn);
}
anastaciu
  • 23,467
  • 7
  • 28
  • 53
  • `"\0\n"` should be working the same as `""`. And `strcspn(buf, "")` should be just `strlen(buf)`. – KamilCuk Apr 13 '20 at 20:30
  • @KamilCuk, yes it's the same, only less explicit, `strlen()` can work but we need to remove `'\n'` from `buf`. I edited the answer to address this. – anastaciu Apr 13 '20 at 20:55
  • I am sorry, I do not understand what is "explicit" about it. `"\0\n"` - the `\n` and terminating zero byte are just ignored and wasting memory. The `\0\n` could work if it would be `memcspn` function that could deal with zero bytes inside the memory. – KamilCuk Apr 13 '20 at 20:57
  • 1
    @KamilCuk, explicit in the sense that it shows what we are looking for, but I see your point. – anastaciu Apr 13 '20 at 21:02
  • Note that `strcspn(buf, "")` is (probably) slower than `strlen(buf)` but produces the same result. There's an outside chance the optimizer has been taught the equivalence, but it's more likely not an optimization it makes. – Jonathan Leffler Apr 16 '20 at 03:58
  • @JonathanLeffler, that seems about right, note that if there is a `'\n'` in the string, `strlen(buf)` will count it. – anastaciu Apr 16 '20 at 09:25
1

If we're specifically talking about credit card numbers, you might want to enforce it only by the number of digits the user enters and not the number of total characters since credit card numbers are often entered with spaces or dashes between them.

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int numberofdigits(char *str)
{
    int i = 0, n = 0;
    while(str[i])
    {
        n += !!(isdigit(str[i]));
        i++;
    }

    return n;
}

int main()
{
    char str[100];
    int digits; 

    do
    {
        printf("Enter a credit card number: ");
        fgets(str, 100, stdin);

        digits = numberofdigits(str);
        printf("Read %d digits.\n", digits);
    }
    while((digits < 13) || (digits > 16));

    printf("Thanks for entering \"%s\"\n", str);

    return 0;
}
Govind Parmar
  • 20,656
  • 7
  • 53
  • 85