The Object of Your Affection - C#

This is my second trip around C# course here. This is my first refactor of this project.

Programs.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace DatingProfile
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("The Object of Your Affection");
      
      Console.Write("Please enter First Name:");
      string firstName = Console.ReadLine();
      while (String.IsNullOrWhiteSpace(firstName))
      {
        Console.Write("Sorry, didn't understand.  What is your First Name?: ");
        firstName = Console.ReadLine();
      }

      Console.Write("Please enter Last Name:");
      string lastName = Console.ReadLine();
      while (String.IsNullOrWhiteSpace(lastName))
      {
        Console.Write("Sorry, didn't understand.  What is your Last Name?: ");
        firstName = Console.ReadLine();
      }

      Console.Write("Please enter your Age:");
      string age = Console.ReadLine();
      while (String.IsNullOrWhiteSpace(age))
      {
        Console.Write("Sorry, didn't understand.  What is your Age?: ");
        firstName = Console.ReadLine();
      }
			
	int iAge;
	while (!Int32.TryParse(age, out iAge))
	{
          Console.WriteLine("Please respond with a number for your age:");
          Console.Write("Please enter your Age:");
          age = Console.ReadLine();
       }

      Console.Write("Please enter your City:");
      string city = Console.ReadLine();
      while (String.IsNullOrWhiteSpace(city))
      {
        Console.Write("Sorry, didn't understand.  What is your City?: ");
        city = Console.ReadLine();
      }

      Console.Write("Please enter your Country:");
      string country = Console.ReadLine();
      while (String.IsNullOrWhiteSpace(country))
      {
        Console.Write("Sorry, didn't understand.  What is your Country?: ");
        country = Console.ReadLine();
      }

      Console.Write("Please enter your Gender:");
      string gender = Console.ReadLine();
      while (String.IsNullOrWhiteSpace(gender))
      {
        Console.Write("Sorry, what is your Gender?: ");
        gender = Console.ReadLine();
      }

      gender = gender.ToUpper();
      while (gender != "MALE" && gender != "FEMALE")
      {
        Console.Write("Sorry, didn't understand.  What is your Gender?: ");
        gender = Console.ReadLine().ToUpper();
      }
			
      var g = new Profile();
      gender = g.MaleOrFemale(gender);
      
      //Above where you print sams profile information out, add some hobbies to sam: 
      Console.Write("What are your hobbies?:");
      string s = Console.ReadLine();
      string[] hobbys = s.Split(' ').ToList().ToArray(); 
		
      //Instantiate a new Profile object called sam.
      Profile sam = new Profile(firstName, lastName, iAge, city, country, gender);

      //If you call ViewProfile() before calling SetHobbies(), youll get an error because the 
      hobbies field isnt set to any value. 
      //Fix the class so that you can call ViewProfile() without adding hobbies.
      if (hobbys != null) 
      {
        sam.SetHobbies(hobbys);
      }

      //In Main(), test out the new method on sam and print out the result.  Call 
      sam.ViewProfile() and pass the result to Console.WriteLine().
      Console.WriteLine(sam.ViewProfile());
    }
  }
}

Profile.cs

using System;

namespace DatingProfile
{
  class Profile //set up the skeleton of the Profile class.
  {
    //Add the following fields to Profile
    //Add private to all the fields you created in Profile.
    private string firstname;
    private string lastname;
    private int age;
    private string city;
    private string country;
    private string pronouns;
    private string[] hobbies;
  
    /*Users should be able to add their profile information in a constructor.
    Define the constructor and set the fields to the values passed in.
    Use this to differentiate parameters from instance fields.*/
    public Profile(string firstname, string lastname, int age, string city = "", string country = "", string pronouns = "they/them")
    {
      this.firstname = firstname;
      this.lastname = lastname;
      this.age = age;
      this.city = city;
      this.country = country;
      this.pronouns = pronouns;
    }
    
		public Profile()
		{}

    //Define a ViewProfile() method under the constructor.  It should have Public Access, return type of string, no parameters
    public string ViewProfile()
    {
      string bio = ($"Name: {firstname} {lastname}\nAge: {age}\nCity: {city}\nCountry: {country}\nGender: {pronouns}");
     
     //If you call ViewProfile() before calling SetHobbies(), youll get an error because the hobbies field isnt set to any value. 
     //Fix the class so that you can call ViewProfile() without adding hobbies. 
      if (this.hobbies.Length != 0)
      {
        foreach (string s in this.hobbies)
        {
          bio += $"\nHobbies: {s}";
        }
      }
      
      return age < 18 ? "You are not of age to view this profile": bio;
    }

    //Give users a way to set hobbies. Declare a new method SetHobbies() with public access, no return value, a string[] parameter named hobbies. In the method body, set the field this.hobbies equal to the hobbies argument.
    public void SetHobbies(string[] hobbies) => this.hobbies = hobbies;

    public string MaleOrFemale(string s) => s == "MALE" ? "Male" : "Female";
  }
}

That could be a function.

var gender = askFor("Gender")
var country = askFor("Country")
...

That doesn’t account for other kinds of validation… If there are few of them you could have a couple different functions, or you could pass in a function to do the validation:

var gender = prompt(
  "Please enter your gender",
  resp => {"racecar", "spaceship", "train"}.contains(resp.lower())  // or however you write that in C#
)

Or… you could declare fields, something in the direction of:

prompt(
  Profile,
  { ("gender", STRING)
  , ("age", INT)
  }
)

(pass in a constructor and a list of fields that in requires, then you could loop through fields and carry out the questions … or for that matter, it could be a menu where the user chooses a field, enters a value, and so on until it’s complete, prompt could gather the information in any manner, wouldn’t matter, at the end it’d produce a complete person)…might need to wrestle a bit with the type checker to convince it that the correct number of arguments are provided to the constructor when it gets called, probably easier to do:

new Profile(
  prompt("gender", STRING),
  prompt("age", INT)
)

…that’s probably nicer anyway. and definitely easier to get to typecheck. though. this would be asking prompt to have multiple return types, uhmmm well, could pass in a parser function:

(a parser combines validation with conversion, successful parse provides a converted value, failed parse means it’s invalid)

new Profile(
  prompt("gender", parseString),
  prompt("age", parseInt)
)

if the parsers include the desired type in them, then prompt ought to be able adapt its type to match, something in the direction of:

static T prompt<T>(String, Func<String, Result<T>>) {
  ...
}

(I’m making up syntax and types, probably close enough though :P)

presumably a parser would return a result representing either failure (try again) or success (take the value out of the result and return it), a result would behave something like:

var someResult = ...
if (someResult) {
    Console.WriteLine("result is: ", someResult.value)
} else {
    Console.WriteLine("there's no result :(")
}
1 Like

I made a result type… (for indicating parse success/failure)

maybe
class Maybe<T> {
    // A Maybe might be Nothing or Something (val)
    protected Maybe() {} // Can't create a Maybe, no such thing.
}

class Nothing<T> : Maybe<T> {
    // If it's a Nothing... then there's nothing to see.
    public Nothing() {}
}

class Something<T> : Maybe <T> {
    // But if it's a something, then there's a value inside!
    public T val;
    public Something(T val) {
        this.val = val;
    }
}

And some functions that might convert strings to something, and if not something, then nothing.

parse
    static Maybe<int> parseInt(string s) {
        int res;
        bool ok = int.TryParse(s, out res);
        if (ok) {
            return new Something<int>(res);
        }
        return new Nothing<int>();
    }

    static Maybe<string> parseText(string s) {
        if (!String.IsNullOrWhiteSpace(s)) {
            return new Something<string>(s);
        }
        return new Nothing<string>();
    }

    static Maybe<string> parseGender(string s) {
        string[] options = {"MALE", "FEMALE"};
        if (options.Contains(s)) {
            return new Something<string>(s);
        }
        return new Nothing<string>();
    }

And then there’s this, which was repeated over and over:

ask
    static T ask<T>(string prompt, Func<string, Maybe<T>> parse) {
        while (true) {
            Console.WriteLine(prompt);
            string input = Console.ReadLine();
            Maybe<T> result = parse(input);
            if (result is Something<T> something) {
                return something.val;
            }
        }
    }

And then main only needs to describe things from a high-level view:

    static void Main(string[] args)
    {
        Console.WriteLine("The Object of Your Affection");

        Profile sam = new Profile(
            ask("Please enter your FIRST name: ", parseText),
            ask("Please enter your LAST name: ", parseText),
            ask("Please enter your Age: ", parseInt),
            ask("Please enter your city: ", parseText),
            ask("Please enter your country: ", parseText),
            ask("Please enter your gender: ", parseGender)
        );

        Console.Write("What are your hobbies?:");
        sam.SetHobbies(Console.ReadLine().Split(' ').ToList().ToArray());
        Console.WriteLine(sam.ViewProfile());
    }
1 Like

Those are all great ideas with a very readable end result in the main. Gonna take some time for me to grasp all this. Thanks for sharing!

1 Like