-
Notifications
You must be signed in to change notification settings - Fork 344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Is-a filter in local ValueSet expansion #2733
Merged
mmsmits
merged 21 commits into
develop
from
2727-support-is-a-filter-in-local-valueset-expansion
Mar 19, 2024
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
b9ab5c6
add filter extensions
mmsmits 5c6f307
freature: added is-a filter to expand valuesets
mmsmits 78aab37
added tests
mmsmits 9390e07
improved Tests
mmsmits 2264ce1
document and clean up
mmsmits 3fa0eef
remove some unused code
mmsmits 021143a
throw an error when trying to filter codes from LOINC or SNOMED CT
mmsmits 560da4a
(this commit shows better than review words what I meant)
ewoutkramer 49f2dfb
moved valueset expansion extensions to shims.base
mmsmits 800f044
minor improvements
mmsmits 97933f2
added test to check for a positive result
mmsmits 5db1f19
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
mmsmits 2ad9b62
improvement: first flatten codes and then use a lookup to find all de…
mmsmits f19783a
improvement of checking for correct filter is moved to switch statement
mmsmits 931d41e
also copies the properties of CodeSystem concepts to ValueSet contains
mmsmits 5e3621d
make list of CodeSystems to complex to filter a setting
mmsmits 3ceabd7
handles filtering on incomplete codesystems
mmsmits b82ee16
fix test
mmsmits 9f2c78d
remove specific check for certain codesystems. Now check if the CodeS…
mmsmits fbae182
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
mmsmits 9ec0acd
Merge branch 'develop' into 2727-support-is-a-filter-in-local-valuese…
ewoutkramer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 0 additions & 53 deletions
53
src/Hl7.Fhir.Conformance/Model/ValueSetExpansionExtensions.cs
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
src/Hl7.Fhir.Shims.Base/Model/ValueSetExpansionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using CSDC = Hl7.Fhir.Model.CodeSystem.ConceptDefinitionComponent; | ||
|
||
#nullable enable | ||
|
||
namespace Hl7.Fhir.Model | ||
{ | ||
public static class ValueSetExpansionExtensions | ||
{ | ||
public static ValueSet.ContainsComponent? FindCode(this IEnumerable<ValueSet.ContainsComponent> cnt, string code, string? system = null) | ||
{ | ||
foreach (var contains in cnt) | ||
{ | ||
var result = contains.FindCode(code, system); | ||
if (result != null) return result; | ||
} | ||
|
||
return null; | ||
} | ||
public static ValueSet.ContainsComponent? FindCode(this ValueSet.ContainsComponent contains, string code, string? system = null) | ||
{ | ||
// Direct hit | ||
if (code == contains.Code && (system == null || system == contains.System)) | ||
return contains; | ||
|
||
// Not in this node, but this node may have child nodes to check | ||
if (contains.Contains?.Any() == true) | ||
return contains.Contains.FindCode(code, system); | ||
else | ||
return null; | ||
} | ||
|
||
internal static CSDC? FindCode(this IEnumerable<CSDC> concepts, string code) | ||
{ | ||
return concepts.findCodeByPredicate(c => c.Code == code); | ||
} | ||
|
||
private static CSDC? findCodeByPredicate(this IEnumerable<CSDC> concepts, Predicate<CSDC> predicate) | ||
{ | ||
foreach (var concept in concepts) | ||
{ | ||
var result = concept.findCodeByPredicate(predicate); | ||
if (result != null) return result; | ||
} | ||
return null; | ||
} | ||
|
||
private static CSDC? findCodeByPredicate(this CSDC concept, Predicate<CSDC> predicate) | ||
{ | ||
// Direct hit | ||
if (predicate(concept)) | ||
return concept; | ||
|
||
// Not in this node, but this node may have child nodes to check | ||
if (concept.Concept?.Any() == true) | ||
return concept.Concept.findCodeByPredicate(predicate); | ||
else | ||
return null; | ||
} | ||
|
||
/// <summary> | ||
/// Loops through all concepts and descendants and returns a flat list of concepts, without a nested hierarchy | ||
/// </summary> | ||
/// <param name="concepts">List of code system concepts</param> | ||
/// <returns></returns> | ||
internal static List<CSDC> Flatten(this IEnumerable<CSDC> concepts) | ||
{ | ||
var flatList = new List<CSDC>(); | ||
|
||
foreach (var concept in concepts) | ||
{ | ||
if (concept.Concept?.Any() == true) | ||
{ | ||
flatList.AddRange(concept.Concept.Flatten()); | ||
concept.Concept.Clear(); | ||
} | ||
flatList.Add(concept); | ||
} | ||
return flatList; | ||
} | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
src/Hl7.Fhir.Shims.Base/Specification/Terminology/CodeSystemFilterProcessor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
using Hl7.Fhir.Model; | ||
using Hl7.Fhir.Specification.Source; | ||
using Hl7.Fhir.Utility; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using T = System.Threading.Tasks; | ||
|
||
#nullable enable | ||
|
||
namespace Hl7.Fhir.Specification.Terminology | ||
{ | ||
|
||
internal static class CodeSystemFilterProcessor | ||
{ | ||
private const string SUBSUMEDBYCODE = "subsumedBy"; | ||
|
||
/// <summary> | ||
/// Retrieve codes from a CodeSystem resource bases on one or multiple filters. | ||
/// </summary> | ||
/// <param name="codeSystemUri">Uri of the CodeSystem</param> | ||
/// <param name="filters">Filters to be applied</param> | ||
/// <param name="settings">ValueSetExpanderSettings </param> | ||
/// <returns></returns> | ||
/// <exception cref="ValueSetExpansionTooComplexException">Thrown when a filter is applied that is not supported (yet)</exception> | ||
/// <exception cref="CodeSystemUnknownException">Thrown when no resource resolver was set in ValueSetExpanderSettings.ValueSetSource</exception> | ||
/// <exception cref="CodeSystemUnknownException">Thrown when the requested CodeSystem can not be found by the resource resolver in ValueSetExpanderSettings</exception> | ||
internal async static T.Task<IEnumerable<ValueSet.ContainsComponent>> FilterConceptsFromCodeSystem(string codeSystemUri, List<ValueSet.FilterComponent> filters, ValueSetExpanderSettings settings) | ||
{ | ||
if (settings.ValueSetSource == null) | ||
throw Error.InvalidOperation($"No valueset resolver available to resolve codesystem '{codeSystemUri}', so the expansion cannot be completed."); | ||
|
||
var codeSystem = await settings.ValueSetSource.AsAsync().FindCodeSystemAsync(codeSystemUri).ConfigureAwait(false) | ||
?? throw new CodeSystemUnknownException($"Cannot find codesystem '{codeSystemUri}', so the defined filter(s) cannot be applied."); | ||
|
||
if (codeSystem.Content.GetLiteral() != "complete") | ||
throw new CodeSystemIncompleteException($"CodeSystem {codeSystemUri} is marked incomplete, so the defines filter(s) cannot be applied."); | ||
|
||
|
||
var result = applyFilters(filters, codeSystem); | ||
|
||
return result.Select(c => c.ToContainsComponent(codeSystem, settings)); | ||
} | ||
|
||
private static List<CodeSystem.ConceptDefinitionComponent> applyFilters(List<ValueSet.FilterComponent> filters, CodeSystem codeSystem) | ||
{ | ||
var result = codeSystem.Concept; | ||
var properties = codeSystem.Property; | ||
|
||
ewoutkramer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
foreach (var filter in filters) | ||
{ | ||
result = applyFilter(result, properties, filter).ToList(); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
private static IEnumerable<CodeSystem.ConceptDefinitionComponent> applyFilter(List<CodeSystem.ConceptDefinitionComponent> concepts, List<CodeSystem.PropertyComponent> properties, ValueSet.FilterComponent filter) | ||
{ | ||
return filter.Op switch | ||
{ | ||
FilterOperator.IsA => applyIsAFilter(concepts, properties, filter), | ||
_ => throw new ValueSetExpansionTooComplexException($"ConceptSets with a filter {filter.Op} are not yet supported.") | ||
}; | ||
} | ||
|
||
private static IEnumerable<CodeSystem.ConceptDefinitionComponent> applyIsAFilter(List<CodeSystem.ConceptDefinitionComponent> concepts, List<CodeSystem.PropertyComponent> properties, ValueSet.FilterComponent filter) | ||
{ | ||
var result = new List<CodeSystem.ConceptDefinitionComponent>(); | ||
|
||
//find descendants based on subsumedBy | ||
if (properties.Any(p => p.Code == SUBSUMEDBYCODE)) | ||
{ | ||
//first find the parent itself (if it's in the CodeSystem) | ||
if (concepts.FindCode(filter.Value) is { } concept) | ||
result.Add(concept); | ||
|
||
//Create a lookup which lists children by parent. | ||
var flattened = concepts.Flatten(); | ||
var childrenLookup = CreateSubsumedByLookup(flattened); | ||
|
||
//find descendants based on that lookup | ||
var descendants = applySubsumedBy(childrenLookup, filter); | ||
result.AddRange(descendants); | ||
} | ||
else | ||
{ | ||
//SubsumedBy is not used, we should only check for a nested hierarchy, and include the code and it's descendants | ||
if (concepts.FindCode(filter.Value) is { } concept) | ||
ewoutkramer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
result.Add(concept); | ||
} | ||
return result; | ||
} | ||
|
||
private static ILookup<string, CodeSystem.ConceptDefinitionComponent> CreateSubsumedByLookup(List<CodeSystem.ConceptDefinitionComponent> flattenedConcepts) | ||
{ | ||
return flattenedConcepts | ||
.SelectMany(concept => concept.Property | ||
.Where(p => p.Code == SUBSUMEDBYCODE && p.Value is Code && ((Code)p.Value).Value is not null) | ||
.Select(p => new { SubsumedByValue = ((Code)p.Value).Value, Concept = concept })) | ||
.ToLookup(x => x.SubsumedByValue, x => x.Concept); | ||
} | ||
|
||
private static List<CodeSystem.ConceptDefinitionComponent> applySubsumedBy(ILookup<string, CodeSystem.ConceptDefinitionComponent> lookup, ValueSet.FilterComponent filter) | ||
{ | ||
var result = new List<CodeSystem.ConceptDefinitionComponent>(); | ||
var root = filter.Value; | ||
if (root != null) | ||
{ | ||
addDescendants(lookup, root, result); | ||
} | ||
return result; | ||
} | ||
|
||
//recursively loop through all the children to eventually find all descendants. | ||
private static void addDescendants(ILookup<string, CodeSystem.ConceptDefinitionComponent> lookup, string parent, List<CodeSystem.ConceptDefinitionComponent> result) | ||
{ | ||
if (lookup[parent] is { } children) | ||
{ | ||
foreach (var child in children) | ||
{ | ||
result.Add(child); | ||
parent = child.Code; | ||
addDescendants(lookup, parent, result); | ||
} | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
#nullable restore |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.