"""Professional Services Request Prioritization
Order: high -> medium -> low; within a priority, fewer estimated_hours first;
full ties keep their original order.
Works on plain Python 3 (no external libraries), so it runs on ideone as-is.
"""
# Lower number = handled earlier. A dict makes the rule easy to read and extend.
PRIORITY_RANK = {"high": 0, "medium": 1, "low": 2}
def _sort_key(request):
"""Validate one request and return its (priority_rank, hours) sort key."""
# Required fields must exist
for field in ("id", "customer", "priority", "estimated_hours"):
if field not in request:
raise ValueError(f"Request is missing '{field}': {request}")
# Normalise priority so "High" / " HIGH " still work
priority = str(request["priority"]).strip().lower()
if priority not in PRIORITY_RANK:
raise ValueError(
f"Unknown priority '{request['priority']}' in {request['id']}; "
f"expected one of {list(PRIORITY_RANK)}"
)
# Hours must be a non-negative number (bool is an int in Python, so reject it)
hours = request["estimated_hours"]
if isinstance(hours, bool) or not isinstance(hours, (int, float)) or hours < 0:
raise ValueError(f"Invalid estimated_hours '{hours}' in {request['id']}")
return PRIORITY_RANK[priority], hours
def prioritize_requests(requests):
"""Return a NEW list of requests in the order they should be handled.
sorted() is stable: items with equal keys keep their original relative order,
which satisfies the "preserve original order on a full tie" rule for free.
The input list is not modified.
"""
return sorted(requests, key=_sort_key)
# ----------------------------- tests -----------------------------
def ids(requests):
return [r["id"] for r in requests]
def make(req_id, priority, hours, customer="Acme"):
return {"id": req_id, "customer": customer, "priority": priority, "estimated_hours": hours}
def test_sample_input():
sample = [
make("REQ-101", "medium", 3, "Alpha Ltd"),
make("REQ-102", "high", 5, "Beta Corp"),
make("REQ-103", "low", 2, "Alpha Ltd"),
make("REQ-104", "high", 4, "Gamma Inc"),
]
assert ids(prioritize_requests(sample)) == ["REQ-104", "REQ-102", "REQ-101", "REQ-103"]
def test_same_priority_orders_by_hours():
reqs = [make("A", "medium", 8), make("B", "medium", 1), make("C", "medium", 4)]
assert ids(prioritize_requests(reqs)) == ["B", "C", "A"]
def test_tie_preserves_original_order():
# Same priority AND same hours -> must stay in input order (stability)
reqs = [make("FIRST", "high", 3), make("SECOND", "high", 3), make("THIRD", "high", 3)]
assert ids(prioritize_requests(reqs)) == ["FIRST", "SECOND", "THIRD"]
def test_priority_beats_hours():
# A low-priority 1-hour job must not jump ahead of a high-priority 40-hour job
reqs = [make("LOW", "low", 1), make("HIGH", "high", 40)]
assert ids(prioritize_requests(reqs)) == ["HIGH", "LOW"]
def test_empty_list():
assert prioritize_requests([]) == []
def test_single_request_and_zero_hours():
reqs = [make("ONLY", "low", 0)]
assert ids(prioritize_requests(reqs)) == ["ONLY"]
def test_priority_case_and_whitespace_insensitive():
reqs = [make("L", "LOW", 1), make("H", " High ", 9)]
assert ids(prioritize_requests(reqs)) == ["H", "L"]
def test_float_hours():
reqs = [make("A", "high", 2.5), make("B", "high", 2.25)]
assert ids(prioritize_requests(reqs)) == ["B", "A"]
def test_input_not_modified():
reqs = [make("A", "low", 1), make("B", "high", 1)]
snapshot = list(reqs)
prioritize_requests(reqs)
assert reqs == snapshot
def test_invalid_data_raises():
bad_inputs = [
[make("X", "urgent", 2)], # unknown priority
[make("X", "high", -1)], # negative hours
[make("X", "high", "3")], # hours not a number
[make("X", "high", True)], # bool is not valid hours
[{"id": "X", "customer": "A", "priority": "high"}], # missing field
]
for bad in bad_inputs:
try:
prioritize_requests(bad)
except ValueError:
continue
raise AssertionError(f"Expected ValueError for {bad}")
def run_tests():
tests = [v for k, v in globals().items() if k.startswith("test_") and callable(v)]
for test in tests:
test()
print(f"PASS {test.__name__}")
print(f"\nAll {len(tests)} tests passed")
if __name__ == "__main__":
sample_requests = [
make("REQ-101", "medium", 3, "Alpha Ltd"),
make("REQ-102", "high", 5, "Beta Corp"),
make("REQ-103", "low", 2, "Alpha Ltd"),
make("REQ-104", "high", 4, "Gamma Inc"),
]
print("Handling order:")
for position, req in enumerate(prioritize_requests(sample_requests), start=1):
print(f"{position}. {req['id']} {req['customer']:<10} "
f"{req['priority']:<6} {req['estimated_hours']}h")
print()
run_tests()
IiIiUHJvZmVzc2lvbmFsIFNlcnZpY2VzIFJlcXVlc3QgUHJpb3JpdGl6YXRpb24KCk9yZGVyOiBoaWdoIC0+IG1lZGl1bSAtPiBsb3c7IHdpdGhpbiBhIHByaW9yaXR5LCBmZXdlciBlc3RpbWF0ZWRfaG91cnMgZmlyc3Q7CmZ1bGwgdGllcyBrZWVwIHRoZWlyIG9yaWdpbmFsIG9yZGVyLgpXb3JrcyBvbiBwbGFpbiBQeXRob24gMyAobm8gZXh0ZXJuYWwgbGlicmFyaWVzKSwgc28gaXQgcnVucyBvbiBpZGVvbmUgYXMtaXMuCiIiIgoKIyBMb3dlciBudW1iZXIgPSBoYW5kbGVkIGVhcmxpZXIuIEEgZGljdCBtYWtlcyB0aGUgcnVsZSBlYXN5IHRvIHJlYWQgYW5kIGV4dGVuZC4KUFJJT1JJVFlfUkFOSyA9IHsiaGlnaCI6IDAsICJtZWRpdW0iOiAxLCAibG93IjogMn0KCgpkZWYgX3NvcnRfa2V5KHJlcXVlc3QpOgogICAgIiIiVmFsaWRhdGUgb25lIHJlcXVlc3QgYW5kIHJldHVybiBpdHMgKHByaW9yaXR5X3JhbmssIGhvdXJzKSBzb3J0IGtleS4iIiIKICAgICMgUmVxdWlyZWQgZmllbGRzIG11c3QgZXhpc3QKICAgIGZvciBmaWVsZCBpbiAoImlkIiwgImN1c3RvbWVyIiwgInByaW9yaXR5IiwgImVzdGltYXRlZF9ob3VycyIpOgogICAgICAgIGlmIGZpZWxkIG5vdCBpbiByZXF1ZXN0OgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKGYiUmVxdWVzdCBpcyBtaXNzaW5nICd7ZmllbGR9Jzoge3JlcXVlc3R9IikKCiAgICAjIE5vcm1hbGlzZSBwcmlvcml0eSBzbyAiSGlnaCIgLyAiIEhJR0ggIiBzdGlsbCB3b3JrCiAgICBwcmlvcml0eSA9IHN0cihyZXF1ZXN0WyJwcmlvcml0eSJdKS5zdHJpcCgpLmxvd2VyKCkKICAgIGlmIHByaW9yaXR5IG5vdCBpbiBQUklPUklUWV9SQU5LOgogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoCiAgICAgICAgICAgIGYiVW5rbm93biBwcmlvcml0eSAne3JlcXVlc3RbJ3ByaW9yaXR5J119JyBpbiB7cmVxdWVzdFsnaWQnXX07ICIKICAgICAgICAgICAgZiJleHBlY3RlZCBvbmUgb2Yge2xpc3QoUFJJT1JJVFlfUkFOSyl9IgogICAgICAgICkKCiAgICAjIEhvdXJzIG11c3QgYmUgYSBub24tbmVnYXRpdmUgbnVtYmVyIChib29sIGlzIGFuIGludCBpbiBQeXRob24sIHNvIHJlamVjdCBpdCkKICAgIGhvdXJzID0gcmVxdWVzdFsiZXN0aW1hdGVkX2hvdXJzIl0KICAgIGlmIGlzaW5zdGFuY2UoaG91cnMsIGJvb2wpIG9yIG5vdCBpc2luc3RhbmNlKGhvdXJzLCAoaW50LCBmbG9hdCkpIG9yIGhvdXJzIDwgMDoKICAgICAgICByYWlzZSBWYWx1ZUVycm9yKGYiSW52YWxpZCBlc3RpbWF0ZWRfaG91cnMgJ3tob3Vyc30nIGluIHtyZXF1ZXN0WydpZCddfSIpCgogICAgcmV0dXJuIFBSSU9SSVRZX1JBTktbcHJpb3JpdHldLCBob3VycwoKCmRlZiBwcmlvcml0aXplX3JlcXVlc3RzKHJlcXVlc3RzKToKICAgICIiIlJldHVybiBhIE5FVyBsaXN0IG9mIHJlcXVlc3RzIGluIHRoZSBvcmRlciB0aGV5IHNob3VsZCBiZSBoYW5kbGVkLgoKICAgIHNvcnRlZCgpIGlzIHN0YWJsZTogaXRlbXMgd2l0aCBlcXVhbCBrZXlzIGtlZXAgdGhlaXIgb3JpZ2luYWwgcmVsYXRpdmUgb3JkZXIsCiAgICB3aGljaCBzYXRpc2ZpZXMgdGhlICJwcmVzZXJ2ZSBvcmlnaW5hbCBvcmRlciBvbiBhIGZ1bGwgdGllIiBydWxlIGZvciBmcmVlLgogICAgVGhlIGlucHV0IGxpc3QgaXMgbm90IG1vZGlmaWVkLgogICAgIiIiCiAgICByZXR1cm4gc29ydGVkKHJlcXVlc3RzLCBrZXk9X3NvcnRfa2V5KQoKCiMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gdGVzdHMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmRlZiBpZHMocmVxdWVzdHMpOgogICAgcmV0dXJuIFtyWyJpZCJdIGZvciByIGluIHJlcXVlc3RzXQoKCmRlZiBtYWtlKHJlcV9pZCwgcHJpb3JpdHksIGhvdXJzLCBjdXN0b21lcj0iQWNtZSIpOgogICAgcmV0dXJuIHsiaWQiOiByZXFfaWQsICJjdXN0b21lciI6IGN1c3RvbWVyLCAicHJpb3JpdHkiOiBwcmlvcml0eSwgImVzdGltYXRlZF9ob3VycyI6IGhvdXJzfQoKCmRlZiB0ZXN0X3NhbXBsZV9pbnB1dCgpOgogICAgc2FtcGxlID0gWwogICAgICAgIG1ha2UoIlJFUS0xMDEiLCAibWVkaXVtIiwgMywgIkFscGhhIEx0ZCIpLAogICAgICAgIG1ha2UoIlJFUS0xMDIiLCAiaGlnaCIsIDUsICJCZXRhIENvcnAiKSwKICAgICAgICBtYWtlKCJSRVEtMTAzIiwgImxvdyIsIDIsICJBbHBoYSBMdGQiKSwKICAgICAgICBtYWtlKCJSRVEtMTA0IiwgImhpZ2giLCA0LCAiR2FtbWEgSW5jIiksCiAgICBdCiAgICBhc3NlcnQgaWRzKHByaW9yaXRpemVfcmVxdWVzdHMoc2FtcGxlKSkgPT0gWyJSRVEtMTA0IiwgIlJFUS0xMDIiLCAiUkVRLTEwMSIsICJSRVEtMTAzIl0KCgpkZWYgdGVzdF9zYW1lX3ByaW9yaXR5X29yZGVyc19ieV9ob3VycygpOgogICAgcmVxcyA9IFttYWtlKCJBIiwgIm1lZGl1bSIsIDgpLCBtYWtlKCJCIiwgIm1lZGl1bSIsIDEpLCBtYWtlKCJDIiwgIm1lZGl1bSIsIDQpXQogICAgYXNzZXJ0IGlkcyhwcmlvcml0aXplX3JlcXVlc3RzKHJlcXMpKSA9PSBbIkIiLCAiQyIsICJBIl0KCgpkZWYgdGVzdF90aWVfcHJlc2VydmVzX29yaWdpbmFsX29yZGVyKCk6CiAgICAjIFNhbWUgcHJpb3JpdHkgQU5EIHNhbWUgaG91cnMgLT4gbXVzdCBzdGF5IGluIGlucHV0IG9yZGVyIChzdGFiaWxpdHkpCiAgICByZXFzID0gW21ha2UoIkZJUlNUIiwgImhpZ2giLCAzKSwgbWFrZSgiU0VDT05EIiwgImhpZ2giLCAzKSwgbWFrZSgiVEhJUkQiLCAiaGlnaCIsIDMpXQogICAgYXNzZXJ0IGlkcyhwcmlvcml0aXplX3JlcXVlc3RzKHJlcXMpKSA9PSBbIkZJUlNUIiwgIlNFQ09ORCIsICJUSElSRCJdCgoKZGVmIHRlc3RfcHJpb3JpdHlfYmVhdHNfaG91cnMoKToKICAgICMgQSBsb3ctcHJpb3JpdHkgMS1ob3VyIGpvYiBtdXN0IG5vdCBqdW1wIGFoZWFkIG9mIGEgaGlnaC1wcmlvcml0eSA0MC1ob3VyIGpvYgogICAgcmVxcyA9IFttYWtlKCJMT1ciLCAibG93IiwgMSksIG1ha2UoIkhJR0giLCAiaGlnaCIsIDQwKV0KICAgIGFzc2VydCBpZHMocHJpb3JpdGl6ZV9yZXF1ZXN0cyhyZXFzKSkgPT0gWyJISUdIIiwgIkxPVyJdCgoKZGVmIHRlc3RfZW1wdHlfbGlzdCgpOgogICAgYXNzZXJ0IHByaW9yaXRpemVfcmVxdWVzdHMoW10pID09IFtdCgoKZGVmIHRlc3Rfc2luZ2xlX3JlcXVlc3RfYW5kX3plcm9faG91cnMoKToKICAgIHJlcXMgPSBbbWFrZSgiT05MWSIsICJsb3ciLCAwKV0KICAgIGFzc2VydCBpZHMocHJpb3JpdGl6ZV9yZXF1ZXN0cyhyZXFzKSkgPT0gWyJPTkxZIl0KCgpkZWYgdGVzdF9wcmlvcml0eV9jYXNlX2FuZF93aGl0ZXNwYWNlX2luc2Vuc2l0aXZlKCk6CiAgICByZXFzID0gW21ha2UoIkwiLCAiTE9XIiwgMSksIG1ha2UoIkgiLCAiIEhpZ2ggIiwgOSldCiAgICBhc3NlcnQgaWRzKHByaW9yaXRpemVfcmVxdWVzdHMocmVxcykpID09IFsiSCIsICJMIl0KCgpkZWYgdGVzdF9mbG9hdF9ob3VycygpOgogICAgcmVxcyA9IFttYWtlKCJBIiwgImhpZ2giLCAyLjUpLCBtYWtlKCJCIiwgImhpZ2giLCAyLjI1KV0KICAgIGFzc2VydCBpZHMocHJpb3JpdGl6ZV9yZXF1ZXN0cyhyZXFzKSkgPT0gWyJCIiwgIkEiXQoKCmRlZiB0ZXN0X2lucHV0X25vdF9tb2RpZmllZCgpOgogICAgcmVxcyA9IFttYWtlKCJBIiwgImxvdyIsIDEpLCBtYWtlKCJCIiwgImhpZ2giLCAxKV0KICAgIHNuYXBzaG90ID0gbGlzdChyZXFzKQogICAgcHJpb3JpdGl6ZV9yZXF1ZXN0cyhyZXFzKQogICAgYXNzZXJ0IHJlcXMgPT0gc25hcHNob3QKCgpkZWYgdGVzdF9pbnZhbGlkX2RhdGFfcmFpc2VzKCk6CiAgICBiYWRfaW5wdXRzID0gWwogICAgICAgIFttYWtlKCJYIiwgInVyZ2VudCIsIDIpXSwgICAgICAgICAgICAgICAgICAgICAgIyB1bmtub3duIHByaW9yaXR5CiAgICAgICAgW21ha2UoIlgiLCAiaGlnaCIsIC0xKV0sICAgICAgICAgICAgICAgICAgICAgICAjIG5lZ2F0aXZlIGhvdXJzCiAgICAgICAgW21ha2UoIlgiLCAiaGlnaCIsICIzIildLCAgICAgICAgICAgICAgICAgICAgICAjIGhvdXJzIG5vdCBhIG51bWJlcgogICAgICAgIFttYWtlKCJYIiwgImhpZ2giLCBUcnVlKV0sICAgICAgICAgICAgICAgICAgICAgIyBib29sIGlzIG5vdCB2YWxpZCBob3VycwogICAgICAgIFt7ImlkIjogIlgiLCAiY3VzdG9tZXIiOiAiQSIsICJwcmlvcml0eSI6ICJoaWdoIn1dLCAgICMgbWlzc2luZyBmaWVsZAogICAgXQogICAgZm9yIGJhZCBpbiBiYWRfaW5wdXRzOgogICAgICAgIHRyeToKICAgICAgICAgICAgcHJpb3JpdGl6ZV9yZXF1ZXN0cyhiYWQpCiAgICAgICAgZXhjZXB0IFZhbHVlRXJyb3I6CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgcmFpc2UgQXNzZXJ0aW9uRXJyb3IoZiJFeHBlY3RlZCBWYWx1ZUVycm9yIGZvciB7YmFkfSIpCgoKZGVmIHJ1bl90ZXN0cygpOgogICAgdGVzdHMgPSBbdiBmb3IgaywgdiBpbiBnbG9iYWxzKCkuaXRlbXMoKSBpZiBrLnN0YXJ0c3dpdGgoInRlc3RfIikgYW5kIGNhbGxhYmxlKHYpXQogICAgZm9yIHRlc3QgaW4gdGVzdHM6CiAgICAgICAgdGVzdCgpCiAgICAgICAgcHJpbnQoZiJQQVNTICB7dGVzdC5fX25hbWVfX30iKQogICAgcHJpbnQoZiJcbkFsbCB7bGVuKHRlc3RzKX0gdGVzdHMgcGFzc2VkIikKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgc2FtcGxlX3JlcXVlc3RzID0gWwogICAgICAgIG1ha2UoIlJFUS0xMDEiLCAibWVkaXVtIiwgMywgIkFscGhhIEx0ZCIpLAogICAgICAgIG1ha2UoIlJFUS0xMDIiLCAiaGlnaCIsIDUsICJCZXRhIENvcnAiKSwKICAgICAgICBtYWtlKCJSRVEtMTAzIiwgImxvdyIsIDIsICJBbHBoYSBMdGQiKSwKICAgICAgICBtYWtlKCJSRVEtMTA0IiwgImhpZ2giLCA0LCAiR2FtbWEgSW5jIiksCiAgICBdCiAgICBwcmludCgiSGFuZGxpbmcgb3JkZXI6IikKICAgIGZvciBwb3NpdGlvbiwgcmVxIGluIGVudW1lcmF0ZShwcmlvcml0aXplX3JlcXVlc3RzKHNhbXBsZV9yZXF1ZXN0cyksIHN0YXJ0PTEpOgogICAgICAgIHByaW50KGYie3Bvc2l0aW9ufS4ge3JlcVsnaWQnXX0gIHtyZXFbJ2N1c3RvbWVyJ106PDEwfSAiCiAgICAgICAgICAgICAgZiJ7cmVxWydwcmlvcml0eSddOjw2fSB7cmVxWydlc3RpbWF0ZWRfaG91cnMnXX1oIikKICAgIHByaW50KCkKICAgIHJ1bl90ZXN0cygp