cofounderella/lib/utils/math.dart

291 lines
8.6 KiB
Dart
Raw Normal View History

import 'dart:math';
2024-06-12 00:39:29 +02:00
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
import '../enumerations.dart';
2024-05-30 16:37:34 +02:00
import '../models/user_profile.dart';
/// Approximate determination of age
int calcAge(int? birthYear) {
if (birthYear != null) {
return (DateTime.now().year - birthYear);
}
return 0;
}
/// Returns the approximate age in parentheses,
/// or an empty string if [birthYear] is the current year or null.
String ageInfo(int? birthYear) {
int age = calcAge(birthYear);
String ageInfo = age > 0 ? '($age)' : '';
return ageInfo;
}
///
/// Convert decimal coordinate to degrees minutes seconds (DMS).
///
String convertDecimalToDMS(double decimalValue, {required bool isLatitude}) {
bool isNegative = decimalValue < 0;
double absoluteValue = decimalValue.abs();
int degrees = absoluteValue.toInt();
double minutesDecimal = (absoluteValue - degrees) * 60;
int minutes = minutesDecimal.toInt();
double secondsDecimal = (minutesDecimal - minutes) * 60;
double seconds = double.parse(secondsDecimal.toStringAsFixed(2));
String direction;
if (isLatitude) {
direction = isNegative ? 'S' : 'N';
} else {
direction = isNegative ? 'W' : 'E';
}
// return formatted string
return '${degrees.abs()}° ${minutes.abs()}\' ${seconds.abs().toStringAsFixed(2)}" $direction';
}
///
/// Distance in kilometers between two location coordinates
///
double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
const R = 6371; // earth radius in kilometers
// distance between latitudes and longitudes
final dLat = _degreesToRadians(lat2 - lat1);
final dLon = _degreesToRadians(lon2 - lon1);
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(_degreesToRadians(lat1)) *
cos(_degreesToRadians(lat2)) *
sin(dLon / 2) *
sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
double _degreesToRadians(double degrees) {
return degrees * pi / 180;
}
2024-05-30 16:37:34 +02:00
///
/// Shortest distance between two users locations
///
double shortestDistanceBetweenUsers(
UserProfile currentUser, UserProfile otherUser) {
try {
if (currentUser.locations.isEmpty || otherUser.locations.isEmpty) {
return double.nan;
}
double shortestDistance = double.nan;
// locations currentUser
for (var loc1 in currentUser.locations.values) {
if (loc1 != null && loc1.latitude != null && loc1.longitude != null) {
for (var loc2 in otherUser.locations.values) {
if (loc2 != null && loc2.latitude != null && loc2.longitude != null) {
double distance = calculateDistance(loc1.latitude!, loc1.longitude!,
loc2.latitude!, loc2.longitude!);
if (shortestDistance.isNaN || distance < shortestDistance) {
shortestDistance = distance;
}
}
}
}
}
return shortestDistance;
} catch (e) {
return double.nan;
}
}
2024-06-12 00:39:29 +02:00
/// Calculates the matching score of [otherUser] for [currentUser].
double calculateMatchScore(UserProfile currentUser, UserProfile otherUser) {
// weights
const double distanceWeight = 0.55;
const double skillWeight = 0.25;
const double availabilityWeight = 0.065;
const double visionWeight = 0.04;
const double riskWeight = 0.035;
const double workWeight = 0.025;
const double cultureWeight = 0.02;
const double communicationWeight = 0.015;
if (kDebugMode) {
double weightSum = (distanceWeight +
skillWeight +
availabilityWeight +
visionWeight +
riskWeight +
workWeight +
cultureWeight +
communicationWeight);
if (weightSum != 1) {
debugPrint(
'Warning --> calculateMatchScore : Weights Sum $weightSum != 1');
}
}
// Score on locations distance
double distance = shortestDistanceBetweenUsers(currentUser, otherUser);
double distanceScore = _distanceToPercentage(distance);
// Score on skills
int matchingSkillsSought = currentUser.skillsSought
.toSet()
.intersection(otherUser.skills.toSet())
.length;
int matchingSkillsOffered = otherUser.skills
.toSet()
.intersection(currentUser.skillsSought.toSet())
.length;
int skillsSought = currentUser.skillsSought.length;
int skillsOffered = otherUser.skills.length;
// Idea: Calculate sum of matching skills divided by amount of skills listed.
// As each list can have up to 3 skills max, this gives the following equation
// with 3 skills: 0 1/3 2/3 3/3; with 2 skills: 0 1/2 2/2; with 1 skill: 0 1.
// In total this will result in a total of 5 different states:
// [0], [1/3], [1/2], [2/3], and [1].
double valueSought = matchingSkillsSought / skillsSought;
int scoreSought;
if (valueSought == 1) {
scoreSought = 4;
} else if (valueSought == 2 / 3) {
scoreSought = 3;
} else if (valueSought == 1 / 2) {
scoreSought = 2;
} else if (valueSought == 1 / 3) {
scoreSought = 1;
} else {
scoreSought = 0;
}
double valueOffered = matchingSkillsOffered / skillsOffered;
int scoreOffered;
if (valueOffered == 1) {
scoreOffered = 4;
} else if (valueOffered == 2 / 3) {
scoreOffered = 3;
} else if (valueOffered == 1 / 2) {
scoreOffered = 2;
} else if (valueOffered == 1 / 3) {
scoreOffered = 1;
} else {
scoreOffered = 0;
}
int skillsScore = scoreSought + scoreOffered; // 8 points max
// Score on availability
int availabilityScore;
AvailabilityOption currentAvail = currentUser.availability;
AvailabilityOption otherAvail = otherUser.availability;
if (currentAvail == AvailabilityOption.flexible ||
otherAvail == AvailabilityOption.flexible ||
currentAvail == otherAvail) {
availabilityScore = 3;
} else {
int availabilityDifference = (currentAvail.index - otherAvail.index).abs();
availabilityScore = 3 - availabilityDifference;
}
// Score on Vision
List<VisionOption> currentVisions = currentUser.visions;
List<VisionOption> otherVisions = otherUser.visions;
int matchingVisions =
currentVisions.toSet().intersection(otherVisions.toSet()).length;
int visionScore;
if (matchingVisions == 4 ||
((currentVisions.length == otherVisions.length) &&
(matchingVisions == currentVisions.length))) {
visionScore = 4; // full match, max score
} else if (matchingVisions > 1) {
visionScore = 3; // some match
} else if (matchingVisions == 1) {
visionScore = 2; // one match
} else {
visionScore = 0; // no match
}
// Score on WorkValues
List<WorkValueOption> currentWorks = currentUser.workValues;
List<WorkValueOption> otherWorks = otherUser.workValues;
int matchingWorks =
currentWorks.toSet().intersection(otherWorks.toSet()).length;
int workScore;
if (matchingWorks == 2 ||
((currentWorks.length == otherWorks.length) && (matchingWorks == 1))) {
workScore = 4; // full match, max score
} else if (matchingWorks == 1) {
workScore = 2; // semi match
} else {
workScore = 0; // no match
}
// Score on Risk
int riskScore = 0;
if (currentUser.risk == otherUser.risk) {
riskScore = 1;
}
// Score on CorporateCulture
int cultureScore = 0;
if (currentUser.culture == otherUser.culture) {
cultureScore = 1;
}
// Score on Communication
int communicationScore = 0;
if (currentUser.communication == otherUser.communication) {
communicationScore = 1;
}
// Calc total score. Sum of each score divided by its own max score value,
// multiplied with its own weight.
double totalScore = (distanceWeight * distanceScore / 100) +
(skillWeight * skillsScore / 8) +
(availabilityWeight * availabilityScore / 3) +
(visionWeight * visionScore / 4) +
(workWeight * workScore / 4) +
(riskWeight * riskScore) +
(cultureWeight * cultureScore) +
(communicationWeight * communicationScore);
return totalScore * 100;
}
double _distanceToPercentage(double distance) {
// Self predefined data points
final List<double> distances = [0, 5, 50, 100, 200];
final List<double> percentages = [100, 99.5, 95, 90, 50];
if (distance.isNaN || distance >= distances.last) {
return percentages.last;
} else if (distance <= distances[0]) {
return percentages[0];
}
// Linear interpolation
for (int i = 1; i < distances.length; i++) {
if (distances[i] >= distance) {
double x0 = distances[i - 1];
double y0 = percentages[i - 1];
double x1 = distances[i];
double y1 = percentages[i];
// Interpolate
double percentage = y0 + (y1 - y0) * (distance - x0) / (x1 - x0);
return percentage;
}
}
// Fallback return value, though code should never reach here
return percentages.last; // 50
}