Resolved Getting a mobile number from a user

WB1975

Well-known member
Joined
Apr 3, 2020
Messages
87
Programming Experience
Beginner
if i wanted a user to enter a mobile number in a console application
so the user can only input
011 086 087 to start and then 7 digits after that
so as in 0142223434

would it be best done with regular expressions? or is there a better way?
 
Last edited by a moderator:
I know John, I ran your code. I actually like the way you did it. Despite it was slow executing for me. I will rerun it later, as I have a keen investigative mind for looking at things that should work better than they do. I'd like to find what was slowing it down.

That's why I said if you done it all in Regex, the overhead would kill the performance... This is a topic me and Skydiver tirelessly exhausted on another board, by running unit tests to poke for performance issues while using Regex and compared them against Linq and traditional basic code concepts. Obviously Regex failed against the others in terms of performance. But Microsoft are planning and pledged a big release in the coming weeks which will address these issues once and for all. And the aims of the patches will mean that there will no longer be any overhead costs on performance when using Regex.

But if you was to use RegEx in there too. It would be even slower. Using RegEx alone may be quicker, but the three lines of code was taken just as long as my method. Sure, the difference is only in a few ms. But that becomes a problem if you happen to be processing thousands of mobile numbers at once. Not that our OP is doing that. I'm just saying from the point of an analogy.
 
Another variant using the Any and All extensions:
C#:
var input = "111-2345 678"; // = Console.ReadLine()
var remove = " -".ToCharArray();
var prefixes = new string[] { "111", "112", "113" };
Array.ForEach(remove, c => input = input.Replace(c.ToString(), string.Empty));
var valid = input.Length == 10 && input.All(c => char.IsDigit(c)) && prefixes.Any(s => input.StartsWith(s));
 
I think you could improve the speed from 137ms Greatly John. You wrote it very elegantly bar the loops, if you don't mind my criticism?

Coming at post #11 from a performance point only, One line executions with no set conditions would execute at O(0) complexity. But for while and for iterations, they would be O( n ), 'n' equals the number of required loops and this depends if there are conditions to be met. If there are conditions, they would execute as O(1) So, if we needed to iterate 20000 cycles : for (int integer = 0; integer < 20000; integer++). This would accumulate to O(20000) because the iteration cycle would be 20000 per the total iterations. Where this gets slow is when you factor in if additional loops, Then you get to O(n*2) complexity for calling the additional while loop, and this isn't efficient for performance when you add if conditions which add O(1) complexity. This is why I almost always try to avoid nested loops. I'm noticing a performance decrease when I enter into the GetDigit() call.

Ah now you're talking speed. Without running the Linq query integrated with your other example, it runs at 5ms, which is beautiful. Factoring in the Linq changes to your example and I can output 0.25ms. Flawless performance. (y)
 
Last edited:
I had to have another go at this one. Only I've added some extra little bits into it such as backspacing. Mind you, that could probably do with some tweaking. I also added the prefix checking which I overlooked in the last one. :
C#:
    internal static class Program
    {
        internal static void Main()
        {
            Execute();
        }

        internal static void Execute()
        {
            Console.WriteLine("What's your mobile phone number?");
            Number_Builder number_Builder = new Number_Builder();
            StringBuilder numberConstructor = new StringBuilder();
            bool hasPrefix = false;
            while (number_Builder.MobileNumber.Length < number_Builder.ContainsSeparater)
            {
                char Key = Console.ReadKey(true).KeyChar;
                if (" -|".Contains(Key.ToString()))
                {
                    number_Builder.IsSeparated(Key);
                }
                else if (char.IsDigit(Key))
                {
                    numberConstructor = number_Builder.IsNumber(Key, numberConstructor);
                    if (numberConstructor.Length == 3)
                    {
                        hasPrefix = true; number_Builder.MobilePrefix = numberConstructor.ToString().Substring(0, 3);
                    }
                    else hasPrefix = false;
                }
                else if (numberConstructor.Length != 0 && Console.ReadKey().Key == ConsoleKey.Backspace)
                {
                    numberConstructor.Length--;
                }
                if (hasPrefix && !number_Builder.IsAllowed_Prefix())
                {
                    Console.Clear();
                    Console.WriteLine($"The { numberConstructor } network provider is not allowed. We allow { number_Builder.AllowedPrefix[0] }, { number_Builder.AllowedPrefix[1] }, { number_Builder.AllowedPrefix[2] }. Try again.");
                    number_Builder.MobileNumber = string.Empty; numberConstructor.Length = 0; Console.WriteLine("What's your mobile phone number?");
                }
                Console.SetCursorPosition(34, Console.CursorTop - 1);
                Console.WriteLine(numberConstructor);
            }
            number_Builder.MobilePrefix = numberConstructor.ToString().Substring(0, 3);
            StringBuilder number_Constructor = new StringBuilder().Insert(0, string.Concat(number_Builder.MobilePrefix, number_Builder.SeperatorValues, number_Builder.MobileNumber.ToString().Remove(0, 3)));
            number_Builder.MobileNumber = numberConstructor.ToString();
            number_Builder.CosmeticNumber = number_Constructor.ToString();
            int MobileAsInt = Convert.ToInt32(number_Builder.MobileNumber);
            Console.WriteLine($"Prefix number verified : { number_Builder.MobilePrefix }");
            Console.WriteLine($"Mobile number verified : { number_Builder.MobileNumber }");
            Console.WriteLine($"Cosmetic number verified : { number_Builder.CosmeticNumber }");

        }
    }
    public class Number_Builder
    {
        public string SeperatorValues { get; set; } = string.Empty;
        public string MobilePrefix { get; set; } = string.Empty;
        public int ContainsSeparater { get; set; } = 10;
        public string MobileNumber { get; set; } = string.Empty;
        public string CosmeticNumber { get; set; } = string.Empty;
        public string[] AllowedPrefix = { "111", "112", "113" };
        public bool IsAllowed_Prefix()
        {
            return AllowedPrefix.Contains(MobilePrefix);
        }
        public void IsSeparated(char Value_In)
        {
            SeperatorValues = string.Concat(SeperatorValues, $"{Value_In}");
        }
        public StringBuilder IsNumber(char Number_In, StringBuilder num_Constructor)
        {
            MobileNumber = num_Constructor.Append(Number_In.ToString()).ToString();
            return num_Constructor;
        }
    }
Room for improvement. Any takers? :cool: Suggested improvements welcomed also.
 
Nice addition of trying to support backspacing. Unfortunately, the backspacing supports only the happy path when the user is entering digits and attempts backspacing. Unfortunately things don't work too well if the user types in '0' 'A' '1', or '0' <space> '1' .

As I have time later I'll join in all the fun and show my attempt at the interactive validation.
 
It's always fun to turn one of these into a little game. Look forward to what you come up with. I've made some changes already. I should have been reading the Key as ConsoleKeyInfo instead of char. This way I can read both the key on line 36 and chars. Now it should work fine.
New improved changes from post 19:
using System;
using System.Linq;
using System.Text;

namespace TestConsoleApp
{
    internal static class Program
    {
        internal static void Main()
        {
            Execute();
        }

        internal static void Execute()
        {
            Console.WriteLine("What's your mobile phone number?");
            Number_Builder number_Builder = new Number_Builder();
            StringBuilder numberConstructor = new StringBuilder();
            bool hasPrefix = false;
            while (number_Builder.MobileNumber.Length < number_Builder.ContainsSeparater)
            {
                ConsoleKeyInfo Key = Console.ReadKey(true);
                if (" -|".Contains(Key.KeyChar.ToString()))
                {
                    number_Builder.IsSeparated(Key.KeyChar);
                }
                else if (char.IsDigit(Key.KeyChar))
                {
                    numberConstructor = number_Builder.IsNumber(Key.KeyChar, numberConstructor);
                    if (numberConstructor.Length == 3)
                    {
                        hasPrefix = true; number_Builder.MobilePrefix = numberConstructor.ToString().Substring(0, 3);
                    }
                    else hasPrefix = false;
                }
                else if (numberConstructor.Length != 0 && Key.Key == ConsoleKey.Backspace)
                {
                    numberConstructor.Length--;
                }
                if (hasPrefix && !number_Builder.IsAllowed_Prefix())
                {
                    Console.Clear();
                    Console.WriteLine($"The { numberConstructor } network provider is not allowed. We allow { number_Builder.AllowedPrefix[0] }, { number_Builder.AllowedPrefix[1] }, { number_Builder.AllowedPrefix[2] }. Try again.");
                    number_Builder.MobileNumber = string.Empty; numberConstructor.Length = 0; Console.WriteLine("What's your mobile phone number?");
                }
                Console.SetCursorPosition(34, Console.CursorTop - 1);
                Console.WriteLine(numberConstructor);
            }
            number_Builder.MobilePrefix = numberConstructor.ToString().Substring(0, 3);
            StringBuilder number_Constructor = new StringBuilder().Insert(0, string.Concat(number_Builder.MobilePrefix, number_Builder.SeperatorValues, number_Builder.MobileNumber.Remove(0, 3)));
            number_Builder.MobileNumber = numberConstructor.ToString();
            number_Builder.CosmeticNumber = number_Constructor.ToString();
            int MobileAsInt = Convert.ToInt32(number_Builder.MobileNumber);
            Console.WriteLine($"Prefix number verified : { number_Builder.MobilePrefix }");
            Console.WriteLine($"Mobile number verified : { number_Builder.MobileNumber }");
            Console.WriteLine($"Cosmetic number verified : { number_Builder.CosmeticNumber }");

        }
    }
    public class Number_Builder
    {
        public string SeperatorValues { get; set; } = string.Empty;
        public string MobilePrefix { get; set; } = string.Empty;
        public int ContainsSeparater { get; set; } = 10;
        public string MobileNumber { get; set; } = string.Empty;
        public string CosmeticNumber { get; set; } = string.Empty;
        public string[] AllowedPrefix = { "011", "012", "013" };
        public bool IsAllowed_Prefix()
        {
            return AllowedPrefix.Contains(MobilePrefix);
        }
        public void IsSeparated(char Value_In)
        {
            SeperatorValues = string.Concat(SeperatorValues, $"{Value_In}");
        }
        public StringBuilder IsNumber(char Number_In, StringBuilder num_Constructor)
        {
            MobileNumber = num_Constructor.Append(Number_In.ToString()).ToString();
            return num_Constructor;
        }
    }
}
 
Please do!

And also I dont mind seeing solutions using regular code and regex, that really wouldnt hurt.
 
Just joining in on the fun. Currently the code for my ValidatedInput class is working, but the MaskedInput is still in its early phases, but I just wanted to show what it could possibly look like if you uncomment the first line to define USE_MASKED_INPUT.

Anyway, the code shows 3 ways of doing the phone number validation. It took me a while to figure out that validation is actually a separate problem from accepting valid input. And so in my implementation, a "control" that implements the IValidatedInput interface fires an event giving the caller a chance to validate the data enter. I've done the Regex, LINQ, and plain old string inspection approaches of validation. What make this nice is that the validation method can be swapped around easily.

C#:
// #define USE_MASKED_INPUT
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

namespace PhoneNumberValidation
{
    public class ValidatingEventArgs : EventArgs
    {
        public bool IsValid { get; set; }
        public string Message { get; set; }
    }

    interface IValidatedInput
    {
        string Input { get; }
        bool IsValid { get; }

        event EventHandler<ValidatingEventArgs> Validating;

        string ReadInput(string prompt);
    }

    class ValidatedInput : IValidatedInput
    {
        public string Input { get; protected set; }
        public bool IsValid { get; protected set; }

        public event EventHandler<ValidatingEventArgs> Validating;

        public virtual string ReadInput(string prompt)
        {
            while (true)
            {
                Console.Write(prompt);
                Input = Console.ReadLine();
                if (IsInputValid(out string message))
                    return Input;
                Console.WriteLine(message);
            }
        }

        protected virtual bool IsInputValid(out string message)
        {
            var args = new ValidatingEventArgs();
            args.IsValid = true;
            args.Message = String.Empty;
            OnValidating(args);
            message = args.Message;
            IsValid = args.IsValid;
            return args.IsValid;
        }

        protected virtual void OnValidating(ValidatingEventArgs args) => Validating?.Invoke(this, args);
    }

#if USE_MASKED_INPUT
    class ConsoleState : IDisposable
    {
        public int CursorLeft { get; set; }
        public int CursorTop { get; set; }
        public ConsoleColor ForegroundColor { get; set; }
        public ConsoleColor BackgroundColor { get; set; }

        public ConsoleState()
        {
            Capture();
        }

        public void Capture()
        {
            CursorLeft = Console.CursorLeft;
            CursorTop = Console.CursorTop;
            ForegroundColor = Console.ForegroundColor;
            BackgroundColor = Console.BackgroundColor;
        }

        public void Restore()
        {
            Console.CursorLeft = CursorLeft;
            Console.CursorTop = CursorTop;
            Console.ForegroundColor = ForegroundColor;
            Console.BackgroundColor = BackgroundColor;
        }

        public void Dispose()
        {
            Restore();
        }
    }

    class MaskedInput : ValidatedInput
    {
        public string Mask { get; }

        public MaskedInput(string mask)
        {
            Mask = mask;
        }

        public override string ReadInput(string prompt)
        {
            using (var consoleState = new ConsoleState())
            {
                do
                {
                    consoleState.Restore();
                    DrawPrompt(prompt);
                    DrawMask();
                    ReadInputInternal();
                } while (!IsInputValid(out string message));

                consoleState.Capture();
            }
            return Input;
        }

        void DrawPrompt(string prompt) => Console.Write(prompt);

        void DrawMask()
        {
            using (var consoleState = new ConsoleState())
            {
                Console.BackgroundColor = ConsoleColor.Black;
                Console.ForegroundColor = ConsoleColor.DarkGray;
                Console.Write(Mask);

                consoleState.Restore();
                Console.ForegroundColor = ConsoleColor.DarkYellow;
                Console.Write(Input);
            }
        }

        void ReadInputInternal()
        {
            //$ TODO: Implement event for each keyed input to allow live validation.
            //$ TODO: Implement accepting only keys that match the mask.
            //$ TODO: Implement pressing escape.
            //$ TODO: Implement backspacing and arrowing.
            //$ TODO: Implement insert and overwride.

            Input = Console.ReadLine();
        }
    }
#endif  // USE_MASKED_INPUT

    class Program
    {
        static void Main(string[] args)
        {
#if USE_MASKED_INPUT
            var validatedInput = new MaskedInput("999 999 9999");
#else
            var validatedInput = new ValidatedInput();
#endif

            //
            // Uncomment one of the validation options below:
            //

            // validatedInput.Validating += OnValidating_Regex;
            // validatedInput.Validating += OnValidating_Linq;
            validatedInput.Validating += OnValidating_StringOps;

            var phoneNumber = validatedInput.ReadInput("Enter mobile number: ");
            if (validatedInput.IsValid)
                Console.WriteLine("{0} is {1}.", phoneNumber, validatedInput.IsValid ? "valid" : "invalid");
        }

        static void OnValidating_Regex(object obj, ValidatingEventArgs args)
        {
            IValidatedInput validatedInput = (IValidatedInput) obj;
            args.IsValid = Regex.Match(validatedInput.Input, @"11[123](-|\s)*[0-9]{3}(-|\s)*[0-9]{4}").Success;
            args.Message = args.IsValid ? "" : "Must match expected pattern.";
        }

        static string[] ValidPrefixes = new string[] { "111", "112", "113" };

        static void OnValidating_Linq(object obj, ValidatingEventArgs args)
        {
            IValidatedInput validatedInput = (IValidatedInput)obj;
            var input = validatedInput.Input;
            var charArray = input.ToCharArray();
            var digits = charArray.Where(ch => char.IsDigit(ch)).ToList();

            args.IsValid = false;

            args.Message = "Must contain 10 digits.";
            if (digits.Count != 10)
                return;

            args.Message = "Must consist of only digits, whitespace, and dashes.";
            if (!input.ToCharArray().All(ch => char.IsDigit(ch) || char.IsWhiteSpace(ch) || ch == '-'))
                return;

            args.Message = "Prefix must be one of " + string.Join(", ", ValidPrefixes);
            var prefix = digits.Take(3).Aggregate("", (s, ch) => s += ch);
            if (!ValidPrefixes.Contains(prefix))
                return;

            args.Message = "";
            args.IsValid = true;
        }

        static void OnValidating_StringOps(object obj, ValidatingEventArgs args)
        {
            IValidatedInput validatedInput = (IValidatedInput)obj;
            var input = validatedInput.Input;
            string digits = "";

            foreach (char ch in input)
            {
                if (char.IsDigit(ch))
                {
                    digits += ch;
                }
                else if (char.IsWhiteSpace(ch) || ch == '-')
                {
                    // do nothing
                }
                else
                {
                    args.IsValid = false;
                    args.Message = "Must consist of only digits, whitespace, and dashes.";
                    return;
                }
            }

            if (digits.Length != 10)
            {
                args.IsValid = false;
                args.Message = "Must contain 10 digits.";
                return;
            }

            var prefix = digits.Substring(0, 3);
            if (!ValidPrefixes.Contains(prefix))
            {
                args.IsValid = false;
                args.Message = "Prefix must be one of " + string.Join(", ", ValidPrefixes);
            }
        }
    }
}
 
Sure. Ask your questions.

( Sorry, I've not had a chance to get back to finishing the masked input, yet. I should have more free time this week. )
 
How could I implement this as a reusable class? or maybe i should implement it into my ConsoleManager class.
 
On line 153 or 155, an instance of the validating class is instantiated. On line 164, you tell the class to fire an event asking that the input the user has entered be validated. On line 166, you tell the class to prompt the user for input. On line 167, you check to see what the use entered was truly valid (or if the user abandoned entering data).

In your event handler (see one of the OnValidating_* methods), you get called with the string the user has entered so far. You can respond with true or false whether that input is valid or not.

What makes this a reusable class is that you can just now focus on writing the validation check logic and the class takes care of the boilerplate input looping logic. What I also hoped to show with the masked input class, is that the input looping logic can be more sophisticated and take individual keypresses into account and do the cursor location management. That way, it it would be possible to limit the user's input to just some valid keys, and if I get around to the masking logic, some valid keys in particular locations.

As for reusing the class in other projects, you would compile the class(es) into a class library. Your other projects can then reference that assembly (aka DLL).
 
compile the class(es) into a class library. Your other projects can then reference that assembly (aka DLL). - is that difficult to do? Would it be the recommended method of reusing it or is there a better way?
 
Back
Top Bottom