Add frequent job changes detection (informational flag)
- Add CheckFrequentJobChanges() to detect short tenure patterns - Flag when 3+ employers have <18 month tenure (no penalty, info only) - Group multiple roles at same company/group together - Calculate and display average tenure across all employers - Add NormalizeCompanyForGrouping() to handle company name variations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -341,6 +341,9 @@ public sealed class ProcessCVCheckJob
|
||||
// Check for early career senior roles (relative to education end date)
|
||||
CheckEarlyCareerSeniorRoles(cvData.Employment, cvData.Education, flags, ref score);
|
||||
|
||||
// Check for frequent job changes (informational only)
|
||||
CheckFrequentJobChanges(cvData.Employment, flags);
|
||||
|
||||
// Penalty for diploma mills (critical)
|
||||
foreach (var edu in educationResults.Where(e => e.IsDiplomaMill))
|
||||
{
|
||||
@@ -633,6 +636,127 @@ public sealed class ProcessCVCheckJob
|
||||
}
|
||||
}
|
||||
|
||||
private const int ShortTenureMonths = 18;
|
||||
private const int MinShortTenuresForFlag = 3;
|
||||
|
||||
private static void CheckFrequentJobChanges(
|
||||
List<EmploymentEntry> employment,
|
||||
List<FlagResult> flags)
|
||||
{
|
||||
// Group employment by normalized company name (to combine roles at same employer)
|
||||
var employerTenures = employment
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.CompanyName) && e.StartDate.HasValue)
|
||||
.Where(e => !IsFreelance(e.CompanyName)) // Exclude freelance
|
||||
.GroupBy(e => NormalizeCompanyForGrouping(e.CompanyName))
|
||||
.Select(g =>
|
||||
{
|
||||
// Calculate total tenure at this employer (sum of all roles)
|
||||
var totalMonths = 0;
|
||||
foreach (var role in g)
|
||||
{
|
||||
if (role.StartDate.HasValue)
|
||||
{
|
||||
var endDate = role.EndDate ?? DateOnly.FromDateTime(DateTime.Today);
|
||||
var months = ((endDate.Year - role.StartDate.Value.Year) * 12) +
|
||||
(endDate.Month - role.StartDate.Value.Month);
|
||||
totalMonths += Math.Max(0, months);
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
CompanyGroup = g.Key,
|
||||
DisplayName = g.First().CompanyName,
|
||||
TotalMonths = totalMonths,
|
||||
RoleCount = g.Count()
|
||||
};
|
||||
})
|
||||
.Where(t => t.TotalMonths > 0) // Exclude zero-tenure entries
|
||||
.ToList();
|
||||
|
||||
if (employerTenures.Count == 0) return;
|
||||
|
||||
// Find short tenures (less than 18 months) at different companies
|
||||
var shortTenures = employerTenures
|
||||
.Where(t => t.TotalMonths < ShortTenureMonths)
|
||||
.ToList();
|
||||
|
||||
// Calculate average tenure across unique employers
|
||||
var avgTenureMonths = employerTenures.Average(t => t.TotalMonths);
|
||||
var avgTenureYears = avgTenureMonths / 12.0;
|
||||
|
||||
// If 3+ different companies with short tenure, flag it (informational only)
|
||||
if (shortTenures.Count >= MinShortTenuresForFlag)
|
||||
{
|
||||
var shortTenureCompanies = string.Join(", ", shortTenures.Take(5).Select(t => $"{t.DisplayName} ({t.TotalMonths}mo)"));
|
||||
var moreCount = shortTenures.Count > 5 ? $" and {shortTenures.Count - 5} more" : "";
|
||||
|
||||
flags.Add(new FlagResult
|
||||
{
|
||||
Category = FlagCategory.Timeline.ToString(),
|
||||
Severity = FlagSeverity.Info.ToString(),
|
||||
Title = "Frequent Job Changes",
|
||||
Description = $"Candidate has {shortTenures.Count} employers with tenure under {ShortTenureMonths} months: {shortTenureCompanies}{moreCount}. Average tenure: {avgTenureYears:F1} years across {employerTenures.Count} employers.",
|
||||
ScoreImpact = 0 // Informational only, no penalty
|
||||
});
|
||||
}
|
||||
// Even without frequent changes, note average tenure if it's low
|
||||
else if (avgTenureMonths < 24 && employerTenures.Count >= 3)
|
||||
{
|
||||
flags.Add(new FlagResult
|
||||
{
|
||||
Category = FlagCategory.Timeline.ToString(),
|
||||
Severity = FlagSeverity.Info.ToString(),
|
||||
Title = "Average Tenure",
|
||||
Description = $"Average tenure: {avgTenureYears:F1} years across {employerTenures.Count} employers.",
|
||||
ScoreImpact = 0 // Informational only
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes company name for grouping purposes.
|
||||
/// Groups companies like "BMW UK", "BMW Group", "BMW (UK) Ltd" together.
|
||||
/// </summary>
|
||||
private static string NormalizeCompanyForGrouping(string companyName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(companyName)) return "";
|
||||
|
||||
var name = companyName.ToLowerInvariant().Trim();
|
||||
|
||||
// Remove common suffixes
|
||||
var suffixes = new[] { " limited", " ltd", " plc", " llp", " inc", " corporation", " corp",
|
||||
" uk", " (uk)", " u.k.", " group", " holdings", " services" };
|
||||
foreach (var suffix in suffixes)
|
||||
{
|
||||
if (name.EndsWith(suffix))
|
||||
{
|
||||
name = name[..^suffix.Length].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove parenthetical content
|
||||
name = System.Text.RegularExpressions.Regex.Replace(name, @"\([^)]*\)", "").Trim();
|
||||
|
||||
// Take first significant word(s) as the company identifier
|
||||
// This helps group "Unilever Bestfood" with "Unilever UK"
|
||||
var words = name.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (words.Length >= 1)
|
||||
{
|
||||
// Use first word if it's substantial (4+ chars), or first two words
|
||||
if (words[0].Length >= 4)
|
||||
{
|
||||
return words[0];
|
||||
}
|
||||
else if (words.Length >= 2)
|
||||
{
|
||||
return words[0] + " " + words[1];
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static int GetSeniorityLevel(string? jobTitle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jobTitle)) return 0;
|
||||
|
||||
Reference in New Issue
Block a user