A Unity project demonstrating a gig selection algorithm

My Algorithm for Picking Freelance Gigs Online

What’s the coolest thing about being a software guy and doing freelance gigs online for high-end customers? You can always write your own code! All of it! No project managers, no SCRUM masters, no clients capable of doing white box testing, no code reviewers of any kind at all … You’re in charge of every single code line; this means the general framework, the substance scripts (e.g., business logic kind of things), unit tests, Unit(y) tests (hahah, Unity3D is still my favorite stack after all these years).

Of course, you’ll also have full responsibility for every single line, too, which should never be taken lightly. As I showed earlier, too many devs fall short of taking complete accountability in their freelance software development.

This time, let’s focus on the funny side (as none of the previous articles managed to do it). Being a guy who loves all the coding stuff, I thought the other day why not write my own code on how to select freelance gigs! It should be entirely possible since I have a systematic approach. You’re going to need one if you’re into this for the long term.

There are only two channels for getting freelance gigs: IRL and online. The IRL part is way too complicated to be described here. It’s art. You’d need to be a real code wizard to debug it! 😉 The online channel is far easier to divide into individual repeatable steps. My story here is somewhat limited, unfortunately, as I practically stopped applying for jobs years ago. It turned out that once I got to the top in a freelance site’s keyword ranking, the clients started asking for me.

Picking the best freelance gigs online worked out great... which was nice.

“… uncle died and unexpectedly left me all his yachts … which was nice.” Getting hundreds of invitations from clients feels the same. Then, the only problem is how to pick your “yacht”?

Before digging into this, you might want to take a look at the strangest invitations I ever got as it clarifies the extent of my experiences on freelancing sites as well as the need for the algorithm that I’m about to present here.

Now, coding time!

C# is my all-time favorite programming language, and Unity, my favorite stack. So we’ll go with these. This is my ProjectPicker.cs script for any new version of Unity Editor.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class ClientProfile
{
    public string firstName;
    public string lastName;    
    public int projectsPaidTotal;
}

[Serializable]
public class Project
{
    public ClientProfile client;
    public string description;
    public int durationInDays;
    public string startDate;
    public int totalBudget;
    public int invitationsSent;

    public DateTime GetStartDate()
    {
        return DateTime.Parse(startDate);
    }
}

public class ProjectPicker : MonoBehaviour
{
    [Tooltip("Define all your project invitations in this list")]
    public List<Project> invitedProjects = new List<Project>();

    [Tooltip("Define all your current projects in your backlog in this list")]
    public List<Project> myProjectBacklog = new List<Project>();

    [Tooltip("Set your keyword whitelist")]
    public List<string> keywords = new List<string>();

    private const string msgString1 = "Dear {name}, thank you for your invitation.";
    private const string msgString2 = "Thank you for your understanding. I hope it works out.";

    private const string errNoCanDo = "Too busy on other projects, sorry.";
    private const string errNoMatch = "Unfortunately the project does not match with my skillset.";
    private const string errNoExp = "Unfortunately I rarely work with clients who have little experience using this site.";
    private const string errLowBudget = "Due to excessive amount of invitations I've started to auto-reject all projects below $5,000.";
    private const string errSmall = "Unfortunately I have to prioritize larger projects at this moment.";

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Evaluating " + invitedProjects.Count + " projects ...");
        
        List <Project> acceptableProjects = new List<Project>();

        foreach (Project invitedProject in invitedProjects)
        {
            try
            {
                if (ChooseProject(invitedProject))
                {
                    acceptableProjects.Add(invitedProject);
                }
            }
            catch(Exception e)
            {
                Debug.LogError("Project '" + invitedProject.description + "' rejected: " + e.Message);
            }
        }

        int biggestProjectBudget = 0;
        Project bestProject = null;

        foreach (Project acceptedProject in acceptableProjects)
        {
            if(acceptedProject.totalBudget > biggestProjectBudget)
            {
                biggestProjectBudget = acceptedProject.totalBudget;
                bestProject = acceptedProject;
            }
        }

        if (bestProject == null)
        {
            Debug.Log("Business as usual, not a single good project found at this moment. Better to keep waiting.");
        }
        else
        {
            Debug.Log("Fantastic! You got a new project invitation that actually makes sense: '" + bestProject.description + "' for " + bestProject.client.firstName +
                " " + bestProject.client.lastName + " worth at least " + bestProject.totalBudget + ". Go tiger!");
        }
    }

    private bool ChooseProject(Project proposedProject)
    {
        if (proposedProject.GetStartDate() < CountETA(myProjectBacklog))
        {
            throw new StackOverflowException(FormErrorMessage(proposedProject.client, errNoCanDo));
        }
        else if(!ContainsKeywords(proposedProject, keywords))
        {
            throw new ArgumentException(FormErrorMessage(proposedProject.client, errNoMatch));
        }
        else if (proposedProject.client.projectsPaidTotal < 10000 || proposedProject.invitationsSent > 50)
        {
            throw new EntryPointNotFoundException(FormErrorMessage(proposedProject.client, errNoExp));
        }
        else if (proposedProject.totalBudget < 5000)
        {
            throw new ArgumentException(FormErrorMessage(proposedProject.client, errLowBudget));
        }
        else if (proposedProject.durationInDays < 3)
        {
            throw new InvalidOperationException(FormErrorMessage(proposedProject.client, errSmall));
        }
        else
        {
            return true;
        }
    }

    private DateTime CountETA(List<Project> projects)
    {
        try
        {
            int totalDaysRemaining = 0;

            foreach (Project project in projects)
            {
                DateTime endDate = project.GetStartDate().AddDays(project.durationInDays);

                if (endDate > DateTime.Now)
                {
                    int daysLeftNow = endDate.Subtract(DateTime.Now).Days;

                    totalDaysRemaining += daysLeftNow;
                }
            }

            return DateTime.Now.AddDays(totalDaysRemaining);
        }
        catch(Exception)
        {
        }
        return DateTime.Now;
    }

    private bool ContainsKeywords(Project project, List<string> acceptableKeywords)
    {
        foreach(string keyword in acceptableKeywords)
        {
            if(project.description.ToUpper().Contains(keyword.ToUpper()))
            {
                return true;
            }
        }
        return false;
    }

    private string FormErrorMessage(ClientProfile client, string mainMessage)
    {
        string finalString = msgString1.Replace("{name}", client.firstName);
        finalString = finalString + " " + mainMessage;
        finalString = finalString + " " + msgString2;
        return finalString;
    }
}

Oh, I forgot the comments! No good deed goes unpunished; no good code goes uncommented. I’d better put something good and helpful. As the #2 commandment of programming states, “thou shalt not write obvious comments in the code.” In a typical case where I don’t have (m)any others to read the code, my comments tend to be in the form of notes to self, usually explaining the purpose of something non-trivial. This is how I’d put it, in this context:

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class ClientProfile
{
    public string firstName;
    public string lastName;    
    public int projectsPaidTotal;
}

[Serializable]
public class Project
{
    public ClientProfile client;
    public string description;
    public int durationInDays;
    public string startDate;
    public int totalBudget;
    public int invitationsSent;

    public DateTime GetStartDate()
    {
        return DateTime.Parse(startDate);
    }
}

public class ProjectPicker : MonoBehaviour
{
    [Tooltip("Define all your project invitations in this list")]
    public List<Project> invitedProjects = new List<Project>();

    [Tooltip("Define all your current projects in your backlog in this list")]
    public List<Project> myProjectBacklog = new List<Project>();

    [Tooltip("Set your keyword whitelist")]
    public List<string> keywords = new List<string>();

    // Polite but to the point is the best approach. Never waste anyone's time.
    private const string msgString1 = "Dear {name}, thank you for your invitation.";
    private const string msgString2 = "Thank you for your understanding. I hope it works out.";

    private const string errNoCanDo = "Too busy on other projects, sorry.";
    private const string errNoMatch = "Unfortunately the project does not match with my skillset.";
    private const string errNoExp = "Unfortunately I rarely work with clients who have little experience using this site.";
    private const string errLowBudget = "Due to excessive amount of invitations I've started to auto-reject all projects below $5,000.";
    private const string errSmall = "Unfortunately I have to prioritize larger projects at this moment.";

    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Evaluating " + invitedProjects.Count + " projects ...");
        
        List <Project> acceptableProjects = new List<Project>();

        // Step 1: Loop through all project invitations and pick the suitable ones
        foreach (Project invitedProject in invitedProjects)
        {
            try
            {
                if (ChooseProject(invitedProject))
                {
                    acceptableProjects.Add(invitedProject);
                }
            }
            catch(Exception e)
            {
                Debug.LogError("Project '" + invitedProject.description + "' rejected: " + e.Message);
            }
        }

        // Step 2: Choose the biggest project among the acceptable projects.
        // It is not always true that the biggest project is the best one, but in most cases high budget indicates
        // the seriousness of the client. In most cases...
        int biggestProjectBudget = 0;
        Project bestProject = null;

        foreach (Project acceptedProject in acceptableProjects)
        {
            if(acceptedProject.totalBudget > biggestProjectBudget)
            {
                biggestProjectBudget = acceptedProject.totalBudget;
                bestProject = acceptedProject;
            }
        }

        if (bestProject == null)
        {
            Debug.Log("Business as usual, not a single good project found at this moment. Better to keep waiting.");
        }
        else
        {
            Debug.Log("Fantastic! You got a new project invitation that actually makes sense: '" + bestProject.description + "' for " + bestProject.client.firstName +
                " " + bestProject.client.lastName + " worth at least " + bestProject.totalBudget + ". Go tiger!");

            /*
             * |||||      ______
             *      \    /  ||||
             *     _ \  / _ ||||
             *        \/    ||||
             *        /\    ||||
             *       }  {   ||||
             *       \   \__||||
             *   -----      ||||
             *   
             */
        }
    }

    private bool ChooseProject(Project proposedProject)
    {
        // An obvious comment: this functions return true for projects that pass through the qualification filter checks.
        if (proposedProject.GetStartDate() < CountETA(myProjectBacklog))
        {
            // The usual case is that I just don't have the time for anything new. Could add to the backlog but unfortunately
            // when working on freelancing sites timing is often critical and clients are unwilling to wait for you to become available.
            // Some do, though!
            throw new StackOverflowException(FormErrorMessage(proposedProject.client, errNoCanDo));
        }
        else if(!ContainsKeywords(proposedProject, keywords))
        {
            // Basically, if there core keywords do not match it is often the case that the project is simply a bad match
            // or that the client really has no clue about what the keywords are. Some, very few, cases of the latter option
            // worked out for me in the past.
            throw new ArgumentException(FormErrorMessage(proposedProject.client, errNoMatch));
        }
        else if (proposedProject.client.projectsPaidTotal < 10000 || proposedProject.invitationsSent > 50)
        {
            // Freelancing sites are self-organizing entities. Great freelancers get to work with great clients.
            // The rest get to settle to the casual clients which increases the risk of getting things mixed up
            // at some point in the project.
            throw new EntryPointNotFoundException(FormErrorMessage(proposedProject.client, errNoExp));
        }
        else if (proposedProject.totalBudget < 5000)
        {
            // I never go for small projects anymore. I started with them, of course, just like everyone else.
            // Nobody gets to start from the top.
            throw new ArgumentException(FormErrorMessage(proposedProject.client, errLowBudget));
        }
        else if (proposedProject.durationInDays < 3)
        {
            // Three days work is the minimum I can do. Anything smaller would take too much time to discuss and start
            // compared to the actual billable hours. On the other hand, if you do projects that you can wrap in 1-2 days
            // and you have a good well-defined and very specific offering, this could work out.
            throw new InvalidOperationException(FormErrorMessage(proposedProject.client, errSmall));
        }
        else
        {
            // ToDo: Add check for calculating the hourly rate so you can make ridiculous profit! Money, money, money!!!
            return true;
        }
    }

    private DateTime CountETA(List<Project> projects)
    {
        try
        {
            int totalDaysRemaining = 0;

            foreach (Project project in projects)
            {
                DateTime endDate = project.GetStartDate().AddDays(project.durationInDays);

                if (endDate > DateTime.Now)
                {
                    int daysLeftNow = endDate.Subtract(DateTime.Now).Days;

                    totalDaysRemaining += daysLeftNow;
                }
            }

            // The assumption here is that all projects will be started on time but ended whenever the work is completed.
            // Remember to communicate this to all clients!
            return DateTime.Now.AddDays(totalDaysRemaining);
        }
        catch(Exception)
        {
            // This is not supposed to happen in real. It never does!
        }
        return DateTime.Now;
    }

    private bool ContainsKeywords(Project project, List<string> acceptableKeywords)
    {
        foreach(string keyword in acceptableKeywords)
        {
            if(project.description.ToUpper().Contains(keyword.ToUpper()))
            {
                return true;
            }
        }
        return false;
    }

    private string FormErrorMessage(ClientProfile client, string mainMessage)
    {
        string finalString = msgString1.Replace("{name}", client.firstName);
        finalString = finalString + " " + mainMessage;
        finalString = finalString + " " + msgString2;
        return finalString;
    }
}

Looks smart, right? But, I think it is a manifestation of how to screw your own code. Moreover, I think my code is violating all of the commandments mentioned above! In a typical case, I’d never do that; but once you know the rules, you can break the rules.

When you know the rules, you can break the rules … all at once! But first, you need to know the rules.

I think I made more than one mistake here. But, as we always say: “it compiles, therefore, it works!” Yes, it does compile, and yes, it also works. Add this class as a component to your Unity scene, set all the parameters, and you’re ready to go.

It actually works, try it out!

Anyway, let’s review my code against Paul Seal’s ten commandments. Here we go.

Violation of commandment #1: My variables msgString1 and msgString2 are inappropriately named. I should change them to be more descriptive.

Violation of commandment #2: Start of function ChooseProject()includes an obvious comment. Literally.

Violation of commandment #3: I think my exhibitionist side took over subconsciously and smuggled in that comment at the very end of ChooseProject()!

Violation of commandment #4: There is far too much copy-paste all over in the function FormErrorMessage(). You’d only need one line to do the same.

Violation of commandment #7: System.Collections is an unnecessary dependency, but at least there’s no jQuery!

Violation of commandment #8: The end of Start() looks like having very heavy coupling! 😛

Violation of commandment #9: My code sucks and swallows in theCountETA() function.

Violation of commandment #10: I couldn’t possibly make enough excuses to get away with all the magic numbers. I should have a preference file for listing all the parameter thresholds. Moreover, I fully admit I could catch more exceptions in case of having invalid parameters, but because of my excessive use of magic numbers, this will never happen in practice (which what we devs love to say, right?).

There are far more violations to be marked before I could pass this piece of code through my code review process:

  • Apologies for using strings embedded in the code instead of using resource files for localization.
  • I didn’t add parameter validators to Start().
  • The amount of plain-text comments in ChooseProject() is getting next to ridiculous.
  • There are no copyright or license terms mentioned at the beginning of the file, which means you can steal it with pride!

Did you see me missing anything? You should; I missed violating commandments #5 and #6. I didn’t store any credentials with weak hash algorithms because I didn’t have a login function at all. Also, I didn’t steal anything; every line of code is an authentic spaghetti code of mine.

Without writing it in the file, I do reserve all rights to the above code! It’s mine (but I can grant you an unlimited GNU General Public License upon your CoachLancer registration)!

Happy project picking.