Password Checker C#

I recently refactored this project using LINQ.

using System;
using System.Linq;

namespace PasswordChecker
{
  class Program
  {
    public static void Main(string[] args)
    {
      //Let’s start by defining all of the standards for a password.
      int minLength = 8;

      //string lowercase = "qwertyuiopasdfghjklzxcvbnm";
      //string uppercase = "QWERTYUIOPASDFGHJKLZXCVBNM";
      //string digits = "0123456789";
      //string specialChars = "!@#$%^&*()_+-=[]|{};:<>?,.";
     
      //Ask the user to enter a password and capture their input in a variable.
      Console.WriteLine("Enter a password.");
      string password = Console.ReadLine();
			
      //Make boolean so we can use in if statements later on for validation 
      bool containsAtLeastOneUppercase = password.Any(char.IsUpper);			
      bool containsAtLeastOneLowercase = password.Any(char.IsLower);			
      bool containsAtLeastOneSpecialChar = password.Any(ch => ! Char.IsLetterOrDigit(ch));      
      bool containsAtLeastOneDigit = password.Any(char.IsDigit);			      
      
      //Define a variable score to hold their score and set it to 0.
      int score = 0;
      
      //If the password is greater than or equal to the minimum length, add a point to the score.
      if (password.Length >= minLength)
      {
        score++;
        Console.WriteLine("Minimum Length test passed.");
      }
      else 
      {
      	Console.WriteLine("Minimum Length test failed.");
      }
      
      //If the password contains uppercase letters, add a point.
      //if (Tools.Contains(password, uppercase))
      if (containsAtLeastOneUppercase)      
      {
        score++;
        Console.WriteLine("Uppercase test passed.");
      }
      else 
      {
      	Console.WriteLine("Uppercase test failed.");
      }
      
      //If the password contains lowercase letters, add a point.
      //if (Tools.Contains(password, lowercase))
      if (containsAtLeastOneLowercase)			
      {
        score++;
        Console.WriteLine("Lowercase test passed.");
      }
      else 
      {
      	Console.WriteLine("Lowercase test failed.");
      }
      
      //If the password contains digits, add a point.
      //if (Tools.Contains(password, digits))
      if (containsAtLeastOneDigit)			
			{
        score++;
        Console.WriteLine("Digits test passed.");
      }
      else 
      {
      	Console.WriteLine("Digits test failed.");
      }
      
      //If the password contains special characters, add a point.
      //if (Tools.Contains(password, specialChars))
      if (containsAtLeastOneSpecialChar)			
      {
        score++;
        Console.WriteLine("Special Characters test passed.");
      }
      else 
      {
      	Console.WriteLine("Special Characters test failed.");
      }
      
      //Print the score to the console
      Console.WriteLine($"Final Score: {score}");
    
      switch (score)
      {
      	case 5: case 4: Console.WriteLine("Password strength is extremely strong."); break;
      	case 3: Console.WriteLine("Password strength is strong."); break;
      	case 2: Console.WriteLine("Password strength is medium."); break;
      	case 1: Console.WriteLine("Password strength is weak."); break;
      	case 0: Console.WriteLine("Password strength is crap."); break;
      	default: Console.WriteLine("ERROR!"); break;
      }
    }
  }
}

What if you stuck them in a list, ran them all, and got the sum?

after some copy-pasting work, this is the unique stuff:

("Minimum Length"    , password.Length >= minLength)
("Uppercase"         , password.Any(char.IsUpper))
("Lowercase"         , password.Any(char.IsLower))
("Digits"            , password.Any(ch => ! Char.IsLetterOrDigit(ch)))
("Special Characters", password.Any(char.IsDigit))

I came up with this.

using System;
using System.Linq;

namespace PasswordChecker {
class Program {
    public static void Main(string[] args) {
        (Func<string, bool>, string)[] factors = {
            (p => p.Length >= 8, "Minimum Length"),
            (p => p.Any(char.IsUpper), "Uppercase"),
            (p => p.Any(char.IsLower), "Lowercase"),
            (p => p.Any(ch => !char.IsLetterOrDigit(ch)), "Special Characters"),
            (p => p.Any(char.IsDigit), "Digits"),
        };
        Console.WriteLine("Enter a password.");
        string password = Console.ReadLine();
        int total = 0;
        foreach (var (f, s) in factors) {
            if (f(password)) {
                Console.WriteLine(s + " test passed.");
                total += 1;
            }
            else {
                Console.WriteLine(s + " test failed.");
            }
        }
    }
}
}

I have no clue what I’m doing with C#, so the below code is what’s really going on in my head, with C# I’m just trying to get something like it to compile.

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module PasswordChecker where

import           Data.Char
import qualified Data.Text                     as Text
import           Protolude

main :: IO ()
main = do
  let factors =
        [ ((>= 8) . length       , "Minimum Length")
        , (any isUpper           , "Uppercase")
        , (any isLower           , "Lowercase")
        , (any (not . isAlphaNum), "Special Characters")
        , (any isDigit           , "Digits")
        ]
  putText "Enter a password."
  password     <- Text.unpack <$> getLine
  score :: Int <- map sum $ forM factors $ \(f, s) -> do
    if f password
      then putText (s <> " test passed.") $> 1
      else putText (s <> " test failed.") $> 0
  print score

C#'s foreach loop was at first done with linq (select and sum)

int score = factors.Select(runFactors(password)).Sum();

but that got really tortured (because there needed to be a large function signature to satisfy the compiler). The haskell version is not a for-loop, but looks so much like one that it was clear that C# should use foreach.

1 Like
        (Func<string, bool>, string)[] factors = {
            (p => p.Length >= 8, "Minimum Length"),
            (p => p.Any(char.IsUpper), "Uppercase"),
            (p => p.Any(char.IsLower), "Lowercase"),
            (p => p.Any(ch => !char.IsLetterOrDigit(ch)), "Special Characters"),
            (p => p.Any(char.IsDigit), "Digits"),
        };

If that compiles and is functional then I would say that be a great idea to do instead.

        foreach (var (f, s) in factors) {

I am unaware that this can be done. Is that like a key/value check?

It compiles. Probably need a recentish compiler since it uses tuple literals.
Though, now that I revisit it, there’s no particular reason why the evaluation needs be delayed, so this would be an improvement:
(and is pretty much what you already have, all you’re missing is sticking them into some kind of list)

        (bool, string)[] factors = {
            (password.Length >= 8, "Minimum Length"),
            (password.Any(char.IsUpper), "Uppercase"),
            (password.Any(char.IsLower), "Lowercase"),
            (password.Any(ch => !char.IsLetterOrDigit(ch)), "Special Characters"),
            (password.Any(char.IsDigit), "Digits"),
        };

I don’t have key-value pairs anywhere. Just pairs. Tuples.

var (a, b) = (3, 4);

Tuples are new to me. This refactor definitely fits the DRY method versus my revision. Much more concise as well. Once you understand ** (bool, string) factors ** it all makes sense.