219 lines
5.1 KiB
Dart
219 lines
5.1 KiB
Dart
import 'package:meta/meta.dart';
|
|
|
|
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
|
|
|
/// Represents a validated Military Occupational Specialty (MOS) code.
|
|
///
|
|
/// MOS codes follow the U.S. Army standard format: 2 digits + 1 letter
|
|
/// (e.g., "11B", "68W", "13F"). This value object enforces that format
|
|
/// and validates against the known MOS dictionary.
|
|
@immutable
|
|
class DutyMos {
|
|
/// Creates a [DutyMos] from a raw MOS code string.
|
|
///
|
|
/// Throws [ArgumentError] if the code is not a valid 3-character MOS.
|
|
factory DutyMos(String code) {
|
|
final normalized = code.trim().toUpperCase();
|
|
if (!_isValidFormat(normalized)) {
|
|
throw ArgumentError(
|
|
'Invalid MOS code format: "$code". '
|
|
'Expected format: 2 digits + 1 letter (e.g., "11B", "68W").',
|
|
);
|
|
}
|
|
if (!_knownMosCodes.contains(normalized)) {
|
|
throw ArgumentError(
|
|
'Unknown MOS code: "$normalized". '
|
|
'Code is not in the authorized MOS dictionary.',
|
|
);
|
|
}
|
|
return DutyMos._(normalized);
|
|
}
|
|
|
|
/// Creates a [DutyMos] without dictionary validation (for testing/migration).
|
|
factory DutyMos.unvalidated(String code) {
|
|
final normalized = code.trim().toUpperCase();
|
|
if (!_isValidFormat(normalized)) {
|
|
throw ArgumentError(
|
|
'Invalid MOS code format: "$code". '
|
|
'Expected format: 2 digits + 1 letter (e.g., "11B", "68W").',
|
|
);
|
|
}
|
|
return DutyMos._(normalized);
|
|
}
|
|
|
|
const DutyMos._(this.code);
|
|
|
|
/// The normalized MOS code string (e.g., "11B", "68W", "13F").
|
|
final String code;
|
|
|
|
/// Returns true if [code] matches the 2-digit + 1-letter MOS format.
|
|
static bool _isValidFormat(String code) {
|
|
if (code.length != 3) return false;
|
|
final digits = code.substring(0, 2);
|
|
final letter = code[2];
|
|
return RegExp(r'^\d{2}$').hasMatch(digits) && RegExp(r'^[A-Z]$').hasMatch(letter);
|
|
}
|
|
|
|
/// Returns true if [code] is a known MOS in the authorized dictionary.
|
|
static bool isKnown(String code) {
|
|
return _knownMosCodes.contains(code.trim().toUpperCase());
|
|
}
|
|
|
|
/// Returns all MOS codes for a given [DoctrinalBranch].
|
|
static Set<String> codesForBranch(DoctrinalBranch branch) {
|
|
return _branchMosMap[branch] ?? {};
|
|
}
|
|
|
|
/// The complete set of authorized MOS codes.
|
|
static const Set<String> _knownMosCodes = {
|
|
// Infantry & Maneuver (CMF 11 / 15)
|
|
'11A', '11B', '11C', '15W', '15A', '15T', '15B', '15D', '15G', '153A',
|
|
// Medical (CMF 68)
|
|
'68W', '68A', '68D', '68E', '68F', '68G', '68J',
|
|
'68K', '68P', '68Q', '68R', '68S', '68T', '68X',
|
|
// Field Artillery (CMF 13)
|
|
'13A', '13B', '13D', '13F', '13J', '13M', '13P', '13R', '13T',
|
|
// Signal (CMF 25)
|
|
'25A', '25B', '25C', '25D', '25E', '25H', '25L', '25M',
|
|
'25N', '25P', '25Q', '25R', '25S', '25U', '25V', '25W', '25X', '25Z',
|
|
// Cyber/EW (CMF 17)
|
|
'17A', '17C', '17E', '17S',
|
|
// Aviation (CMF 15)
|
|
'15Z',
|
|
// Logistics (CMF 91/92)
|
|
'91A', '91B', '91C', '91D', '91E', '91F', '91G', '91H',
|
|
'91J', '91L', '91M', '91P', '91S', '91X', '91Z',
|
|
'92A', '92F', '92G', '92L', '92M', '92R', '92S', '92W', '92Y', '92Z',
|
|
// Engineer (CMF 12)
|
|
'12A', '12B', '12C', '12D', '12G', '12H', '12K', '12M',
|
|
'12N', '12P', '12Q', '12R', '12T', '12W', '12X', '12Y', '12Z',
|
|
};
|
|
|
|
/// Maps each [DoctrinalBranch] to its associated MOS codes.
|
|
static const Map<DoctrinalBranch, Set<String>> _branchMosMap = {
|
|
DoctrinalBranch.infantry: {
|
|
'11A',
|
|
'11B',
|
|
'11C',
|
|
'15W',
|
|
'15A',
|
|
'15T',
|
|
'153A',
|
|
},
|
|
DoctrinalBranch.medical: {
|
|
'68W',
|
|
'68A',
|
|
'68D',
|
|
'68E',
|
|
'68F',
|
|
'68G',
|
|
'68J',
|
|
'68K',
|
|
'68P',
|
|
'68Q',
|
|
'68R',
|
|
'68S',
|
|
'68T',
|
|
'68X',
|
|
},
|
|
DoctrinalBranch.fires: {
|
|
'13A',
|
|
'13B',
|
|
'13D',
|
|
'13F',
|
|
'13J',
|
|
'13M',
|
|
'13P',
|
|
'13R',
|
|
'13T',
|
|
},
|
|
DoctrinalBranch.signal: {
|
|
'25A',
|
|
'25B',
|
|
'25C',
|
|
'25D',
|
|
'25E',
|
|
'25H',
|
|
'25L',
|
|
'25M',
|
|
'25N',
|
|
'25P',
|
|
'25Q',
|
|
'25R',
|
|
'25S',
|
|
'25U',
|
|
'25V',
|
|
'25W',
|
|
'25X',
|
|
'25Z',
|
|
},
|
|
DoctrinalBranch.cyberEw: {'17A', '17C', '17E', '17S'},
|
|
DoctrinalBranch.aviation: {
|
|
'15A',
|
|
'15B',
|
|
'15D',
|
|
'15G',
|
|
'15T',
|
|
'15W',
|
|
'15Z',
|
|
'153A',
|
|
},
|
|
DoctrinalBranch.logistics: {
|
|
'91A',
|
|
'91B',
|
|
'91C',
|
|
'91D',
|
|
'91E',
|
|
'91F',
|
|
'91G',
|
|
'91H',
|
|
'91J',
|
|
'91L',
|
|
'91M',
|
|
'91P',
|
|
'91S',
|
|
'91X',
|
|
'91Z',
|
|
'92A',
|
|
'92F',
|
|
'92G',
|
|
'92L',
|
|
'92M',
|
|
'92R',
|
|
'92S',
|
|
'92W',
|
|
'92Y',
|
|
'92Z',
|
|
},
|
|
DoctrinalBranch.engineer: {
|
|
'12A',
|
|
'12B',
|
|
'12C',
|
|
'12D',
|
|
'12G',
|
|
'12H',
|
|
'12K',
|
|
'12M',
|
|
'12N',
|
|
'12P',
|
|
'12Q',
|
|
'12R',
|
|
'12T',
|
|
'12W',
|
|
'12X',
|
|
'12Y',
|
|
'12Z',
|
|
},
|
|
};
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) || (other is DutyMos && other.code == code);
|
|
|
|
@override
|
|
int get hashCode => code.hashCode;
|
|
|
|
@override
|
|
String toString() => code;
|
|
}
|