diff --git a/.editorconfig b/.editorconfig index 22053b22b..ec73f67a5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,7 +15,6 @@ ij_visual_guides = none ij_wrap_on_typing = false [*.{kt,kts}] -insert_final_newline = true ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_name_count_to_use_star_import = 999 @@ -118,7 +117,7 @@ ij_java_for_statement_wrap = off ij_java_generate_final_locals = false ij_java_generate_final_parameters = false ij_java_if_brace_force = never -ij_java_imports_layout = $*,|,java.**,javax.**,|,org.**,com.**,|,* +ij_java_imports_layout = $*, |, java.**, javax.**, |, org.**, com.**, |, * ij_java_indent_case_from_switch = false ij_java_insert_inner_class_imports = false ij_java_insert_override_annotation = true @@ -155,7 +154,7 @@ ij_java_names_count_to_use_import_on_demand = 3 ij_java_new_line_after_lparen_in_annotation = false ij_java_new_line_after_lparen_in_deconstruction_pattern = true ij_java_new_line_after_lparen_in_record_header = false -ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_packages_to_use_import_on_demand = java.awt.*, javax.swing.* ij_java_parameter_annotation_wrap = off ij_java_parentheses_expression_new_line_after_left_paren = false ij_java_parentheses_expression_right_paren_on_new_line = false @@ -269,3 +268,4 @@ ij_java_while_on_new_line = false ij_java_wrap_comments = true ij_java_wrap_first_method_in_call_chain = false ij_java_wrap_long_lines = false + diff --git a/framework/common/pom.xml b/framework/common/pom.xml index 3daa8895a..a51b1619f 100644 --- a/framework/common/pom.xml +++ b/framework/common/pom.xml @@ -32,6 +32,11 @@ org.apache.commons commons-text + + org.apache.jena + jena-arq + 4.8.0 + org.apache.opennlp opennlp-tools @@ -50,6 +55,11 @@ jgrapht-core ${jgrapht.version} + + org.jsoup + jsoup + 1.16.1 + org.junit.jupiter junit-jupiter-engine diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/Disambiguation.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/Disambiguation.java new file mode 100644 index 000000000..3a9574bc1 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/Disambiguation.java @@ -0,0 +1,153 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api; + +import java.io.IOException; +import java.io.Serializable; +import java.util.*; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; + +/** + * This class represents an abbreviation with a known set of meanings. An abbreviation is a string such as "ArDoCo" and has the meaning "Architecture + * Documentation Consistency". The abbreviation that is disambiguated by the meanings of this class is final, but the meanings can be changed. An instance of + * this class can be serialized and deserialized into JSON using Jackson. + */ +@Deterministic +@JsonSerialize(using = Disambiguation.DisambiguationSerializer.class) +public class Disambiguation implements Comparable, Serializable { + private final String abbreviation; + private final SortedSet meanings; + + public String getAbbreviation() { + return abbreviation; + } + + public SortedSet getMeanings() { + return new TreeSet<>(meanings); + } + + @Override + public int compareTo(Disambiguation o) { + return abbreviation.compareTo(o.abbreviation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof Disambiguation other) { + return getAbbreviation().equals(other.getAbbreviation()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(getAbbreviation()); + } + + /** + * Used by the Jackson library to serialize a disambiguation into JSON format. + */ + public static class DisambiguationSerializer extends JsonSerializer { + @Override + public void serialize(Disambiguation abbreviation, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringField("abbreviation", abbreviation.abbreviation); + jsonGenerator.writeArrayFieldStart("meanings"); + var meanings = abbreviation.meanings; + for (var meaning : meanings) { + jsonGenerator.writeString(meaning); + } + jsonGenerator.writeEndArray(); + jsonGenerator.writeEndObject(); + } + } + + /** + * Creates a new disambiguation of the provided abbreviation using the given array of meanings. + * + * @param abbreviation the abbreviation that is disambiguated by this instance + * @param meanings an array of meanings for the abbreviation, may be empty + */ + @JsonCreator + public Disambiguation(@JsonProperty("abbreviation") String abbreviation, @JsonProperty("meanings") String[] meanings) { + this(abbreviation, new TreeSet<>(Arrays.stream(meanings).toList())); + } + + /** + * Creates a new disambiguation of the provided abbreviation using the given set of meanings. + * + * @param abbreviation the abbreviation that is disambiguated by this instance + * @param meanings a set of meanings for the abbreviation, may be empty + */ + public Disambiguation(String abbreviation, SortedSet meanings) { + this.abbreviation = abbreviation; + this.meanings = new TreeSet<>(meanings); + } + + /** + * Adds all meanings from another disambiguation to this disambiguation. Be careful, this does not perform any checks regarding the abbreviations. + * + * @param other the other disambiguation + * @return this + */ + public Disambiguation addMeanings(Disambiguation other) { + meanings.addAll(other.meanings); + return this; + } + + /** + * Searches the text for meanings contained by this disambiguation and replaces them with the abbreviation. + * + * @param text the text to search + * @param ignoreCase whether letter case should be ignored when searching for the meanings + * @return the abbreviated text + */ + public String replaceMeaningWithAbbreviation(String text, boolean ignoreCase) { + var abbreviatedText = text; + for (String meaning : meanings) { + String pattern = ignoreCase ? "(?i)" : ""; + pattern += meaning; + abbreviatedText = abbreviatedText.replaceAll(pattern, abbreviation); + } + return abbreviatedText; + } + + /** + * Merges the first map with the second map in a new map. If a key already exists, the disambiguations are merged non-destructively. + * + * @param a the first map + * @param b the second map + * @return a mutable map + */ + + public static SortedMap merge(SortedMap a, SortedMap b) { + var mergedMap = new TreeMap<>(a); + for (var entry : b.entrySet()) { + mergedMap.merge(entry.getKey(), entry.getValue(), Disambiguation::merge); + } + return mergedMap; + } + + /** + * Merges the first disambiguation with the second disambiguation into a new instance. + * + * @param a first ambiguation + * @param b second ambiguation + * @return new merged disambiguation + */ + + public static Disambiguation merge(Disambiguation a, Disambiguation b) { + var temp = new TreeSet<>(a.meanings); + temp.addAll(b.meanings); + return new Disambiguation(a.abbreviation, temp); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputDiagramData.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputDiagramData.java index af1769adf..7b6fe9ab3 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputDiagramData.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputDiagramData.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api; import java.io.File; @@ -7,6 +7,7 @@ import java.util.List; import java.util.Objects; +import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; /** @@ -16,7 +17,9 @@ public class InputDiagramData implements PipelineStepData { public static final String ID = "InputDiagramData"; private static final List ALLOWED_FILE_TYPES = List.of("jpg", "png", "jpeg"); - private final transient String pathToDiagrams; + private String pathToDiagrams; + + private List> diagramFiles; /** * Create the data by a directory of image files. @@ -36,12 +39,24 @@ public InputDiagramData(File diagramDirectory) { this(Objects.requireNonNull(diagramDirectory).getAbsolutePath()); } + /** + * Create the data by a list of image files. + * + * @param diagramFiles the diagram files + */ + public InputDiagramData(List> diagramFiles) { + Objects.requireNonNull(diagramFiles); + this.diagramFiles = diagramFiles; + } + /** * Get all image files of the given directory (not recursive). * * @return a list of image files ordered by name */ - public List getFiles() { + public List> getDiagramData() { + if (diagramFiles != null) + return diagramFiles; if (pathToDiagrams == null) return List.of(); File directoryOfDiagrams = new File(pathToDiagrams); @@ -51,10 +66,11 @@ public List getFiles() { File[] allFiles = directoryOfDiagrams.listFiles(); if (allFiles == null) return List.of(); - List diagrams = Arrays.stream(allFiles) + List> diagrams = Arrays.stream(allFiles) .filter(File::isFile) .filter(f -> ALLOWED_FILE_TYPES.stream().anyMatch(t -> f.getName().toLowerCase().endsWith("." + t))) .sorted(Comparator.comparing(File::getName)) + .map(f -> new Pair<>(f.getName(), f)) .toList(); logger.info("Found {} diagrams to consider.", diagrams.size()); return diagrams; diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputTextData.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputTextData.java index 5c488a86e..d166666a5 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputTextData.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/InputTextData.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api; import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; @@ -7,7 +7,7 @@ public class InputTextData implements PipelineStepData { public static final String ID = "InputTextData"; - private transient String text; + private String text; public InputTextData(String text) { super(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/PreprocessingData.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/PreprocessingData.java index c6e00d92f..b72629334 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/PreprocessingData.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/PreprocessingData.java @@ -1,17 +1,17 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api; import edu.kit.kastel.mcse.ardoco.core.api.text.Text; import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; /** - * This class serves as container for different data after preprocessing to store as {@link PipelineStepData}. Right - * now, this includes the preprocessed {@link Text} only. + * This class serves as container for different data after preprocessing to store as {@link PipelineStepData}. Right now, this includes the preprocessed + * {@link Text} only. */ public class PreprocessingData implements PipelineStepData { public static final String ID = "PreprocessingData"; - private transient Text preprocessedText; + private final Text preprocessedText; public PreprocessingData(Text preprocessedText) { super(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/DiagramUtility.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/DiagramUtility.java index 3aea758ab..2c9b33d66 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/DiagramUtility.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/DiagramUtility.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common; import java.util.List; @@ -108,7 +108,7 @@ public static List getContainedBoxes(Box box, SortedMap boxes) */ public static Box addBox(Diagram diagram, String text) { TextBox textBox = new TextBox(0, 0, 0, 0, 1.0, text, null); - Box box = new Box(String.valueOf(diagram.getBoxes().size()), new int[] { 0, 0, 0, 0 }, 1.0, null, List.of(textBox), null); + Box box = new Box(diagram, String.valueOf(diagram.getBoxes().size()), new int[] { 0, 0, 0, 0 }, 1.0, null, List.of(textBox), null); diagram.addBox(box); return box; diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/JsonMapping.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/JsonMapping.java deleted file mode 100644 index 6bd14552b..000000000 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramconsistency/common/JsonMapping.java +++ /dev/null @@ -1,36 +0,0 @@ -/* Licensed under MIT 2023. */ -package edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common; - -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Utility class for getting {@link ObjectMapper}s. - */ -public final class JsonMapping { - /** - * Gets the default {@link ObjectMapper}. - */ - public static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); - - private JsonMapping() { - - } - - private static ObjectMapper createObjectMapper() { - ObjectMapper oom = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); - - oom.setVisibility(oom.getSerializationConfig() - .getDefaultVisibilityChecker() - .withFieldVisibility(JsonAutoDetect.Visibility.ANY) - .withGetterVisibility(JsonAutoDetect.Visibility.NONE) - .withSetterVisibility(JsonAutoDetect.Visibility.NONE) - .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE) - .withCreatorVisibility(JsonAutoDetect.Visibility.ANY)); - - return oom; - } -} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBox.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBox.java new file mode 100644 index 000000000..8bae24782 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBox.java @@ -0,0 +1,152 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.Optional; + +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityComparable; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +/** + * This record represents a 2-dimensional bounding box spanning from the top left point (minX, minY) to (maxX, maxY) in an image where top left is (0,0). + * + * @param minX x coordinate of the left bounding box edge + * @param minY y coordinate of the top bounding box edge + * @param maxX x coordinate of the right bounding box edge + * @param maxY y coordinate of the bottom bounding box edge + */ +public record BoundingBox(int minX, int minY, int maxX, int maxY) implements Comparable, SimilarityComparable, Serializable { + + /** + * Threshold used by the IoU to determine whether bounding boxes are similar using {@link #similar(GlobalConfiguration, BoundingBox)}. + */ + public static final double SIMILARITY_THRESHOLD = 0.7; + + /** + * Tries calculating a new bounding box of the intersection between this and another bounding box. If the bounding boxes do not intersect, an empty optional + * is provided. + * + * @param other another bounding box + * @return the optional bounding box + */ + public Optional intersect(BoundingBox other) { + if (minX() > other.maxX() || maxX() < other.minX() || minY() > other.maxY() || maxY() < other.minY()) + return Optional.empty(); + return Optional.of(new BoundingBox(Math.max(minX(), other.minX()), Math.max(minY(), other.minY()), Math.min(maxX(), other.maxX()), Math.min(maxY(), + other.maxY()))); + } + + /** + * Calculates the area of union between this and another bounding box. + * + * @param other another bounding box + * @return the area >= 0 + */ + public double union(BoundingBox other) { + var i = intersect(other).map(BoundingBox::area).orElse(0.0); + return area() + other.area() - i; + } + + /** + * Calculates the bounding box of this and another bounding box. + * + * @param other another bounding box + * @return a new bounding box + */ + public BoundingBox combine(BoundingBox other) { + return new BoundingBox(Math.min(minX, other.minX), Math.max(maxX, other.maxX), Math.min(minY, other.minY), Math.max(maxY, other.maxY)); + } + + /** + * @return the area of a bounding box, area >= 0 + */ + public double area() { + return ((double) maxX() - minX()) * (maxY() - minY()); + } + + /** + * Calculates the intersection over union metric for this bounding box with another bounding box. This metric is the fraction of the intersection with the + * union of two bounding boxes. If the bounding boxes are identical, this will produce return 1. If the bounding boxes do not intersect, it will return 0 + * instead. For any other case, a value in the range (0,1) is returned depending on the relative overlap. + * + * @param other another bounding box + * @return iou in the range [0,1] + */ + public double intersectionOverUnion(BoundingBox other) { + return intersect(other).map(i -> i.area() / union(other)).orElse(0.0); + } + + /** + * The percentage of another bounding box that is contained in this instance. + * + * @param other another bounding box + * @return percentage in the range [0,1] + */ + public double contains(BoundingBox other) { + return contains(other, false); + } + + /** + * The percentage of another bounding box that is contained in this instance. If the considerArea flag is set, a smaller box can not contain a larger box. + * Returns a value in the range [0,1] with 1 representing an entirely contained box. + * + * @param other a {@link BoundingBox} + * @param considerArea whether the area of both boxes should be considered + * @return percentage in the range [0,1] + */ + public double contains(BoundingBox other, boolean considerArea) { + if (considerArea && other.area() > area()) + return 0.0; + return intersect(other).map(i -> i.area() / other.area()).orElse(0.0); + } + + /** + * Whether this instance contains another bounding box entirely. + * + * @param other a {@link BoundingBox} + * @return true if contained entirely, false otherwise + */ + public boolean containsEntirely(BoundingBox other) { + return Double.compare(contains(other, true), 1.0) == 0; + } + + /** + * {@return the width} + */ + public int width() { + return maxX - minX; + } + + /** + * {@return the height} + */ + public int height() { + return maxY - minY; + } + + /** + * {@return the bounding box coordinates in the order minX, minY, maxX, maxY} + */ + public int[] toCoordinates() { + return new int[] { minX, minY, maxX, maxY }; + } + + @Override + public boolean similar(GlobalConfiguration globalConfiguration, BoundingBox obj) { + if (equals(obj)) + return true; + return intersectionOverUnion(obj) > SIMILARITY_THRESHOLD; + } + + @Override + public int compareTo(BoundingBox o) { + if (equals(o)) + return 0; + return Comparator.comparing(BoundingBox::minX) + .thenComparing(BoundingBox::minY) + .thenComparing(BoundingBox::maxX) + .thenComparing(BoundingBox::maxY) + .compare(this, o); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Box.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Box.java index 4f3d9feb9..a8cc95127 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Box.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Box.java @@ -1,23 +1,26 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; import static java.lang.Math.abs; +import java.awt.*; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.UUID; +import java.util.SortedSet; +import java.util.TreeSet; +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; /** * This class represents a box that is detected by the image recognition. */ -public final class Box implements Serializable { - @JsonProperty - private String uuid = UUID.randomUUID().toString(); +public class Box extends DiagramElement implements Serializable { // the four coordinates x1,y1,x2,y2 @JsonProperty("box") private int[] coordinates; @@ -29,24 +32,56 @@ public final class Box implements Serializable { private List textBoxes = new ArrayList<>(); @JsonProperty("contained") private List containedBoxes = new ArrayList<>(); - private transient Integer dominatingColor = null; + @JsonProperty("dominatingColor") + private Color dominatingColor = null; + @JsonIgnore + private SortedSet references = new TreeSet<>(); + + protected static String calculateUUID(int[] coordinates) { + return String.format("Box [%s]", getBoundingBoxConcat(coordinates)); + } + + /** + * {@return the bounding box coordinates joined with hyphens} + * + * @param coordinates bounding box coordinates + */ + public static String getBoundingBoxConcat(int[] coordinates) { + return Arrays.stream(coordinates).mapToObj((Integer::toString)).reduce((l, r) -> l + "-" + r).orElseThrow(); + } - private Box() { - // Jackson JSON + /** + * Create a new box that is detected on the image. + * + * @param diagram the diagram the box belongs to + * @param uuid the unique identifier of the box + * @param coordinates the coordinates of two corners of the box in pixel. (x1,y1,x2,y2) + * @param confidence a confidence value + * @param classification the classification (e.g., "LABEL"), see {@link Classification} for further details + * @param textBoxes the text boxes that are attached to this box + * @param dominatingColor a dominating color in the box (iff present) + */ + public Box(Diagram diagram, String uuid, int[] coordinates, double confidence, String classification, List textBoxes, Color dominatingColor) { + super(diagram, uuid); + this.coordinates = coordinates; + this.confidence = confidence; + this.classification = classification; + this.textBoxes = textBoxes; + this.dominatingColor = dominatingColor; } /** * Create a new box that is detected on the image. * - * @param uuid a unique identifier + * @param diagram the diagram the box belongs to * @param coordinates the coordinates of two corners of the box in pixel. (x1,y1,x2,y2) * @param confidence a confidence value * @param classification the classification (e.g., "LABEL"), see {@link Classification} for further details * @param textBoxes the text boxes that are attached to this box * @param dominatingColor a dominating color in the box (iff present) */ - public Box(String uuid, int[] coordinates, double confidence, String classification, List textBoxes, Integer dominatingColor) { - this.uuid = uuid; + public Box(Diagram diagram, int[] coordinates, double confidence, String classification, List textBoxes, Color dominatingColor) { + super(diagram, calculateUUID(coordinates)); this.coordinates = coordinates; this.confidence = confidence; this.classification = classification; @@ -54,11 +89,33 @@ public Box(String uuid, int[] coordinates, double confidence, String classificat this.dominatingColor = dominatingColor; } + /** + * Create a new box that is detected on the image. + * + * @param diagram the diagram this box belongs to + * @param uuid the unique identifier of the box + * @param coordinates the coordinates of two corners of the box in pixel. (x1,y1,x2,y2) + * @param confidence a confidence value + * @param classification the classification (e.g., "LABEL"), see {@link Classification} for further details + * @param dominatingColor a dominating color in the box (iff present) + */ + @JsonCreator + public Box(@JacksonInject Diagram diagram, @JsonProperty("uuid") String uuid, @JsonProperty("box") int[] coordinates, + @JsonProperty("confidence") double confidence, @JsonProperty("class") String classification, + @JsonProperty("dominatingColor") Color dominatingColor) { + super(diagram, uuid == null ? calculateUUID(coordinates) : uuid); + this.coordinates = coordinates; + this.confidence = confidence; + this.classification = classification; + this.dominatingColor = dominatingColor; + } + /** * Calculate the area of the box in square pixel. * * @return the area of the box */ + //TODO Delegate to bounding box class public int area() { return abs(coordinates[0] - coordinates[2]) * abs(coordinates[1] - coordinates[3]); } @@ -69,7 +126,8 @@ public int area() { * @return a UUID of the box */ public String getUUID() { - return uuid; + // We stored the UUID in name. + return getName(); } /** @@ -129,7 +187,7 @@ public List getContainedBoxes() { /** * Remove a text box that is associated with the box. - * + * * @param textBox the textbox */ public void removeTextBox(TextBox textBox) { @@ -146,12 +204,40 @@ public List getTexts() { return new ArrayList<>(textBoxes); } + public SortedSet getReferences() { + return new TreeSet<>(references); + } + + /** + * Adds a new reference to the set of references. + * + * @param reference the reference string + * @return true if the reference wasn't already contained, false otherwise + */ + public boolean addReference(String reference) { + return references.add(reference); + } + + public void setReferences(List references) { + this.references = new TreeSet<>(references); + } + + /** + * Tries to remove the given reference from the references + * + * @param reference the reference + * @return true if removed, false otherwise + */ + public boolean removeReference(String reference) { + return references.remove(reference); + } + /** * Get the dominating color of the box (iff present) * * @return the dominating color or {@code null} if not present */ - public Integer getDominatingColor() { + public Color getDominatingColor() { return dominatingColor; } @@ -160,7 +246,41 @@ public Integer getDominatingColor() { * * @param dominatingColor the dominating color */ - public void setDominatingColor(Integer dominatingColor) { + public void setDominatingColor(Color dominatingColor) { this.dominatingColor = dominatingColor; } + + @Override + public BoundingBox getBoundingBox() { + return new BoundingBox(coordinates[0], coordinates[1], coordinates[2], coordinates[3]); + } + + @Override + public String toString() { + return toString(true); + } + + /** + * Returns a string representation of the box, if wrap is set to true, the class name is appended before string + * + * @param wrap whether the class name should be appended + * @return a string representation of the box + */ + public String toString(boolean wrap) { + var formatted = String.format("%s %s", Arrays.stream(coordinates).mapToObj((Integer::toString)).reduce((l, r) -> l + "-" + r).orElseThrow(), + getReferences().stream().findFirst().orElse("REFERENCES_NOT_SET")); + if (wrap) + return String.format("Box [%s]", formatted); + return formatted; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) && obj instanceof Box otherBox && this.textBoxes.equals(otherBox.textBoxes); + } + + @Override + public int hashCode() { + return super.hashCode(); + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Connector.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Connector.java index 45f8cdda9..3ca10d7a3 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Connector.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Connector.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; import java.io.Serializable; @@ -9,7 +9,7 @@ public final class Connector implements Serializable { private String uuid = UUID.randomUUID().toString(); private List connectedBoxes; - private transient List texts = new ArrayList<>(); + private List texts = new ArrayList<>(); private Connector() { // Jackson JSON diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Diagram.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Diagram.java index 8d4d98e56..44f65547f 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Diagram.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/Diagram.java @@ -1,10 +1,31 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; import java.io.File; +import java.io.Serializable; import java.util.List; -public interface Diagram { +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityComparable; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +/** + * Programmatically represents an informal diagram. A diagram is uniquely identified by its (resource) name and can contain an arbitrary number of diagram + * elements and connectors between them. + */ +public interface Diagram extends Comparable, SimilarityComparable, Serializable { + /** + * {@return the full (resource) name of the diagram, e.g. "some/path/to/some-diagram.jpg"} + */ + String getResourceName(); + + /** + * {@return the short (resource) name of the diagram, e.g. "some-diagram.jpg"} + */ + default String getShortResourceName() { + var split = getResourceName().split("/|(\\\\)"); + return split[split.length - 1]; + } + File getLocation(); void addBox(Box box); @@ -19,7 +40,7 @@ public interface Diagram { /** * Returns the raw text boxes that are attached to this diagram. This method does not return the text boxes that are attached to the boxes of this diagram. - * + * * @return the raw text boxes that are attached to this diagram */ List getTextBoxes(); @@ -29,4 +50,21 @@ public interface Diagram { boolean removeConnector(Connector connector); List getConnectors(); + + @Override + default boolean similar(GlobalConfiguration globalConfiguration, Diagram obj) { + if (equals(obj)) + return true; + if (getResourceName().equals(obj.getResourceName())) { + return SimilarityComparable.similar(globalConfiguration, getBoxes(), obj.getBoxes()); + } + return false; + } + + @Override + default int compareTo(Diagram o) { + if (equals(o)) + return 0; + return getResourceName().compareTo(o.getResourceName()); + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramElement.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramElement.java new file mode 100644 index 000000000..345a7e36f --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramElement.java @@ -0,0 +1,119 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; +import org.eclipse.collections.impl.factory.SortedSets; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Entity; +import edu.kit.kastel.mcse.ardoco.core.api.models.ModelElement; +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityComparable; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +/** + * This box represents a geometrical shape with an arbitrary amount of text from a diagram. An element can be uniquely identified by its bounding box or UID and + * the diagram it belongs to. + */ +public abstract class DiagramElement extends Entity implements SimilarityComparable { + private final Diagram diagram; + private DiagramElement parent; + private List children; + + /** + * Creates a new diagram element that is associated with the given diagram and unique identifier. + * + * @param diagram the diagram this element is associated with + * @param uuid the unique identifier + */ + protected DiagramElement(Diagram diagram, String uuid) { + super(uuid); + this.diagram = diagram; + } + + /** + * Returns a {@link BoundingBox}, which encases the element. + * + * @return the {@link BoundingBox} + */ + public abstract BoundingBox getBoundingBox(); + + /** + * Returns the {@link Diagram}, which this element belongs to. + * + * @return the {@link Diagram} + */ + public Diagram getDiagram() { + return this.diagram; + } + + /** + * {@return the set of elements which are direct children of this diagram element} Determined indirectly by searching for diagram elements in the diagram + * which reference this element as their parent. + * + * @see #getParent() + */ + public ImmutableSortedSet getChildren() { + if (children == null) { + var all = getDiagram().getBoxes(); + this.children = new ArrayList<>(all.stream() + .filter(de -> !de.equals(DiagramElement.this) && de.getParent().map(p -> p == DiagramElement.this).orElse(false)) + .map(b -> (DiagramElement) b) + .toList()); + } + return SortedSets.immutable.withAll(children); + } + + /** + * {@return the optional parent of this element, empty if this diagram element is at the top-most level} Searches the diagram for diagram elements whose + * bounding box entirely contain this element. The diagram element with the smallest area is chosen as parent. + * + * @see BoundingBox#containsEntirely(BoundingBox) + */ + public Optional getParent() { + if (parent == null) { + var all = getDiagram().getBoxes(); + parent = all.stream() + .filter(de -> !de.equals(DiagramElement.this) && de.getBoundingBox().containsEntirely(getBoundingBox())) //Find boxes containing this element + .min(Comparator.comparingDouble(de -> de.getBoundingBox().area())) + .orElse(null); + } + return Optional.ofNullable(parent); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof DiagramElement other) { + return Objects.equals(getDiagram().getResourceName(), other.getDiagram().getResourceName()) && getBoundingBox().equals(other.getBoundingBox()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(getDiagram().getResourceName(), getBoundingBox()); + } + + @Override + public int compareTo(ModelElement o) { + if (equals(o)) + return 0; + if (o instanceof DiagramElement other) { + return Comparator.comparing(DiagramElement::getDiagram).thenComparing(DiagramElement::getBoundingBox).compare(this, other); + } + return super.compareTo(o); + } + + @Override + public boolean similar(GlobalConfiguration globalConfiguration, DiagramElement obj) { + if (equals(obj)) + return true; + if (diagram.getResourceName().equals(obj.diagram.getResourceName())) + return getBoundingBox().similar(globalConfiguration, obj.getBoundingBox()); + return false; + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramRecognitionState.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramRecognitionState.java index 08a867bfe..a235bf8f3 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramRecognitionState.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramRecognitionState.java @@ -1,8 +1,9 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; import java.util.List; +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; /** @@ -11,6 +12,26 @@ public interface DiagramRecognitionState extends PipelineStepData { String ID = "DiagramRecognition"; + /** + * Add a new diagram to the state that has not been processed yet. + * + * @param diagram the diagram + */ + void addUnprocessedDiagram(Diagram diagram); + + /** + * {@return an immutable list of unprocessed diagrams} + */ + List getUnprocessedDiagrams(); + + /** + * Removes a diagram from the unprocessed diagrams + * + * @param diagram the diagram to remove + * @return true, if the diagram was removed successfully + */ + boolean removeUnprocessedDiagram(Diagram diagram); + /** * Add a new diagram to the state. * @@ -24,4 +45,17 @@ public interface DiagramRecognitionState extends PipelineStepData { * @return all recognized diagrams */ List getDiagrams(); + + /** + * Add a disambiguation to the state. + * + * @param disambiguation the disambiguation + * @return Whether it was added successfully + */ + boolean addDisambiguation(Disambiguation disambiguation); + + /** + * {@return a list of disambiguations that were discovered during the stage} + */ + List getDisambiguations(); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramUtil.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramUtil.java new file mode 100644 index 000000000..6aa018d0d --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramUtil.java @@ -0,0 +1,87 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimUtils; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy.MaximumStrategy; + +/** + * Provides utility methods that are shared by {@link edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant informants} of diagram related stages. + */ +public class DiagramUtil { + private DiagramUtil() { + throw new IllegalStateException("Cannot be instantiated"); + } + + /** + * Returns the map containing the highest similarity of each word contained by the recommended instance to any word contained by the box. + * + * @param wordSimUtils the word similarity utils to use for comparison + * @param box the box the target words are extracted from + * @param recommendedInstance the recommended instance the words are extracted from + * @return the map + * @see #calculateSimilarityMap(WordSimUtils, SortedSet, SortedSet) + */ + public static SortedMap calculateSimilarityMap(WordSimUtils wordSimUtils, Box box, RecommendedInstance recommendedInstance) { + var deNames = box.getReferences(); + var words = new TreeSet<>(recommendedInstance.getNameMappings().stream().flatMap(nm -> nm.getWords().stream()).toList()); + return calculateSimilarityMap(wordSimUtils, words, deNames); + } + + /** + * {@return the map containing the highest similarity of each word to any word from the target words} + * + * @param wordSimUtils the word similarity utils to use for comparison + * @param words the words + * @param targets the target words + */ + private static SortedMap calculateSimilarityMap(WordSimUtils wordSimUtils, SortedSet words, SortedSet targets) { + var map = new TreeMap(); + words.forEach(w -> map.put(w, calculateHighestSimilarity(wordSimUtils, w, targets))); + + return new TreeMap<>(map); + } + + /** + * {@return the highest similarity of the word to any word from the target words} + * + * @param wordSimUtils the word similarity utils to use for comparison + * @param word the word + * @param targets the target words + */ + private static double calculateHighestSimilarity(WordSimUtils wordSimUtils, Word word, SortedSet targets) { + return targets.stream() + .map(name -> wordSimUtils.getSimilarity(word.getText(), name, new MaximumStrategy(), true)) + .max(Double::compareTo) + .orElse(Double.MIN_VALUE); + } + + /** + * {@return the highest similarity between a word from the noun mapping and a box reference} + * + * @param wordSimUtils the word similarity utils to use for comparison + * @param nounMapping the noun mapping + * @param box the box + */ + public static double calculateHighestSimilarity(WordSimUtils wordSimUtils, NounMapping nounMapping, Box box) { + return nounMapping.getReferenceWords().stream().map(word -> calculateHighestSimilarity(wordSimUtils, word, box)).max(Double::compareTo).orElse(0.0); + } + + /** + * {@return the highest similarity between the word and a box reference} + * + * @param wordSimUtils the word similarity utils to use for comparison + * @param word the word + * @param box the box + */ + public static double calculateHighestSimilarity(WordSimUtils wordSimUtils, Word word, Box box) { + return calculateHighestSimilarity(wordSimUtils, word, box.getReferences()); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBox.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBox.java index e1a953d89..c8ea5bcb5 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBox.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBox.java @@ -1,66 +1,79 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; +import java.awt.*; import java.io.Serializable; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public final class TextBox implements Serializable { - @JsonProperty("x") - private int xCoordinate; - @JsonProperty("y") - private int yCoordinate; - @JsonProperty("w") - private int width; - @JsonProperty("h") - private int height; - @JsonProperty - private double confidence; - @JsonProperty - private String text; - private transient Integer dominatingColor; - - private TextBox() { - // Jackson JSON - } - - public TextBox(int xCoordinate, int yCoordinate, int width, int height, double confidence, String text, Integer dominatingColor) { - this.xCoordinate = xCoordinate; - this.yCoordinate = yCoordinate; - this.width = width; - this.height = height; +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityComparable; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +/** + * A text + */ +public class TextBox implements SimilarityComparable, Serializable { + private static final double SIMILARITY_THRESHOLD = 0.85; + private final BoundingBox boundingBox; + private final double confidence; + private final String text; + private Color dominatingColor = null; + + public TextBox(BoundingBox boundingBox, double confidence, String text) { + this.boundingBox = boundingBox; this.confidence = confidence; this.text = text; + } + + public TextBox(BoundingBox boundingBox, double confidence, String text, Color dominatingColor) { + this(boundingBox, confidence, text); this.dominatingColor = dominatingColor; } + @JsonCreator + public TextBox(@JsonProperty("x") int xCoordinate, @JsonProperty("y") int yCoordinate, @JsonProperty("w") int width, @JsonProperty("h") int height, + @JsonProperty("confidence") double confidence, @JsonProperty("text") String text) { + this(new BoundingBox(xCoordinate, yCoordinate, xCoordinate + width, yCoordinate + height), confidence, text); + } + + public TextBox(int x, int y, int w, int h, double confidence, String text, Color dominatingColor) { + this(x, y, w, h, confidence, text); + this.setDominatingColor(dominatingColor); + } + /** * Get the coordinates of the absolute box as (x1,y1,x2,y2) in pixel. * * @return the absolute coordinates of the box */ public int[] absoluteBox() { - return new int[] { xCoordinate, yCoordinate, xCoordinate + width, yCoordinate + height }; + return new int[] { boundingBox.minX(), boundingBox.minY(), boundingBox.maxX(), boundingBox.maxY() }; + } + + public BoundingBox getBoundingBox() { + return boundingBox; } public int area() { - return width * height; + return getWidth() * getHeight(); } public int getXCoordinate() { - return xCoordinate; + return boundingBox.minX(); } public int getYCoordinate() { - return yCoordinate; + return boundingBox.minY(); } public int getWidth() { - return width; + return boundingBox.width(); } public int getHeight() { - return height; + return boundingBox.height(); } public double getConfidence() { @@ -71,11 +84,37 @@ public String getText() { return text; } - public Integer getDominatingColor() { + public Color getDominatingColor() { return dominatingColor; } - public void setDominatingColor(Integer dominatingColor) { + public void setDominatingColor(Color dominatingColor) { this.dominatingColor = dominatingColor; } + + @Override + public String toString() { + return String.format("TextBox [text=%s]", getText()); + } + + @Override + public boolean similar(GlobalConfiguration globalConfiguration, TextBox obj) { + if (equals(obj)) + return true; + return globalConfiguration.getWordSimUtils().getSimilarity(text, obj.text) > SIMILARITY_THRESHOLD && boundingBox.similar(globalConfiguration, + obj.boundingBox); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof TextBox other) { + return boundingBox.equals(other.boundingBox) && text.equals(other.text); + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return Objects.hash(boundingBox, text); + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/inconsistency/Inconsistency.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/inconsistency/Inconsistency.java index fd57890e1..27a5daea0 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/inconsistency/Inconsistency.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/inconsistency/Inconsistency.java @@ -1,12 +1,14 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.inconsistency; +import java.io.Serializable; + import org.eclipse.collections.api.collection.ImmutableCollection; /** * This interface represents an identified inconsistency of a certain type with a certain reason. */ -public interface Inconsistency { +public interface Inconsistency extends Serializable { /** * Returns the reason why there is an inconsistency @@ -23,12 +25,10 @@ public interface Inconsistency { String getType(); /** - * Return a list with String arrays as entries. The entries should have the format to first state the type of - * inconsistency, then the sentence number and third the id of the model element or the name of the text element (or - * both). Fourth entry can be an optional confidence value + * Return a list with String arrays as entries. The entries should have the format to first state the type of inconsistency, then the sentence number and + * third the id of the model element or the name of the text element (or both). Fourth entry can be an optional confidence value * - * @return List with String arrays as entry with the format {SentenceNumber, ModelElementId/TextElement, (optional) - * confidence}. + * @return List with String arrays as entry with the format {SentenceNumber, ModelElementId/TextElement, (optional) confidence}. */ ImmutableCollection toFileOutput(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/LegacyModelExtractionState.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/LegacyModelExtractionState.java index 5326eaf45..eae127f2b 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/LegacyModelExtractionState.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/LegacyModelExtractionState.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models; +import java.io.Serializable; + import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; @@ -12,7 +14,7 @@ * @deprecated use {@link ModelStates#getModel(String)} */ @Deprecated(since = "0.32.0") -public interface LegacyModelExtractionState extends IConfigurable { +public interface LegacyModelExtractionState extends IConfigurable, Serializable { /** * Returns the unique id of the model * diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelElement.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelElement.java index 82d4c9c2b..dc1306499 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelElement.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelElement.java @@ -1,6 +1,7 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models; +import java.io.Serializable; import java.util.Objects; import edu.kit.kastel.mcse.ardoco.core.common.IdentifierProvider; @@ -8,7 +9,8 @@ /** * A model element. Has an identifier. Two model elements are equal if and only if they have the same identifier. */ -public abstract class ModelElement implements Comparable { +public abstract class ModelElement implements Comparable, Serializable { + private final String id; protected ModelElement() { diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelStates.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelStates.java index 96935876e..e888e9b24 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelStates.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/ModelStates.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models; import java.util.SortedMap; @@ -14,8 +14,8 @@ public class ModelStates implements PipelineStepData { public static final String ID = "ModelStatesData"; - private transient SortedMap models = new TreeMap<>(); - private transient SortedMap legacyModels = new TreeMap<>(); + private SortedMap models = new TreeMap<>(); + private SortedMap legacyModels = new TreeMap<>(); /** * Constructor to create a {@link ModelStates} object that holds all {@link LegacyModelExtractionState}s diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/CodeModel.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/CodeModel.java index 6bc894a86..48cfc4dc2 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/CodeModel.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/CodeModel.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models.arcotl; import java.util.ArrayList; @@ -52,22 +52,18 @@ public CodeModel(CodeItemRepository codeItemRepository, SortedSet getContentIds() { - if (!initialized) - initialize(); + initialize(); return content; } @Override public List getContent() { - if (!initialized) - initialize(); + initialize(); return codeItemRepository.getCodeItemsFromIds(content); } @Override public List getEndpoints() { - if (!initialized) - initialize(); List compilationUnits = new ArrayList<>(); getContent().forEach(c -> compilationUnits.addAll(c.getAllCompilationUnits())); return compilationUnits; @@ -79,11 +75,11 @@ public List getEndpoints() { * @return all code packages of this code model */ public List getAllPackages() { - if (!initialized) - initialize(); List codePackages = new ArrayList<>(); - for (CodeItem c : getContent()) { - for (CodePackage cp : c.getAllPackages()) { + var lContent = getContent(); + for (CodeItem c : lContent) { + var allPackages = c.getAllPackages(); + for (CodePackage cp : allPackages) { if (!codePackages.contains(cp)) { codePackages.add(cp); } @@ -93,8 +89,11 @@ public List getAllPackages() { return codePackages; } - private void initialize() { + private synchronized void initialize() { + if (initialized) + return; this.codeItemRepository.init(); + initialized = true; } @Override diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItem.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItem.java index f3fa86abd..baa0f1c50 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItem.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItem.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.code; import java.util.ArrayList; @@ -43,7 +43,6 @@ protected CodeItem(CodeItemRepository codeItemRepository, String name) { } void registerCurrentCodeItemRepository(CodeItemRepository codeItemRepository) { - codeItemRepository.addCodeItem(this); this.codeItemRepository = codeItemRepository; } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItemRepository.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItemRepository.java index c10471796..05dac9d2f 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItemRepository.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/arcotl/code/CodeItemRepository.java @@ -1,21 +1,24 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.code; +import java.io.Serializable; import java.util.List; import java.util.Objects; import java.util.SortedMap; +import java.util.TreeMap; -import org.eclipse.collections.api.factory.SortedMaps; - +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -public class CodeItemRepository { +public class CodeItemRepository implements Serializable { @JsonProperty - private final SortedMap repository = SortedMaps.mutable.empty(); + private SortedMap repository = new TreeMap<>(); + @JsonIgnore + private boolean initialized = false; public SortedMap getRepository() { - return repository; + return new TreeMap<>(repository); } void addCodeItem(CodeItem codeItem) { @@ -36,7 +39,10 @@ public List getCodeItemsFromIds(List codeItemIds) { return codeItemIds.stream().map(this::getCodeItem).filter(Objects::nonNull).toList(); } - public void init() { + public synchronized void init() { + if (initialized) + return; this.repository.values().forEach(it -> it.registerCurrentCodeItemRepository(this)); + initialized = true; } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/EndpointTuple.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/EndpointTuple.java index ad90d8efb..8543917ed 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/EndpointTuple.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/EndpointTuple.java @@ -1,25 +1,22 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; +import java.io.Serializable; import java.util.Objects; import edu.kit.kastel.mcse.ardoco.core.api.models.Entity; /** - * A tuple of one architecture endpoint and one code endpoint. Every endpoint - * tuple is a possible candidate for the endpoints of a trace link that connects - * corresponding elements of an architecture model and a code model. An endpoint - * tuple cannot consist of two architecture endpoints or of two code endpoints. + * A tuple of one architecture endpoint and one code endpoint. Every endpoint tuple is a possible candidate for the endpoints of a trace link that connects + * corresponding elements of an architecture model and a code model. An endpoint tuple cannot consist of two architecture endpoints or of two code endpoints. */ -public class EndpointTuple { +public class EndpointTuple implements Serializable { private final Entity firstEndpoint; private final Entity secondEndpoint; /** - * @param firstEndpoint the architecture endpoint of the endpoint tuple - * to be created - * @param secondEndpoint the code endpoint of the endpoint tuple to be - * created + * @param firstEndpoint the architecture endpoint of the endpoint tuple to be created + * @param secondEndpoint the code endpoint of the endpoint tuple to be created */ public EndpointTuple(Entity firstEndpoint, Entity secondEndpoint) { this.firstEndpoint = firstEndpoint; diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceLink.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceLink.java index 818c5c6f9..9259c2d58 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceLink.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceLink.java @@ -1,9 +1,10 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; +import java.io.Serializable; import java.util.Objects; -public class TraceLink { +public class TraceLink implements Serializable { private final EndpointTuple endpointTuple; public TraceLink(EndpointTuple endpointTuple) { diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/recommendationgenerator/RecommendationStateStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/recommendationgenerator/RecommendationStateStrategy.java new file mode 100644 index 000000000..e2a606683 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/recommendationgenerator/RecommendationStateStrategy.java @@ -0,0 +1,10 @@ +/* Licensed under MIT 2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator; + +import java.io.Serializable; + +public interface RecommendationStateStrategy extends Serializable { + boolean areRITypesSimilar(String typeA, String typeB); + + boolean areRINamesSimilar(String nameA, String nameB); +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/DependencyTag.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/DependencyTag.java index 13bc90fe6..881059d4e 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/DependencyTag.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/DependencyTag.java @@ -1,10 +1,12 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.text; +import java.io.Serializable; + /** * All possible dependency tags in the framework. */ -public enum DependencyTag { +public enum DependencyTag implements Serializable { /** * An appositional modifier of an NP is an NP immediately to the right of the first NP that serves to define or diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Phrase.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Phrase.java index 624659bd9..c8ba64b61 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Phrase.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Phrase.java @@ -1,10 +1,12 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.text; +import java.io.Serializable; + import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; -public interface Phrase { +public interface Phrase extends Serializable, Comparable { int getSentenceNo(); String getText(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Sentence.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Sentence.java index 466b5bf22..604c7dcca 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Sentence.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Sentence.java @@ -1,12 +1,14 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.text; +import java.io.Serializable; + import org.eclipse.collections.api.list.ImmutableList; /** * Represents a sentence within the document. */ -public interface Sentence { +public interface Sentence extends Serializable { /** * Returns the sentence number (starting at {@code 0}. @@ -16,8 +18,7 @@ public interface Sentence { int getSentenceNumber(); /** - * Return the sentence number used for human readably output. - * Therefore, this method calculates the sentence number starting with {@code 1}. + * Return the sentence number used for human readably output. Therefore, this method calculates the sentence number starting with {@code 1}. * * @return the sentence number starting at one */ @@ -44,4 +45,11 @@ default boolean isEqualTo(Sentence other) { } ImmutableList getPhrases(); + + /** + * Adds a new phrase to the sentence + * + * @param phrase the phrase + */ + void addPhrase(Phrase phrase); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Text.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Text.java index 5800c256a..b1578185b 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Text.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Text.java @@ -1,12 +1,15 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.text; +import java.io.Serializable; + +import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.list.ImmutableList; /** * This interface defines the representation of a text. */ -public interface Text { +public interface Text extends Serializable { /** * Gets the length of the text (amount of words). @@ -24,6 +27,15 @@ default int getLength() { */ ImmutableList words(); + /** + * Gets all phrases of the text (ordered). + * + * @return the phrases + */ + default ImmutableList phrases() { + return Lists.immutable.fromStream(getSentences().stream().flatMap(s -> s.getPhrases().stream())); + } + /** * Returns the word at the given index * diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Word.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Word.java index 8c72b6292..270dd7997 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Word.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/text/Word.java @@ -1,15 +1,17 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.text; +import java.io.Serializable; + import org.eclipse.collections.api.list.ImmutableList; /** * The Interface IWord defines a word in a text. */ -public interface Word extends Comparable { +public interface Word extends Comparable, Serializable { /** - * Gets the sentence number. + * Gets the sentence number starting at 0. * * @return the sentence number */ @@ -51,6 +53,7 @@ public interface Word extends Comparable { Word getNextWord(); /** + * FIXME This description is confusing. Is this relative to the sentence or relative to the entire text? * Gets the position in the sentence / text. * * @return the position diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMapping.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMapping.java index 519099b38..d660dc30e 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMapping.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMapping.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.textextraction; +import java.io.Serializable; + import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; @@ -13,7 +15,7 @@ /** * The Interface INounMapping defines the mapping . */ -public interface NounMapping { +public interface NounMapping extends Serializable { /** * Returns the surface forms (previously called occurrences) of this mapping. @@ -64,7 +66,7 @@ public interface NounMapping { */ ImmutableList getMappingSentenceNo(); - ImmutableList getPhrases(); + ImmutableSortedSet getPhrases(); /** * Gets the probability for name. @@ -103,8 +105,8 @@ public interface NounMapping { void registerChangeListener(NounMappingChangeListener listener); /** - * Will be invoked during the deletion from a state. - * Note: This can be invoked multiple times if the replacement is not available during deletion of the noun mapping + * Will be invoked during the deletion from a state. Note: This can be invoked multiple times if the replacement is not available during deletion of the + * noun mapping * * @param replacement the replacing new noun mapping (or null if none exist) */ diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMappingChangeListener.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMappingChangeListener.java index a2b90f2dd..39bf9e661 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMappingChangeListener.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/NounMappingChangeListener.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.textextraction; -public interface NounMappingChangeListener { +import java.io.Serializable; + +public interface NounMappingChangeListener extends Serializable { void onDelete(NounMapping deletedNounMapping, NounMapping replacement); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseAbbreviation.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseAbbreviation.java new file mode 100644 index 000000000..2b7e0328b --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseAbbreviation.java @@ -0,0 +1,54 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.textextraction; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; + +/** + * An abbreviation with meanings that are phrases. For example, "ArDoCo" is an abbreviation of the phrase "Architecture Documentation Consistency". + */ +@Deterministic +public class PhraseAbbreviation extends Disambiguation { + private final LinkedHashSet phrases; + + public PhraseAbbreviation(String abbreviation, Set phrases) { + super(abbreviation, new TreeSet<>(phrases.stream() + .map(phrase -> phrase.getContainedWords().stream().map(Word::getText).collect(Collectors.joining(" "))) + .toList())); + this.phrases = new LinkedHashSet<>(phrases); + } + + /** + * Adds a phrase as meaning to the abbreviation + * + * @param phrase the phrase + */ + public void addPhrase(Phrase phrase) { + phrases.add(phrase); + } + + /** + * {@return the meanings, which are phrases} + */ + public SortedSet getPhrases() { + return new TreeSet<>(phrases); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMapping.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMapping.java index 1b1dec8db..f573e8368 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMapping.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMapping.java @@ -1,18 +1,21 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.textextraction; +import java.io.Serializable; + import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; +import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; import edu.kit.kastel.mcse.ardoco.core.api.text.PhraseType; import edu.kit.kastel.mcse.ardoco.core.api.text.Word; -public interface PhraseMapping { +public interface PhraseMapping extends Serializable { ImmutableList getNounMappings(TextState textState); - ImmutableList getPhrases(); + ImmutableSortedSet getPhrases(); PhraseType getPhraseType(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMappingChangeListener.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMappingChangeListener.java index 8d2254382..ec5510cc1 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMappingChangeListener.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/PhraseMappingChangeListener.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.textextraction; -public interface PhraseMappingChangeListener { +import java.io.Serializable; + +public interface PhraseMappingChangeListener extends Serializable { void onDelete(PhraseMapping deletedPhraseMapping, PhraseMapping replacement); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextState.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextState.java index b0f92f952..928c10369 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextState.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextState.java @@ -1,13 +1,21 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.textextraction; +import java.util.Optional; + +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.SortedSets; import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; import edu.kit.kastel.mcse.ardoco.core.api.text.Word; import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; +import edu.kit.kastel.mcse.ardoco.core.common.util.AbbreviationDisambiguationHelper; import edu.kit.kastel.mcse.ardoco.core.configuration.IConfigurable; import edu.kit.kastel.mcse.ardoco.core.data.Confidence; import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; @@ -17,35 +25,76 @@ * The Interface ITextState. */ public interface TextState extends IConfigurable, PipelineStepData { + Logger logger = LoggerFactory.getLogger(TextState.class); + String ID = "TextState"; /** - * * Adds a name mapping to the state. + * {@return the text state strategy of the text state} + */ + TextStateStrategy getTextStateStrategy(); + + /** + * Adds a name mapping to the state. * * @param word word of the mapping * @param kind the kind of the mapping * @param probability probability to be a name mapping */ - NounMapping addNounMapping(Word word, MappingKind kind, Claimant claimant, double probability); + default NounMapping addNounMapping(Word word, MappingKind kind, Claimant claimant, double probability) { + return getTextStateStrategy().addOrExtendNounMapping(word, kind, claimant, probability, Lists.immutable.with(word.getText())); + } /** - * * Adds a name mapping to the state. + * Adds a noun mapping of the specified kind to the state with the specified word and surface forms with the provided confidence. The adding and merging of + * the mapping is delegated to the {@link TextStateStrategy}. * * @param word word of the mapping * @param kind the kind of the mapping - * @param probability probability to be a name mapping + * @param claimant the claimant of the mapping + * @param probability probability to be a noun mapping of this kind * @param surfaceForms list of the appearances of the mapping */ - NounMapping addNounMapping(Word word, MappingKind kind, Claimant claimant, double probability, ImmutableList surfaceForms); + default NounMapping addNounMapping(Word word, MappingKind kind, Claimant claimant, double probability, ImmutableList surfaceForms) { + return getTextStateStrategy().addOrExtendNounMapping(word, kind, claimant, probability, surfaceForms); + } - NounMapping addNounMapping(ImmutableSortedSet words, MappingKind kind, Claimant claimant, double probability, ImmutableList referenceWords, - ImmutableList surfaceForms, String reference); + /** + * Adds a noun mapping of the specified kind to the state that contains the specified words, surface forms, etc. The adding and merging of the mapping is + * delegated to the {@link TextStateStrategy}. + * + * @param words words of the mapping + * @param kind kind of the mapping + * @param claimant claimant of the mapping + * @param probability probability to be a noun mapping of this kind + * @param referenceWords references of this noun mapping + * @param surfaceForms surface forms of this noun mapping + * @param reference a joined reference string + * @return the new or merged mapping + */ + default NounMapping addNounMapping(ImmutableSortedSet words, MappingKind kind, Claimant claimant, double probability, + ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { + return getTextStateStrategy().addNounMapping(words, kind, claimant, probability, referenceWords, surfaceForms, reference); + } - NounMapping addNounMapping(ImmutableSortedSet words, ImmutableSortedMap distribution, ImmutableList referenceWords, - ImmutableList surfaceForms, String reference); + /** + * Adds a noun mapping of the specified kind to the state that contains the specified words, surface forms, etc. The adding and merging of the mapping is + * delegated to the {@link TextStateStrategy}. + * + * @param words words of the mapping + * @param distribution distribution of the mapping for the mapping kinds + * @param referenceWords reference words of the mapping + * @param surfaceForms surface forms of the mapping + * @param reference a joined reference string + * @return the new or merged mapping + */ + default NounMapping addNounMapping(ImmutableSortedSet words, ImmutableSortedMap distribution, + ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { + return getTextStateStrategy().addNounMapping(words, distribution, referenceWords, surfaceForms, reference); + } /** - * Removes a noun mapping from the state. + * Removes a noun mapping from the state. Also removes phrase mappings that are associated with the noun mapping. * * @param nounMapping noun mapping to remove * @param replacement the (optional) future replacement of the noun mapping @@ -82,6 +131,15 @@ NounMapping addNounMapping(ImmutableSortedSet words, ImmutableSortedMap getPhraseMappings(); + /** + * {@return all phrase mappings containing a specific phrase} + * + * @param phrase the phrase + */ + default ImmutableList getPhraseMappings(Phrase phrase) { + return Lists.immutable.fromStream(getPhraseMappings().stream().filter(pm -> pm.getPhrases().contains(phrase))); + } + ImmutableList getNounMappingsOfKind(MappingKind mappingKind); ImmutableList getNounMappingsThatBelongToTheSamePhraseMapping(NounMapping nounMapping); @@ -108,4 +166,78 @@ void mergePhraseMappingsAndNounMappings(PhraseMapping phraseMapping, PhraseMappi boolean isWordContainedByMappingKind(Word word, MappingKind kind); ImmutableList getNounMappingsWithSimilarReference(String reference); + + /** + * Adds a {@link WordAbbreviation} for the abbreviation with the specified word meaning to the state using the state's {@link TextStateStrategy}. + * + * @param abbreviation the abbreviation + * @param word the meaning + * @return the resulting word abbreviation in the state + */ + default WordAbbreviation addWordAbbreviation(String abbreviation, Word word) { + logger.debug("Added word abbreviation for {} with meaning {}", abbreviation, word.getText()); + var wordAbbreviation = getTextStateStrategy().addOrExtendWordAbbreviation(abbreviation, word); + AbbreviationDisambiguationHelper.addTransient(wordAbbreviation); + return wordAbbreviation; + } + + /** + * Adds a {@link PhraseAbbreviation} for the abbreviation with the specified phrase meaning to the state using the state's {@link TextStateStrategy}. + * + * @param abbreviation the abbrevation + * @param phrase the meaning + * @return the resulting phrase abbreviation in the state + */ + default PhraseAbbreviation addPhraseAbbreviation(String abbreviation, Phrase phrase) { + logger.debug("Added phrase abbreviation for {} with meaning {}", abbreviation, phrase.getText()); + var phraseAbbreviation = getTextStateStrategy().addOrExtendPhraseAbbreviation(abbreviation, phrase); + AbbreviationDisambiguationHelper.addTransient(phraseAbbreviation); + return phraseAbbreviation; + } + + /** + * {@return all word abbreviations from the state} + */ + ImmutableSortedSet getWordAbbreviations(); + + /** + * {@return all word abbreviations from the state that have the specified meaning} + * + * @param word the meaning to search for + */ + default ImmutableSortedSet getWordAbbreviations(Word word) { + return SortedSets.immutable.ofAll(getWordAbbreviations().stream().filter(w -> w.getWords().contains(word)).toList()); + } + + /** + * {@return the optional word abbreviation from the state that has the specified abbreviation} + * + * @param abbreviation the abbreviation + */ + default Optional getWordAbbreviation(String abbreviation) { + return getWordAbbreviations().stream().filter(w -> w.getAbbreviation().equals(abbreviation)).findFirst(); + } + + /** + * {@return all phrase abbreviations from the state} + */ + ImmutableSortedSet getPhraseAbbreviations(); + + /** + * {@return all phrase abbreviations from the state that have the specified meaning} + * + * @param phrase the meaning to search for + */ + default ImmutableSortedSet getPhraseAbbreviations(Phrase phrase) { + return SortedSets.immutable.ofAll(getPhraseAbbreviations().stream().filter(p -> p.getPhrases().contains(phrase)).toList()); + } + + /** + * {@return the optional phrase abbreviation from the state that has the specified abbreviation} + * + * @param abbreviation the abbreviation + */ + default Optional getPhraseAbbreviation(String abbreviation) { + return getPhraseAbbreviations().stream().filter(p -> p.getAbbreviation().equals(abbreviation)).findFirst(); + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextStateStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextStateStrategy.java new file mode 100644 index 000000000..b6b85ddf7 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/TextStateStrategy.java @@ -0,0 +1,144 @@ +/* Licensed under MIT 2022-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.textextraction; + +import static edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions.AVERAGE; + +import java.io.Serializable; + +import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; +import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; + +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions; +import edu.kit.kastel.mcse.ardoco.core.data.Confidence; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; + +/** + * The Interface for strategies for the text state. Responsible for creating {@link NounMapping NounMappings}, and + * {@link edu.kit.kastel.mcse.ardoco.core.api.Disambiguation Disambiguations} from their constituent parts in a variety of situations. + */ +public interface TextStateStrategy extends Serializable { + /** + * Aggregation function used to aggregate multiple confidences into a single value + */ + AggregationFunctions DEFAULT_AGGREGATOR = AVERAGE; + + void setState(TextState textState); + + /** + * Tries to add a mapping to the state using the specified parameters. If a matching mapping already exists, the mapping is extended instead. + * + * @param word the word + * @param kind the kind + * @param claimant the claimant of the mapping + * @param probability the probability + * @param surfaceForms the surface forms + * @return the resulting noun mapping, either new or merged + */ + + NounMapping addOrExtendNounMapping(Word word, MappingKind kind, Claimant claimant, double probability, ImmutableList surfaceForms); + + /** + * Adds a mapping to the state using the specified parameters. Does not consider whether a matching mapping already exists. + * + * @param words the words + * @param distribution the distribution + * @param referenceWords the reference words + * @param surfaceForms the surface forms + * @param reference the reference, nullable + * @return the newly created noun mapping + */ + + NounMapping addNounMapping(ImmutableSortedSet words, ImmutableSortedMap distribution, ImmutableList referenceWords, + ImmutableList surfaceForms, String reference); + + /** + * Adds a mapping to the state using the specified parameters. Does not consider whether a matching mapping already exists. + * + * @param words the words + * @param kind the kind + * @param claimant the claimant + * @param probability the probability that the mapping is of this kind + * @param referenceWords the reference words + * @param surfaceForms the surface forms + * @param reference the reference, nullable + * @return the newly created noun mapping + */ + + NounMapping addNounMapping(ImmutableSortedSet words, MappingKind kind, Claimant claimant, double probability, ImmutableList referenceWords, + ImmutableList surfaceForms, String reference); + + /** + * Merges two noun mappings into a new mapping of the given kind, probability and claimant without adding it to the state. + * + * @param firstNounMapping the first mapping + * @param secondNounMapping the second mapping + * @param referenceWords reference words to use, nullable + * @param reference reference to use, nullable + * @param mappingKind the mapping kind + * @param claimant the claimant + * @param probability the probability + * @return the merged noun mapping + */ + + NounMapping mergeNounMappingsStateless(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, + MappingKind mappingKind, Claimant claimant, double probability); + + /** + * Merges two noun mappings into a new mapping of the given kind, probability and claimant and adds it to the state. + * + * @param firstNounMapping the first mapping + * @param secondNounMapping the second mapping + * @param referenceWords reference words to use, nullable + * @param reference reference to use, nullable + * @param mappingKind the mapping kind + * @param claimant the claimant + * @param probability the probability + * @return the merged noun mapping + */ + + NounMapping mergeNounMappings(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, + MappingKind mappingKind, Claimant claimant, double probability); + + /** + * Calculates a joined reference for a set of reference words. + * + * @param referenceWords the reference words + * @return a joined reference + */ + default String calculateNounMappingReference(ImmutableList referenceWords) { + StringBuilder refBuilder = new StringBuilder(); + referenceWords.toSortedListBy(Word::getPosition); + referenceWords.toSortedListBy(Word::getSentenceNo); + + for (int i = 0; i < referenceWords.size() - 1; i++) { + refBuilder.append(referenceWords.get(i).getText()).append(" "); + } + refBuilder.append(referenceWords.get(referenceWords.size() - 1).getText()); + return refBuilder.toString(); + } + + /** + * Tries to add a word abbreviation to the state. If the abbreviation already exists, it is extended. + * + * @param abbreviation the abbreviation + * @param word the word + * @return the resulting {@link edu.kit.kastel.mcse.ardoco.core.api.Disambiguation} in the stage + */ + + WordAbbreviation addOrExtendWordAbbreviation(String abbreviation, Word word); + + /** + * Tries to add a phrase abbreviation to the state. If the abbreviation already exists, it is extended. + * + * @param abbreviation the abbreviation + * @param phrase the phrase + * @return the resulting {@link edu.kit.kastel.mcse.ardoco.core.api.Disambiguation} in the stage + */ + + PhraseAbbreviation addOrExtendPhraseAbbreviation(String abbreviation, Phrase phrase); + + ImmutableList getNounMappingsWithSimilarReference(String reference); +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/WordAbbreviation.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/WordAbbreviation.java new file mode 100644 index 000000000..6edcbc8a9 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/textextraction/WordAbbreviation.java @@ -0,0 +1,50 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.textextraction; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; + +/** + * An abbreviation with meanings that are words. For example, "DB" is an abbreviation of the word "Database". + */ +@Deterministic +public class WordAbbreviation extends Disambiguation { + private final LinkedHashSet words; + + public WordAbbreviation(String abbreviation, Set words) { + super(abbreviation, new TreeSet<>(words.stream().map(Word::getText).toList())); + this.words = new LinkedHashSet<>(words); + } + + /** + * Adds a word as meaning to the abbreviation + * + * @param word the word + */ + public void addWord(Word word) { + words.add(word); + } + + /** + * {@return the meanings, which are words} + */ + public SortedSet getWords() { + return new TreeSet<>(words); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/AggregationFunctions.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/AggregationFunctions.java index 88651f355..4b413f1cb 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/AggregationFunctions.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/AggregationFunctions.java @@ -1,13 +1,14 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common; +import java.io.Serializable; import java.util.Collection; import java.util.function.ToDoubleFunction; /** * A set of various aggregation functions for collections of numbers. */ -public enum AggregationFunctions implements ToDoubleFunction> { +public enum AggregationFunctions implements ToDoubleFunction>, Serializable { /** * Use the median of the scores as final score. */ diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/JsonHandling.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/JsonHandling.java new file mode 100644 index 000000000..5ef5027e2 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/JsonHandling.java @@ -0,0 +1,26 @@ +/* Licensed under MIT 2024. */ +package edu.kit.kastel.mcse.ardoco.core.common; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +public final class JsonHandling { + private JsonHandling() { + throw new IllegalAccessError("Utility class"); + } + + public static ObjectMapper createObjectMapper() { + var objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.setVisibility(objectMapper.getSerializationConfig() + .getDefaultVisibilityChecker() // + .withFieldVisibility(JsonAutoDetect.Visibility.ANY) // + .withGetterVisibility(JsonAutoDetect.Visibility.NONE) // + .withSetterVisibility(JsonAutoDetect.Visibility.NONE) // + .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); + return objectMapper; + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Pair.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Pair.java index 36709cffe..fa0cd840d 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Pair.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Pair.java @@ -1,5 +1,7 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.tuple; -public record Pair(T first, U second) { +import java.io.Serializable; + +public record Pair(T first, U second) implements Serializable { } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Triple.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Triple.java index 3e3b3531a..a28514658 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Triple.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/tuple/Triple.java @@ -1,5 +1,7 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.tuple; -public record Triple(T first, U second, V third) { +import java.io.Serializable; + +public record Triple(T first, U second, V third) implements Serializable { } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/AbbreviationDisambiguationHelper.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/AbbreviationDisambiguationHelper.java new file mode 100644 index 000000000..94d880cbd --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/AbbreviationDisambiguationHelper.java @@ -0,0 +1,418 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.*; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; + +/** + * Provides functions to identify and disambiguate abbreviations. Caches results divided into a persistent {@link FileBasedCache} and a transient cache that is + * deleted on program exit. Abbreviations and their meanings are encapsulated as {@link Disambiguation}. The {@link FileBasedCache} is implemented in JSON and + * the file is saved in the user data directory folder of ArDoCo. The helper can be used to disambiguate an abbreviation using online abbreviation directory + * lookups. Such disambiguations are saved in the persistent cache. The transient cache is populated by the stages. When comparing two words, it is generally + * advised to ambiguate both rather than disambiguating. + */ +public final class AbbreviationDisambiguationHelper extends FileBasedCache> { + /** + * Matches abbreviations with up to 1 lowercase letter between uppercase letters. Accounts for camelCase by lookahead, e.g. UserDBAdapter is matched as "DB" + * rather than "DBA". Matches abbreviations at any point in the word, including at the start and end. + */ + private static final Pattern abbreviationsPattern = Pattern.compile("([A-Z]+[a-z]?)+[A-Z](?=([A-Z][a-z])|\\b)"); + /* + (?:([A-Z]+[a-z]?)+(?(?<=[A-Z])\w|[A-Z]))(?=([A-Z][a-z])|\b) + potential improvement, can also match ArDoCo for example, assumes that two letters with first letter capital is an abbr. such as Id, Db.. + */ + + public static final int LIMIT = 2; + private static final Logger logger = LoggerFactory.getLogger(AbbreviationDisambiguationHelper.class); + private static final String ABBREVIATIONS_COM = "https://www.abbreviations.com/"; + private static final String ACRONYM_FINDER_COM = "https://www.acronymfinder.com/Information-Technology/"; + private static AbbreviationDisambiguationHelper instance; + private static SortedMap ambiguated = new TreeMap<>(); + private static final SortedMap local = new TreeMap<>(); + + /** + * {@return the singleton instance of this class} + */ + static synchronized AbbreviationDisambiguationHelper getInstance() { + if (instance == null) { + instance = new AbbreviationDisambiguationHelper(); + } + return instance; + } + + private AbbreviationDisambiguationHelper() { + super("abbreviations", ".json", ""); + } + + /** + * Adds a disambiguation to the transient cache. If the abbreviation already exists, the disambiguations are merged instead. + * + * @param disambiguation the disambiguation + */ + public static void addTransient(Disambiguation disambiguation) { + local.merge(disambiguation.getAbbreviation(), disambiguation, Disambiguation::addMeanings); + ambiguated = new TreeMap<>(); + } + + /** + * Adds a disambiguation to the persistent cache. If the abbreviation already exists, the disambiguations are merged instead. + * + * @param disambiguation the disambiguation + */ + private static void addPersistent(Disambiguation disambiguation) { + //Specifically check. We do not want to mess up our files. + Objects.requireNonNull(disambiguation); + Objects.requireNonNull(disambiguation.getAbbreviation()); + Objects.requireNonNull(disambiguation.getMeanings()); + if (disambiguation.getAbbreviation().isBlank() || disambiguation.getMeanings().isEmpty() || disambiguation.getMeanings() + .stream() + .anyMatch(String::isBlank)) + return; + var disambiguations = getPersistent(); + disambiguations.merge(disambiguation.getAbbreviation(), disambiguation, Disambiguation::addMeanings); + try (var fbCache = getInstance()) { + fbCache.cache(disambiguations); + } + ambiguated = new TreeMap<>(); + } + + /** + * Tries to disambiguate the provided abbreviation and returns the potentially empty set of meanings + * + * @param abbreviation the abbreviation + * @return a set of meanings + */ + public static SortedSet disambiguate(String abbreviation) { + //Specifically check. We do not want to mess up our files. + Objects.requireNonNull(abbreviation); + if (abbreviation.isBlank()) + throw new IllegalArgumentException(); + + var fromTransientCache = getAll().getOrDefault(abbreviation, null); + if (fromTransientCache != null) + return fromTransientCache.getMeanings(); + + var fromPersistentCache = getPersistent().getOrDefault(abbreviation, null); + if (fromPersistentCache != null) + return fromPersistentCache.getMeanings(); + + var fromCrawl = crawl(abbreviation); + addPersistent(fromCrawl); + + return fromCrawl.getMeanings(); + } + + /** + * Replaces all meanings with their known abbreviation in a single string. The result is cached. + * + * @param text a text containing an arbitrary amount of meanings (can be zero) + * @param ignoreCase whether to ignore the casing when searching for a meaning inside the text + * @return a single string where all meanings have been replaced with known abbreviations + */ + public static String ambiguateAll(String text, boolean ignoreCase) { + return ambiguated.computeIfAbsent(text, key -> replaceAllMeanings(key, ignoreCase)); + } + + /** + * Replaces all meanings with their known abbreviation in a single string. For example, "Personal Computer Database" -> "PC DB" + * + * @param text a text containing an arbitrary amount of meanings (can be zero) + * @param ignoreCase whether to ignore the casing when searching for a meaning inside the text + * @return a single string where all meanings have been replaced with known abbreviations + */ + private static String replaceAllMeanings(String text, boolean ignoreCase) { + var replaced = text; + var disambiguations = getAll().values(); + for (var disambiguation : disambiguations) { + replaced = disambiguation.replaceMeaningWithAbbreviation(replaced, ignoreCase); + } + return replaced; + } + + private static SortedMap getPersistent() { + return getInstance().getOrRead(); + } + + /** + * {@return all disambiguations merged from both caches} + */ + public static SortedMap getAll() { + return new TreeMap<>(Disambiguation.merge(new TreeMap<>(local), new TreeMap<>(getPersistent()))); + } + + /** + * Crawls online abbreviation dictionaries for the abbreviation and combines their results. + * + * @param abbreviation the abbreviation + * @return a potentially empty set of meanings + */ + static Disambiguation crawl(String abbreviation) { + logger.info("Using crawler to disambiguate {}", abbreviation); + var meanings = crawlAcronymFinderCom(abbreviation); + meanings.addAll(crawlAbbreviationsCom(abbreviation)); + return new Disambiguation(abbreviation, meanings); + } + + /** + * Crawls abbreviations.com for the specified abbreviation. + * + * @param abbreviation the abbreviation + * @return a potentially empty set of meanings + */ + static SortedSet crawlAbbreviationsCom(String abbreviation) { + SortedSet meanings = new TreeSet<>(); + try { + Document document = Jsoup.connect(ABBREVIATIONS_COM + abbreviation).get(); + var elements = document.select("td > p.desc"); + meanings.addAll(elements.stream().limit(LIMIT).map(e -> removeAllBrackets(e.childNode(0).toString())).toList()); + logger.info("Crawler found {} -> {} on {}", abbreviation, meanings, ABBREVIATIONS_COM); + } catch (IOException e) { + logger.error(e.getCause().getMessage()); + } catch (IndexOutOfBoundsException e) { + logger.warn("Could not parse {} website document, did the layout change?", ABBREVIATIONS_COM); + } + return meanings; + } + + /** + * Crawls acronymfinder.com for the specified abbreviation. + * + * @param abbreviation the abbreviation + * @return a potentially empty set of meanings + */ + static SortedSet crawlAcronymFinderCom(String abbreviation) { + SortedSet meanings = new TreeSet<>(); + try { + Document document = Jsoup.connect(ACRONYM_FINDER_COM + abbreviation + ".html").get(); + var elements = document.select("td.result-list__body__meaning > a, td" + ".result-list__body__meaning"); + meanings.addAll(elements.stream().limit(LIMIT).map(Element::text).map(AbbreviationDisambiguationHelper::removeAllBrackets).toList()); + logger.info("Crawler found {} -> {} on {}", abbreviation, meanings, ACRONYM_FINDER_COM); + } catch (IOException e) { + logger.error(e.getCause().getMessage()); + } catch (IndexOutOfBoundsException e) { + logger.warn("Could not parse {} website document, did the layout change?", ACRONYM_FINDER_COM); + } + return meanings; + } + + @Override + protected SortedMap read() throws CacheException { + try { + logger.info("Reading abbreviations file"); + var read = toMap(createObjectMapper().readValue(getFile(), Disambiguation[].class)); + logger.info("Found {} cached abbreviation", read.size()); + return read; + } catch (IOException e) { + throw new CacheException(e); + } + } + + @Override + public SortedMap getDefault() { + return new TreeMap<>(); + } + + @Override + protected void write(SortedMap content) { + Collection values = content.values(); + try (PrintWriter out = new PrintWriter(getFile())) { + //Parse before writing to the file, so we don't mess up the entire file due to a parsing error + String json = createObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(values); + out.print(json); + logger.info("Saved abbreviations file"); + } catch (IOException e) { + logger.error(e.getCause().getMessage()); + } + } + + private static SortedMap toMap(Disambiguation[] abbreviations) { + var all = new TreeMap(); + for (var abbr : abbreviations) { + all.put(abbr.getAbbreviation(), abbr); + } + return all; + } + + /** + * Removes all brackets and the text between them, does not support nested brackets + * + * @param text the text + * @return the text without brackets + */ + private static String removeAllBrackets(String text) { + String prev = null; + String current = removeBracket(text); + while (!Objects.equals(prev, current)) { + prev = current; + current = removeBracket(current); + } + assert current != null; + return current.trim().replaceAll("\\s+", " "); + } + + /** + * Removes the first bracket and text between, does not support nested brackets + * + * @param text the text + * @return the text without the first bracket + */ + private static String removeBracket(String text) { + var innerMostOpen = text.indexOf("("); + var innerMostClose = text.indexOf(")"); + if (innerMostOpen == -1 || innerMostClose == -1 || innerMostOpen > innerMostClose) + return text; + + var start = text.substring(0, innerMostOpen); + var endBegin = innerMostClose + 1; + + if (endBegin > text.length()) + return start; + + var end = text.substring(endBegin); + return start + end; + } + + /** + * Uses the regex {@link #abbreviationsPattern} to find a set of possible abbreviations contained in the specified text. + * + * @param text the text + * @return a set of possible abbreviations + */ + public static SortedSet getAbbreviationCandidates(String text) { + var matcher = abbreviationsPattern.matcher(text); + return new TreeSet<>(matcher.results().map(MatchResult::group).toList()); + } + + /** + * {@return whether the initialism candidate is an initialism of the text} + * + * @param text the text + * @param initialismCandidate the initialism candidate + * @param initialismThreshold the percentage of characters in a word that need to be uppercase for a word to be considered an initialism candidate + */ + public static boolean isInitialismOf(String text, String initialismCandidate, double initialismThreshold) { + if (!couldBeAbbreviation(initialismCandidate, initialismThreshold)) + return false; + + //Check if the entire Initialism is contained within the single word + if (!text.contains(" ")) + return shareInitial(text, initialismCandidate) && containsAllInOrder(text, initialismCandidate); + + StringBuilder reg = new StringBuilder(); + var initialLcArray = initialismCandidate.toCharArray(); + for (var c : initialLcArray) { + reg.append(c).append("|"); + } + + var onlyInitialismLettersAndBlank = "\\[^(" + reg + "\\s)\\]"; + var split = text.split("\\s+"); + var reducedText = Arrays.stream(split).filter(s -> s.startsWith(onlyInitialismLettersAndBlank)).reduce("", (l, r) -> l + r); + + //The text contains words that are irrelevant to the supposed Initialism + if (reducedText.length() != split.length) + return false; + + return containsAllInOrder(reducedText, initialismCandidate); + } + + /** + * {@return whether the text could be an abbreviation} Compares the share of uppercase letters to the threshold. + * + * @param candidate the initialism candidate + * @param threshold the initialism threshold + */ + public static boolean couldBeAbbreviation(String candidate, double threshold) { + if (candidate.isEmpty()) + return false; + var upperCaseCharacters = 0; + var cArray = candidate.toCharArray(); + for (char c : cArray) { + if (Character.isUpperCase(c)) + upperCaseCharacters++; + } + return upperCaseCharacters >= threshold * candidate.length(); + } + + /** + * {@return whether the text contains all characters of the query in order} The characters do not have to be adjacent and can be separated by any amount of + * characters. + * + * @param text the text + * @param query the query + */ + public static boolean containsAllInOrder(String text, String query) { + return containsInOrder(text, query) == query.length(); + } + + /** + * {@return how many characters of the query are in order} The characters do not have to be adjacent and can be separated by any amount of characters. If a + * character is not in order, it is disregarded. + * + * @param text the text + * @param query the query + */ + public static long containsInOrder(String text, String query) { + return Math.round(maximumAbbreviationScore(text, query, 0, 0, 1, 0)); + } + + /** + * Calculates a score for how well the abbreviation matches the candidate meaning. A higher score indicates better. This function searches all character + * sequences in the meaning which match the abbreviation. Each character of the sequence is rewarded based on the provided parameters and their conditions. + * The maximum result of all such sequences is returned. For example, consider abbrev:"DB" and meaning:"Database". The sequence of characters which match + * the abbreviation are at index 0 and 4. Both characters are rewarded with anyMatch. Character 0 is additionally rewarded with initialMatch, because it is + * at a word boundary and with caseMatch, because its letter cases matches the abbreviation character. Thus, the sequence has a score of rewardInitialMatch + * + rewardCaseMatch + 2 * rewardAnyMatch. If another sequence with a higher score existed, its value would be returned instead. + * + * @param meaningCandidate the candidate meaning + * @param abbreviation the abbreviation + * @param rewardInitialMatch >= 0 + * @param rewardAnyMatch >= 0 + * @param rewardCaseMatch >= 0 + * @param textIndex start index, used for recursion, usually 0 at start + * @return the score >= 0 + */ + public static double maximumAbbreviationScore(String meaningCandidate, String abbreviation, double rewardInitialMatch, double rewardAnyMatch, + double rewardCaseMatch, int textIndex) { + if (abbreviation.isEmpty() || textIndex >= meaningCandidate.length()) + return 0; + var current = abbreviation.substring(0, 1); + var index = meaningCandidate.toLowerCase(Locale.ENGLISH).indexOf(current.toLowerCase(Locale.ENGLISH), textIndex); + if (index == -1) + return 0; + var score = maximumAbbreviationScore(meaningCandidate, abbreviation.substring(1), rewardInitialMatch, rewardAnyMatch, rewardCaseMatch, index + 1); + if (index == 0 || meaningCandidate.charAt(index - 1) == ' ') { + score += rewardInitialMatch; + } + if (meaningCandidate.substring(index, index + 1).equals(current)) { + score += rewardCaseMatch; + } + score += rewardAnyMatch; + return Math.max(score, maximumAbbreviationScore(meaningCandidate, abbreviation, rewardInitialMatch, rewardAnyMatch, rewardCaseMatch, index + 1)); + } + + /** + * Whether the two string share the same initial. + * + * @param a first string + * @param b second string + * @return true if yes, otherwise false + */ + public static boolean shareInitial(String a, String b) { + if (a == null || b == null || a.isEmpty() || b.isEmpty()) + return false; + return a.substring(0, 1).equals(b.substring(0, 1)); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CacheException.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CacheException.java new file mode 100644 index 000000000..b23e81928 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CacheException.java @@ -0,0 +1,16 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +/** + * Exception that can occur during read or write operations of a {@link FileBasedCache} + */ +public class CacheException extends Exception { + /** + * Constructor for cache exception + * + * @param cause the cause + */ + public CacheException(Throwable cause) { + super(cause); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonTextToolsConfig.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonTextToolsConfig.java index e0cc95ce3..56f216923 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonTextToolsConfig.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonTextToolsConfig.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; import org.eclipse.collections.api.list.ImmutableList; @@ -23,6 +23,11 @@ private CommonTextToolsConfig() { */ public static final ImmutableList SEPARATORS_TO_SPLIT = CONFIG.getPropertyAsList("separators_ToSplit"); + /** + * Decides whether abbrevations should be considered during similarity calculations. + */ + public static final boolean CONSIDER_ABBREVIATIONS = CONFIG.isPropertyEnabled("considerAbbreviations"); + /** * Decides whether the levenshtein similarity measure should be used. */ @@ -36,8 +41,7 @@ private CommonTextToolsConfig() { */ public static final int LEVENSHTEIN_MAX_DISTANCE = CONFIG.getPropertyAsInt("levenshtein_MaxDistance"); /** - * The levenshtein distance threshold which, multiplied with the length of the shortest word of a comparison, acts - * as a dynamic distance limit. + * The levenshtein distance threshold which, multiplied with the length of the shortest word of a comparison, acts as a dynamic distance limit. */ public static final double LEVENSHTEIN_THRESHOLD = CONFIG.getPropertyAsDouble("levenshtein_Threshold"); @@ -97,6 +101,14 @@ private CommonTextToolsConfig() { * The path to the sqlite database file used by the GloVe word similarity measure. */ public static final String GLOVE_DB_FILE_PATH = CONFIG.getProperty("glove_DatabaseFilePath"); + /** + * The threshold for a diagram element to be considered similar to a noun mapping. + */ + public static final double DE_NM_SIMILARITY_THRESHOLD = CONFIG.getPropertyAsDouble("de_NM_SimilarityThreshold"); + /** + * The threshold for a diagram element to be considered similar to a word. + */ + public static final double DE_WORD_SIMILARITY_THRESHOLD = CONFIG.getPropertyAsDouble("de_Word_SimilarityThreshold"); private static ResourceAccessor loadParameters(String filePath) { return new ResourceAccessor(filePath, true); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonUtilities.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonUtilities.java index 16c9aa9db..af434776d 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonUtilities.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/CommonUtilities.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; import java.io.File; @@ -218,13 +218,14 @@ public static void addRecommendedInstancesFromNounMappings(ImmutableList /** * Retrieves a list of similar types in the given model state given the word. * - * @param word the word that might have type names in the model state - * @param modelState the model state containing information about types + * @param similarityUtils the similarity utility instance + * @param word the word that might have type names in the model state + * @param modelState the model state containing information about types * @return List of type names in the model state that are similar to the given word */ - public static ImmutableList getSimilarTypes(Word word, LegacyModelExtractionState modelState) { + public static ImmutableList getSimilarTypes(SimilarityUtils similarityUtils, Word word, LegacyModelExtractionState modelState) { var identifiers = getTypeIdentifiers(modelState); - return Lists.immutable.fromStream(identifiers.stream().filter(typeId -> SimilarityUtils.areWordsSimilar(typeId, word.getText()))); + return Lists.immutable.fromStream(identifiers.stream().filter(typeId -> similarityUtils.areWordsSimilar(typeId, word.getText()))); } /** diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/Comparators.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/Comparators.java index 84e71b3bf..88d295976 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/Comparators.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/Comparators.java @@ -1,27 +1,97 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; import java.util.Collection; import org.eclipse.collections.api.collection.ImmutableCollection; +import org.eclipse.collections.api.ordered.SortedIterable; +/** + * Provides functions to compare collections regardless of order + */ public final class Comparators { private Comparators() { throw new IllegalAccessError(); } + /** + * {@return Whether both collections consist of equal elements (regardless of order)} + * + * @param first collection + * @param second collection + * @param Type of the collection + */ public static boolean collectionsEqualsAnyOrder(ImmutableCollection first, ImmutableCollection second) { return collectionsEqualsAnyOrder(first.castToCollection(), second.castToCollection()); } + /** + * {@return Whether both collections consist of equal elements (regardless of order)} + * + * @param first collection + * @param second collection + * @param Type of the collection + */ public static boolean collectionsEqualsAnyOrder(Collection first, Collection second) { return first.size() == second.size() && first.containsAll(second) && second.containsAll(first); } + /** + * {@return Whether both sorted iterables consist of equal elements (regardless of order)} + * + * @param first collection + * @param second collection + * @param Type of the collection + */ + public static boolean collectionsEqualsAnyOrder(SortedIterable first, SortedIterable second) { + var f = first.iterator(); + var s = second.iterator(); + while (f.hasNext() || s.hasNext()) { + if (f.hasNext() != s.hasNext()) + return false; + if (!f.next().equals(s.next())) + return false; + } + return true; + } + + /** + * {@return Whether both collections consist of the same references (regardless of order)} + * + * @param first collection + * @param second collection + * @param Type of the collection + */ public static boolean collectionsIdentityAnyOrder(ImmutableCollection first, ImmutableCollection second) { return collectionsIdentityAnyOrder(first.castToCollection(), second.castToCollection()); } + /** + * {@return Whether both sorted iterables consist of the same references (regardless of order)} + * + * @param first collection + * @param second collection + * @param Type of the collection + */ + public static boolean collectionsIdentityAnyOrder(SortedIterable first, SortedIterable second) { + var f = first.iterator(); + var s = second.iterator(); + while (f.hasNext() || s.hasNext()) { + if (f.hasNext() != s.hasNext()) + return false; + if (f.next() != s.next()) + return false; + } + return true; + } + + /** + * {@return Whether both collections consist of the same references (regardless of order)} + * + * @param first collection + * @param second collection + * @param Type of the collection + */ public static boolean collectionsIdentityAnyOrder(Collection first, Collection second) { return first.size() == second.size() && first.stream().allMatch(f -> second.stream().anyMatch(s -> f == s)); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DataRepositoryHelper.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DataRepositoryHelper.java index a615d8db8..e6dddde9f 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DataRepositoryHelper.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DataRepositoryHelper.java @@ -1,7 +1,13 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData; import edu.kit.kastel.mcse.ardoco.core.api.InputTextData; @@ -15,12 +21,13 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Text; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.data.DeepCopy; import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; import edu.kit.kastel.mcse.ardoco.core.data.ProjectPipelineData; /** - * This class helps to access {@link DataRepository DataRepositories}. It provides methods to access the different - * {@link PipelineStepData} that is stored within the repository that are used within ArDoCo. + * This class helps to access {@link DataRepository DataRepositories}. It provides methods to access the different {@link PipelineStepData} that is stored + * within the repository that are used within ArDoCo. */ public final class DataRepositoryHelper { @@ -39,9 +46,9 @@ public static boolean hasProjectPipelineData(DataRepository dataRepository) { } /** - * Returns the {@link ProjectPipelineData} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the data is not present. - * To make sure that there is data present, use {@link #hasProjectPipelineData(DataRepository)} + * Returns the {@link ProjectPipelineData} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and + * throw an {@link java.util.NoSuchElementException} if the data is not present. To make sure that there is data present, use + * {@link #hasProjectPipelineData(DataRepository)} * * @param dataRepository the DataRepository to access * @return the data @@ -51,8 +58,8 @@ public static ProjectPipelineData getProjectPipelineData(DataRepository dataRepo } /** - * Returns the input text as String stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the data is not present. + * Returns the input text as String stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and throw + * an {@link java.util.NoSuchElementException} if the data is not present. * * @param dataRepository the DataRepository to access * @return the text @@ -82,9 +89,9 @@ public static boolean hasAnnotatedText(DataRepository dataRepository) { } /** - * Returns the {@link Text} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the data is not present. - * To make sure that there is data present, use {@link #hasAnnotatedText(DataRepository)} + * Returns the {@link Text} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and throw an + * {@link java.util.NoSuchElementException} if the data is not present. To make sure that there is data present, use + * {@link #hasAnnotatedText(DataRepository)} * * @param dataRepository the DataRepository to access * @return the text @@ -104,9 +111,8 @@ public static boolean hasTextState(DataRepository dataRepository) { } /** - * Returns the {@link TextState} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasTextState(DataRepository)} + * Returns the {@link TextState} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and throw an + * {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use {@link #hasTextState(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -126,9 +132,9 @@ public static boolean hasModelStatesData(DataRepository dataRepository) { } /** - * Returns the {@link ModelStates} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasModelStatesData(DataRepository)} + * Returns the {@link ModelStates} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and throw + * an {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use + * {@link #hasModelStatesData(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -148,9 +154,9 @@ public static boolean hasRecommendationStates(DataRepository dataRepository) { } /** - * Returns the {@link RecommendationStates} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasRecommendationStates(DataRepository)} + * Returns the {@link RecommendationStates} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail + * and throw an {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use + * {@link #hasRecommendationStates(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -170,9 +176,9 @@ public static boolean hasConnectionStates(DataRepository dataRepository) { } /** - * Returns the {@link ConnectionStates} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasConnectionStates(DataRepository)} + * Returns the {@link ConnectionStates} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and + * throw an {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use + * {@link #hasConnectionStates(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -192,9 +198,9 @@ public static boolean hasInconsistencyStates(DataRepository dataRepository) { } /** - * Returns the {@link InconsistencyStates} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasInconsistencyStates(DataRepository)} + * Returns the {@link InconsistencyStates} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail and + * throw an {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use + * {@link #hasInconsistencyStates(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -217,9 +223,9 @@ public static boolean hasCodeTraceabilityState(DataRepository dataRepository) { } /** - * Returns the {@link CodeTraceabilityState} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasInconsistencyStates(DataRepository)} + * Returns the {@link CodeTraceabilityState} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail + * and throw an {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use + * {@link #hasInconsistencyStates(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -252,9 +258,9 @@ public static boolean hasDiagramRecognitionState(DataRepository dataRepository) } /** - * Returns the {@link DiagramRecognitionState} stored within the provided {@link DataRepository}. - * This does not check if there actually is one and will fail and throw an {@link java.util.NoSuchElementException} if the state is not present. - * To make sure that there is data present, use {@link #hasConnectionStates(DataRepository)} + * Returns the {@link DiagramRecognitionState} stored within the provided {@link DataRepository}. This does not check if there actually is one and will fail + * and throw an {@link java.util.NoSuchElementException} if the state is not present. To make sure that there is data present, use + * {@link #hasConnectionStates(DataRepository)} * * @param dataRepository the DataRepository to access * @return the state @@ -274,4 +280,21 @@ public static void putDiagramDirectory(DataRepository dataRepository, File diagr dataRepository.addData(InputDiagramData.ID, data); } + + /** + * {@return a deep copy of a serializable object using serialization} + * + * @param object the object to copy + */ + @DeepCopy + public static T deepCopy(T object) { + try { + var byteArrayOutputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(byteArrayOutputStream).writeObject(object); + var byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + return (T) new ObjectInputStream(byteArrayInputStream).readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DbPediaHelper.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DbPediaHelper.java new file mode 100644 index 000000000..01d9dbbbf --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/DbPediaHelper.java @@ -0,0 +1,216 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import org.apache.commons.compress.utils.Lists; +import org.apache.jena.query.ParameterizedSparqlString; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.ResultSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * This class provides lists of computer- and software-related terminology. It retrieves the terminology from the DBPedia ontology using SPARQL queries. The + * class caches the lists in a {@link FileBasedCache} in the user data directory folder of ArDoCo. + */ +public class DbPediaHelper extends FileBasedCache { + private static final Logger logger = LoggerFactory.getLogger(DbPediaHelper.class); + private static DbPediaHelper instance; + + /** + * {@return the singleton instance} + */ + static synchronized DbPediaHelper getInstance() { + if (instance == null) { + instance = new DbPediaHelper(); + } + return instance; + } + + private DbPediaHelper() { + super("dbpedia", ".json", ""); + } + + /** + * SPARQL query to retrieve programming languages from the Yago programming languages and DBOntology programming languages category. + * + * @return a list of programming languages + */ + private List loadProgrammingLanguages() { + ParameterizedSparqlString qs = new ParameterizedSparqlString(""" + prefix rdf: + prefix rdfs: + PREFIX dbo: + PREFIX yago: + + SELECT ?label + WHERE { + { + ?pl dbo:abstract ?abstract . + ?pl rdfs:label ?label . + ?pl rdf:type yago:ProgrammingLanguage106898352 . + FILTER (LANG(?abstract) = 'en') . + FILTER (LANG(?label)='en') + } + UNION + { + ?pl dbo:abstract ?abstract . + ?pl rdfs:label ?label . + ?pl dbo:influenced ?influenced . + ?pl dbo:influencedBy ?influencedBy . + ?pl rdf:type dbo:ProgrammingLanguage . + FILTER (LANG(?abstract) = 'en') . + FILTER (LANG(?label)='en') + } + } + GROUP BY ?label"""); + + var languages = runQuery(qs); + logger.info("Retrieved {} programming languages from DBPedia", languages.size()); + return languages; + } + + /** + * SPARQL query to retrieve markup languages from the Yago markup languages category. + * + * @return a list of markup languages + */ + private List loadMarkupLanguages() { + ParameterizedSparqlString qs = new ParameterizedSparqlString(""" + prefix rdf: + prefix rdfs: + PREFIX dbo: + PREFIX yago: + + SELECT ?label + WHERE { + ?pl dbo:abstract ?abstract . + ?pl rdfs:label ?label . + ?pl rdf:type yago:MarkupLanguage106787835 . + FILTER (LANG(?abstract) = 'en') . + FILTER (LANG(?label)='en') + } + GROUP BY ?label"""); + + var languages = runQuery(qs); + logger.info("Retrieved {} markup languages from DBPedia", languages.size()); + return languages; + } + + /** + * SPARQL query to retrieve softwares from the DBOntology software category. + * + * @return a list of softwares + */ + private List loadSoftware() { + ParameterizedSparqlString qs = new ParameterizedSparqlString(""" + prefix rdf: + prefix rdfs: + PREFIX dbo: + PREFIX yago: + + SELECT ?label + WHERE { + ?p rdf:type dbo:Software . + ?p dbo:programmingLanguage ?pl . + ?pl dbo:abstract ?abstract . + ?pl rdfs:label ?label . + FILTER (LANG(?abstract) = 'en') . + FILTER (LANG(?label)='en') + } + GROUP BY ?label"""); + + var software = runQuery(qs); + logger.info("Retrieved {} software from DBPedia", software.size()); + return software; + } + + /** + * {@return all labels retrieved by the SPARQL query} + * + * @param query the parameterized query + */ + private List runQuery(ParameterizedSparqlString query) { + var list = List.of(); + ResultSet results; + try (QueryExecution exec = QueryExecution.service("http://dbpedia.org/sparql").query(query.asQuery()).build()) { + results = exec.execSelect(); + var asList = Lists.newArrayList(results); + list = asList.stream().map(l -> l.getLiteral("label").getLexicalForm().replaceAll("\\((.*?)\\)", "").trim()).sorted().toList(); + } + return list; + } + + @Override + protected void write(DbPediaData r) { + try (PrintWriter out = new PrintWriter(getFile())) { + //Parse before writing to the file, so we don't mess up the entire file due to a parsing error + String json = createObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(r); + out.print(json); + logger.info("Saved {} file", getIdentifier()); + } catch (IOException e) { + logger.error(e.getCause().getMessage()); + } + } + + @Override + protected DbPediaData getDefault() { + return new DbPediaData(loadProgrammingLanguages(), loadMarkupLanguages(), loadSoftware()); + } + + @Override + protected DbPediaData read() throws CacheException { + try { + logger.info("Reading {} file", getIdentifier()); + return createObjectMapper().readValue(getFile(), new TypeReference<>() { + }); + } catch (IOException e) { + logger.error("Error reading {} file", getIdentifier()); + throw new CacheException(e); + } + } + + /** + * Record used for caching + * + * @param programmingLanguages the list of programming languages + * @param markupLanguages the list of markup languages + * @param software the list of software + */ + protected record DbPediaData(List programmingLanguages, List markupLanguages, List software) { + } + + /** + * {@return whether a word is a programming language} + * + * @param word the word + */ + public static boolean isWordProgrammingLanguage(String word) { + return getInstance().getOrRead().programmingLanguages().stream().anyMatch(s -> s.replaceAll("\\s+", "").equalsIgnoreCase(word.replaceAll("\\s+", ""))); + } + + /** + * {@return whether a word is a markup language} + * + * @param word the word + */ + public static boolean isWordMarkupLanguage(String word) { + return getInstance().getOrRead().markupLanguages().stream().anyMatch(s -> s.replaceAll("\\s+", "").equalsIgnoreCase(word.replaceAll("\\s+", ""))); + } + + /** + * {@return whether a word is a software} + * + * @param word the word + */ + public static boolean isWordSoftware(String word) { + return getInstance().getOrRead().software().stream().anyMatch(s -> s.replaceAll("\\s+", "").equalsIgnoreCase(word.replaceAll("\\s+", ""))); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FileBasedCache.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FileBasedCache.java new file mode 100644 index 000000000..d5f98e007 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FileBasedCache.java @@ -0,0 +1,199 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This abstract class provides the structure for a file-based cache. Only one instance should be created for each cache file. The cache files are saved in the + * user data directory folder of ArDoCo. + * + * @param the type of cached content + */ +public abstract class FileBasedCache implements AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger(FileBasedCache.class); + private static final String TEMP_DIR = loadTempDir(); + + private File file; + private final String identifier; + private final String fileExtension; + private final String subFolder; + private boolean flagWrite = false; + private T currentState = null; + private int originalStateHash; + + /** + * Writes the content to the file at {@link #getFile()} + * + * @param content the content + */ + protected abstract void write(T content); + + /** + * {@return the cached content} + */ + public T getOrRead() { + if (currentState == null) { + T fileState = null; + try { + fileState = read(); + } catch (CacheException e) { + try { + resetFile(); + fileState = read(); + } catch (CacheException ex) { + // If resetting doesn't solve the issue, fail entirely + throw new IllegalStateException(ex); + } + } + originalStateHash = Objects.hash(fileState); + currentState = fileState; + } + return currentState; + } + + /** + * Caches the content. This does not write it to the disk immediately. + * + * @param content the content + */ + public void cache(T content) { + currentState = content; + flagWrite = true; + } + + /** + * Constructor for the file-based cache + * + * @param identifier name of the cache file + * @param fileExtension extension of the cache file + * @param subFolder sub-folder in the user directory, must end with {@link File#separator} + */ + protected FileBasedCache(String identifier, String fileExtension, String subFolder) { + this.identifier = identifier; + this.fileExtension = fileExtension; + if (!subFolder.isEmpty() && !subFolder.endsWith(File.separator)) + throw new IllegalArgumentException(); + this.subFolder = subFolder; + } + + /** + * Reads the content of the file at {@link #getFile()} + * + * @throws CacheException thrown if an error occurs while reading + */ + protected abstract T read() throws CacheException; + + /** + * {@return the default content that is written if the file is reset using {@link #resetFile()}} + */ + protected abstract T getDefault(); + + /** + * {@return the name of the cache file} + */ + public String getIdentifier() { + return this.identifier; + } + + /** + * Resets the cache file to default content. + */ + public void resetFile() { + try { + deleteFile(); + getFile(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * Deletes the file at {@link #getFileHandle()} + * + * @return whether the file was deleted successfully by the file system + */ + protected boolean deleteFile() { + try { + if (file == null) + file = getFileHandle(); + return Files.deleteIfExists(file.toPath()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + /** + * {@return the file handle of the cache file} + * + * @throws IOException on a file system exception + */ + protected File getFileHandle() throws IOException { + + file = new File(TEMP_DIR + File.separator + subFolder + this.identifier + this.fileExtension); + if (file.getParentFile().mkdirs()) { + logger.info("Created directory {}", file.getParentFile().getCanonicalPath()); + } + return file; + } + + /** + * {@return the cache file} + * + * @throws IOException on a file system exception + */ + protected File getFile() throws IOException { + if (file != null && file.exists()) + return file; + + file = getFileHandle(); + + if (file.createNewFile()) { + logger.info("Created {} file {}", this.identifier, file.getCanonicalPath()); + T defaultContent = getDefault(); + write(defaultContent); + } + + return file; + } + + private static String loadTempDir() { + try { + Path tempDir = Files.createTempDirectory("ArDoCo"); + logger.info("Created temporary directory {}", tempDir); + addDeleteHook(tempDir); + return tempDir.toString(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private static void addDeleteHook(Path tempDir) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + Files.walk(tempDir).map(Path::toFile).forEach(File::delete); + } catch (IOException e) { + throw new IllegalStateException(e); + } + })); + } + + @Override + public void close() { + if (flagWrite) { + if (currentState == null) { + deleteFile(); + } else { + if (Objects.hash(currentState) != originalStateHash) { + write(currentState); + } + } + } + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/PhraseMappingAggregatorStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/PhraseMappingAggregatorStrategy.java index 48ee8c0aa..87e0955aa 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/PhraseMappingAggregatorStrategy.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/PhraseMappingAggregatorStrategy.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; import static edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils.cosineSimilarity; @@ -11,17 +11,17 @@ @Deterministic public enum PhraseMappingAggregatorStrategy implements ToDoubleBiFunction { - MAX_SIMILARITY((a, b) -> uniqueDot(a.getPhrases(), b.getPhrases()).stream() + MAX_SIMILARITY((a, b) -> uniqueDot(a.getPhrases().toImmutableList(), b.getPhrases().toImmutableList()).stream() .mapToDouble(p -> cosineSimilarity(p.first().getPhraseVector().toSortedMap(), p.second().getPhraseVector().toSortedMap())) .max() .orElse(Double.NaN)), // - MIN_SIMILARITY((a, b) -> uniqueDot(a.getPhrases(), b.getPhrases()).stream() + MIN_SIMILARITY((a, b) -> uniqueDot(a.getPhrases().toImmutableList(), b.getPhrases().toImmutableList()).stream() .mapToDouble(p -> cosineSimilarity(p.first().getPhraseVector().toSortedMap(), p.second().getPhraseVector().toSortedMap())) .min() .orElse(Double.NaN)), // - AVG_SIMILARITY((a, b) -> uniqueDot(a.getPhrases(), b.getPhrases()).stream() + AVG_SIMILARITY((a, b) -> uniqueDot(a.getPhrases().toImmutableList(), b.getPhrases().toImmutableList()).stream() .mapToDouble(p -> cosineSimilarity(p.first().getPhraseVector().toSortedMap(), p.second().getPhraseVector().toSortedMap())) .average() .orElse(Double.NaN)); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SerializableFileBasedCache.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SerializableFileBasedCache.java new file mode 100644 index 000000000..b21decb66 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SerializableFileBasedCache.java @@ -0,0 +1,89 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import java.io.*; +import java.lang.reflect.ParameterizedType; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * A {@link FileBasedCache} that is implemented using Java's default serialization. + * + * @param serializable content + */ +public class SerializableFileBasedCache extends FileBasedCache { + private static final Logger logger = LoggerFactory.getLogger(SerializableFileBasedCache.class); + + private final Class contentClass; + + /** + * Creates a new serializable file based cache that contains content of the given class and is saved in a file with the given identifier and sub-folder. + * + * @param contentClass the class of serializable content + * @param identifier the identifier of the cache + * @param subFolder the sub-folder of the cache + */ + public SerializableFileBasedCache(Class contentClass, String identifier, String subFolder) { + super(identifier, ".ser", subFolder + contentClass.getSimpleName() + File.separator); + this.contentClass = contentClass; + } + + public SerializableFileBasedCache(TypeReference typeReference, String identifier, String subFolder) { + super(identifier, ".ser", subFolder + sanitizeFileName(processTypeReference(typeReference).getSimpleName()) + File.separator); + this.contentClass = (Class) processTypeReference(typeReference); + } + + private static String sanitizeFileName(String name) { + var noForbiddenChars = name.replaceAll("[\\\\/:*?\"<>|]", ""); + return noForbiddenChars.replace('.', '-'); + } + + private static Class processTypeReference(TypeReference typeReference) { + var type = typeReference.getType(); + if (type instanceof ParameterizedType parameterizedType) { + type = parameterizedType.getRawType(); + } + if (type instanceof Class cls) { + return cls; + } else { + throw new IllegalArgumentException("TypeReference type could not be resolved to a class"); + } + } + + @Override + protected void write(T content) { + try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(getFile()))) { + out.writeObject(content); + logger.info("Saved {} file", getIdentifier()); + } catch (IOException e) { + logger.error("Error reading file", e); + } + } + + @Override + protected @Nullable T read() throws CacheException { + try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(getFile()))) { + logger.info("Reading {} file", getIdentifier()); + var dObj = in.readObject(); + if (dObj == null || contentClass.isInstance(dObj)) { + return (T) dObj; + } + throw new ClassCastException(); + } catch (InvalidClassException | ClassNotFoundException | ClassCastException | EOFException e) { + logger.warn("SerialVersionUID might have changed, resetting {} file", getIdentifier()); + throw new CacheException(e); + } catch (IOException e) { + logger.error("Error reading {} file", getIdentifier()); + throw new CacheException(e); + } + } + + @Override + protected T getDefault() { + return null; + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityComparable.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityComparable.java new file mode 100644 index 000000000..07785056e --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityComparable.java @@ -0,0 +1,39 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import java.util.Collection; + +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +/** + * Classes implementing this interface provide the functionality to determine whether an instance is similar to the provided type, and if collections of + * instances are similar to collections of the type. + * + * @param the type + */ +public interface SimilarityComparable { + /** + * {@return whether the instance is similar to the given object} Has to return true if {@link Object#equals} returns true. The result of this function + * should be symmetric, but does not have to be transitive. + * + * @param globalConfiguration the pipeline meta data containing the similarity configuration + * @param obj some object + */ + boolean similar(GlobalConfiguration globalConfiguration, T obj); + + /** + * {@return both collections consist of elements, that have a corresponding similar element in the other collection} Does not care about order and should + * returns true for equal lists if {@link #similar(GlobalConfiguration, Object)} was implemented correctly. + * + * @param globalConfiguration the pipeline meta data containing the similarity configuration + * @param a some collection + * @param b some other collection + */ + static > boolean similar(GlobalConfiguration globalConfiguration, Collection a, Collection b) { + if (a.equals(b)) + return true; + if (a.size() != b.size()) + return false; + return a.parallelStream().allMatch(element -> b.stream().anyMatch(other -> element.similar(globalConfiguration, other))); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityUtils.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityUtils.java index eb7126171..ea698edeb 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityUtils.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/SimilarityUtils.java @@ -1,6 +1,7 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -27,10 +28,18 @@ * This class is a utility class. */ @Deterministic -public final class SimilarityUtils { +public final class SimilarityUtils implements Serializable { + private final WordSimUtils wordSimUtils; - private SimilarityUtils() { - throw new IllegalAccessError(); + public SimilarityUtils(WordSimUtils wordSimUtils) { + this.wordSimUtils = wordSimUtils; + } + + public ImmutableList getSimilarSurfaceWords(RecommendedInstance recommendedInstance, ModelInstance instance) { + return Lists.immutable.fromStream(recommendedInstance.getNameMappings() + .stream() + .flatMap(n -> n.getSurfaceForms().stream()) + .filter(s -> wordSimUtils.areWordsSimilar(s, instance.getFullName()))); } /** @@ -40,7 +49,7 @@ private SimilarityUtils() { * @param nm2 the second NounMapping * @return true, if the {@link NounMapping}s are similar; false if not. */ - public static boolean areNounMappingsSimilar(NounMapping nm1, NounMapping nm2) { + public boolean areNounMappingsSimilar(NounMapping nm1, NounMapping nm2) { var nm1Words = nm1.getReferenceWords(); var nm2Words = nm2.getReferenceWords(); var nm1Reference = nm1.getReference(); @@ -63,14 +72,14 @@ public static boolean areNounMappingsSimilar(NounMapping nm1, NounMapping nm2) { } /** - * Compares a given {@link NounMapping} with a given {@link ModelInstance} for similarity. Checks if all names, the - * longest name or a single name are similar to the reference of the NounMapping. + * Compares a given {@link NounMapping} with a given {@link ModelInstance} for similarity. Checks if all names, the longest name or a single name are + * similar to the reference of the NounMapping. * * @param nounMapping the {@link NounMapping} * @param instance the {@link ModelInstance} * @return true, iff the {@link NounMapping} and {@link ModelInstance} are similar. */ - public static boolean isNounMappingSimilarToModelInstance(NounMapping nounMapping, ModelInstance instance) { + public boolean isNounMappingSimilarToModelInstance(NounMapping nounMapping, ModelInstance instance) { if (areWordsOfListsSimilar(instance.getNameParts(), Lists.immutable.with(nounMapping.getReference())) || areWordsSimilar(instance.getFullName(), nounMapping.getReference())) { return true; @@ -91,7 +100,7 @@ public static boolean isNounMappingSimilarToModelInstance(NounMapping nounMappin * @param instance the {@link ModelInstance} * @return true, iff the {@link Word} and {@link ModelInstance} are similar. */ - public static boolean isWordSimilarToModelInstance(Word word, ModelInstance instance) { + public boolean isWordSimilarToModelInstance(Word word, ModelInstance instance) { var names = instance.getNameParts(); return compareWordWithStringListEntries(word, names); } @@ -103,7 +112,7 @@ public static boolean isWordSimilarToModelInstance(Word word, ModelInstance inst * @param instance the {@link ModelInstance} * @return true, iff the {@link RecommendedInstance} and {@link ModelInstance} are similar. */ - public static boolean isRecommendedInstanceSimilarToModelInstance(RecommendedInstance ri, ModelInstance instance) { + public boolean isRecommendedInstanceSimilarToModelInstance(RecommendedInstance ri, ModelInstance instance) { var name = ri.getName(); var nameList = Lists.immutable.with(name.split(" ")); return instance.getFullName().equalsIgnoreCase(ri.getName()) || areWordsOfListsSimilar(instance.getNameParts(), nameList); @@ -116,24 +125,25 @@ public static boolean isRecommendedInstanceSimilarToModelInstance(RecommendedIns * @param instance the {@link ModelInstance} * @return true, iff the {@link Word} and the type of the {@link ModelInstance} are similar. */ - public static boolean isWordSimilarToModelInstanceType(Word word, ModelInstance instance) { + public boolean isWordSimilarToModelInstanceType(Word word, ModelInstance instance) { var types = instance.getTypeParts(); return compareWordWithStringListEntries(word, types); } - private static boolean compareWordWithStringListEntries(Word word, ImmutableList names) { - if (areWordsOfListsSimilar(names, Lists.immutable.with(word.getText()))) { - return true; - } + private boolean compareWordWithStringListEntries(Word word, ImmutableList names) { + return compareWordWithStringListEntries(word.getText(), names); + } + private boolean compareWordWithStringListEntries(String word, ImmutableList names) { for (String name : names) { - if (areWordsSimilar(name, word.getText())) { + if (areWordsSimilar(name, word)) { return true; } } return false; } + //FIXME this method is a duplicate of an existing method in WordSimUtils and should be removed /** * Checks the similarity of two {@link Word}s. * @@ -141,12 +151,11 @@ private static boolean compareWordWithStringListEntries(Word word, ImmutableList * @param word2 the second word * @return true, if the words are similar; false if not. */ - public static boolean areWordsSimilar(Word word1, Word word2) { - var word1Text = word1.getText(); - var word2Text = word2.getText(); - return areWordsSimilar(word1Text, word2Text); + public boolean areWordsSimilar(Word word1, Word word2) { + return wordSimUtils.areWordsSimilar(word1, word2); } + //FIXME this method is a duplicate of an existing method in WordSimUtils and should be removed /** * Checks the similarity of two string. Uses Jaro-Winkler similarity and Levenshtein to assess the similarity. * @@ -154,63 +163,70 @@ public static boolean areWordsSimilar(Word word1, Word word2) { * @param word2 String of second word * @return true, if the test string is similar to the original; false if not. */ - public static boolean areWordsSimilar(String word1, String word2) { - return WordSimUtils.areWordsSimilar(word1, word2); + public boolean areWordsSimilar(String word1, String word2) { + return wordSimUtils.areWordsSimilar(word1, word2); } /** - * Checks the similarity of a list with test strings to a list of "original" strings. In this method all test - * strings are compared to all originals. For this the method uses the areWordsSimilar method with a given - * threshold. All matches are counted. If the proportion of similarities between the lists is greater than the given - * threshold the method returns true. + * Checks the similarity of a list with test strings to a list of "original" strings. In this method all test strings are compared to all originals. For + * this the method uses the areWordsSimilar method with a given threshold. All matches are counted. If the proportion of similarities between the lists is + * greater than the given threshold the method returns true. * * @param originals list of original strings * @param words2test list of test strings * @param minProportion threshold for proportional similarity between the lists * @return true if the list are similar, false if not */ - public static boolean areWordsOfListsSimilar(ImmutableList originals, ImmutableList words2test, double minProportion) { + public boolean areWordsOfListsSimilar(ImmutableList originals, ImmutableList words2test, double minProportion) { if (areWordsSimilar(String.join(" ", originals), String.join(" ", words2test))) { return true; } - var counter = 0; + var max = Math.max(originals.size(), words2test.size()); + var counterSimilar = 0; + var counterDissimilar = 0; + var possiblySimilar = originals.size() * words2test.size(); for (String o : originals) { for (String wd : words2test) { if (areWordsSimilar(o, wd)) { - counter++; + counterSimilar++; + if (1.0 * counterSimilar / max >= minProportion) + return true; + } else { + counterDissimilar++; + if (1.0 * (possiblySimilar - counterDissimilar) / max < minProportion) + return false; //minProportion can no longer be achieved, can stop here } } } - return 1.0 * counter / Math.max(originals.size(), words2test.size()) >= minProportion; + return 1.0 * counterSimilar / max >= minProportion; } /** - * Checks the similarity of a list, containing test strings, and a list of originals. This check is not - * bidirectional! This method uses the areWordsSimilar method with a given threshold. + * Checks the similarity of a list, containing test strings, and a list of originals. This check is not bidirectional! This method uses the areWordsSimilar + * method with a given threshold. * * @param originals list of original strings * @param words2test list of test strings * @return true if the list are similar, false if not */ - public static boolean areWordsOfListsSimilar(ImmutableList originals, ImmutableList words2test) { + public boolean areWordsOfListsSimilar(ImmutableList originals, ImmutableList words2test) { return areWordsOfListsSimilar(originals, words2test, CommonTextToolsConfig.JAROWINKLER_SIMILARITY_THRESHOLD); } /** - * Extracts most likely matches of a list of recommended instances by similarity to a given instance. For this, the - * method uses an increasing minimal proportional threshold with the method areWordsOfListsSimilar method. If all - * lists are similar to the given instance by a threshold of 1-increase value the while loop can be left. If the - * while loop ends with more than one possibility or all remaining lists are sorted out in the same run, all are + * Extracts most likely matches of a list of recommended instances by similarity to a given instance. For this, the method uses an increasing minimal + * proportional threshold with the method areWordsOfListsSimilar method. If all lists are similar to the given instance by a threshold of 1-increase value + * the while loop can be left. If the while loop ends with more than one possibility or all remaining lists are sorted out in the same run, all are * returned. Elsewhere only the remaining recommended instance is returned within the list. * * @param instance instance to use as original for compare * @param recommendedInstances recommended instances to check for similarity * @return a list of the most similar recommended instances (to the instance names) */ - public static ImmutableList getMostRecommendedInstancesToInstanceByReferences(ModelInstance instance, + public ImmutableList getMostRecommendedInstancesToInstanceByReferences(ModelInstance instance, ImmutableList recommendedInstances) { var instanceNames = instance.getNameParts(); var similarity = CommonTextToolsConfig.JAROWINKLER_SIMILARITY_THRESHOLD; @@ -231,7 +247,7 @@ public static ImmutableList getMostRecommendedInstancesToIn allListsSimilar++; } - if (!SimilarityUtils.areWordsOfListsSimilar(instanceNames, Lists.immutable.with(ri.getName()), getMostRecommendedIByRefMinProportion)) { + if (!this.areWordsOfListsSimilar(instanceNames, Lists.immutable.with(ri.getName()), getMostRecommendedIByRefMinProportion)) { risToRemove.add(ri); } } @@ -248,7 +264,7 @@ public static ImmutableList getMostRecommendedInstancesToIn } - private static boolean checkRecommendedInstanceWordSimilarityToInstance(ModelInstance instance, RecommendedInstance ri) { + private boolean checkRecommendedInstanceWordSimilarityToInstance(ModelInstance instance, RecommendedInstance ri) { var instanceNames = instance.getNameParts(); for (var sf : ri.getNameMappings().flatCollect(NounMapping::getSurfaceForms)) { var splitSF = CommonUtilities.splitCases(String.join(" ", CommonUtilities.splitAtSeparators(sf))); @@ -259,14 +275,14 @@ private static boolean checkRecommendedInstanceWordSimilarityToInstance(ModelIns return false; } - private static boolean checkRecommendedInstanceForSelection(ModelInstance instance, RecommendedInstance ri, double similarity) { + private boolean checkRecommendedInstanceForSelection(ModelInstance instance, RecommendedInstance ri, double similarity) { var instanceNames = instance.getNameParts(); ImmutableList longestNameSplit = Lists.immutable.of(CommonUtilities.splitCases(instance.getFullName()).split(" ")); ImmutableList recommendedInstanceNames = Lists.immutable.with(ri.getName()); boolean instanceNameAndRIName = areWordsSimilar(instance.getFullName(), ri.getName()); - boolean instanceNamesAndRIs = SimilarityUtils.areWordsOfListsSimilar(instanceNames, recommendedInstanceNames, similarity); - boolean longestNameSplitAndRINames = SimilarityUtils.areWordsOfListsSimilar(longestNameSplit, recommendedInstanceNames, similarity); + boolean instanceNamesAndRIs = this.areWordsOfListsSimilar(instanceNames, recommendedInstanceNames, similarity); + boolean longestNameSplitAndRINames = this.areWordsOfListsSimilar(longestNameSplit, recommendedInstanceNames, similarity); boolean listOfNamesSimilarEnough = 1.0 * similarEntriesOfList(instanceNames, recommendedInstanceNames) / Math.max(instanceNames.size(), recommendedInstanceNames.size()) >= similarity; boolean listOfNameSplitSimilarEnough = 1.0 * similarEntriesOfList(longestNameSplit, recommendedInstanceNames) / Math.max(instanceNames.size(), @@ -280,8 +296,8 @@ private static boolean checkRecommendedInstanceForSelection(ModelInstance instan var splitSurfaceForm = CommonUtilities.splitCases(surfaceForm); var surfaceFormWords = CommonUtilities.splitAtSeparators(splitSurfaceForm); - boolean instanceNamesXSurfaceForms = SimilarityUtils.areWordsOfListsSimilar(instanceNames, surfaceFormWords, similarity); - boolean longestNameXSurfaceForms = SimilarityUtils.areWordsOfListsSimilar(longestNameSplit, surfaceFormWords, similarity); + boolean instanceNamesXSurfaceForms = this.areWordsOfListsSimilar(instanceNames, surfaceFormWords, similarity); + boolean longestNameXSurfaceForms = this.areWordsOfListsSimilar(longestNameSplit, surfaceFormWords, similarity); boolean listOfNamesXSurfaceFormSimilarEnough = 1.0 * similarEntriesOfList(instanceNames, surfaceFormWords) / Math.max(instanceNames.size(), surfaceFormWords.size()) >= similarity; boolean listOfSplitNamesXSurfaceFormSimilarEnough = 1.0 * similarEntriesOfList(longestNameSplit, surfaceFormWords) / Math.max(longestNameSplit @@ -303,7 +319,7 @@ private static boolean coversOtherPhraseVector(PhraseMapping phraseMapping1, Phr return phraseVector1.keysView().containsAll(phraseVector2.keysView().toSortedSet()); } - private static boolean containsAllNounMappingsOfPhraseMapping(TextState textState, PhraseMapping phraseMapping1, PhraseMapping phraseMapping2) { + private boolean containsAllNounMappingsOfPhraseMapping(TextState textState, PhraseMapping phraseMapping1, PhraseMapping phraseMapping2) { return phraseMapping1.getNounMappings(textState).containsAllIterable(phraseMapping2.getNounMappings(textState)); } @@ -321,7 +337,7 @@ static double cosineSimilarity(Map firstPhraseVector, Map otherPhraseMappings, + public PhraseMapping getMostSimilarPhraseMapping(TextState textState, PhraseMapping phraseMapping, ImmutableList otherPhraseMappings, double minCosineSimilarity) { if (otherPhraseMappings.isEmpty()) { @@ -341,7 +357,7 @@ public static PhraseMapping getMostSimilarPhraseMapping(TextState textState, Phr return mostSimilarPhraseMapping; } - public static ImmutableList> uniqueDot(ImmutableList first, ImmutableList second) { + public static ImmutableList> uniqueDot(ImmutableList first, ImmutableList second) { List> result = new ArrayList<>(); for (A a : first) for (B b : second) @@ -349,7 +365,7 @@ public static ImmutableList> uniqueDot(ImmutableList first, return Lists.immutable.withAll(result); } - public static double getPhraseMappingSimilarity(TextState textState, PhraseMapping firstPhraseMapping, PhraseMapping secondPhraseMapping, + public double getPhraseMappingSimilarity(TextState textState, PhraseMapping firstPhraseMapping, PhraseMapping secondPhraseMapping, PhraseMappingAggregatorStrategy strategy) { PhraseType firstPhraseType = firstPhraseMapping.getPhraseType(); PhraseType secondPhraseType = secondPhraseMapping.getPhraseType(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ComparisonContext.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ComparisonContext.java index 3712fed78..519aebfe2 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ComparisonContext.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ComparisonContext.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; import java.util.Objects; @@ -6,35 +6,60 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Word; /** - * A ComparisonContext contains all information that can be used for comparing similarity between objects that occur - * within ArDoCo. The fields {@link #firstString} and {@link #secondString} are always not null. The field - * {@link #lemmatize} decides whether the lemmatized version of both words should be used for comparison. + * A ComparisonContext contains all information that can be used for comparing similarity between objects that occur within ArDoCo. The fields + * {@link #firstString} and {@link #secondString} are always not null. The field {@link #lemmatize} decides whether the lemmatized version of both words should + * be used for comparison. The field {@link #characterMatch} provides a function to determine whether two {@link UnicodeCharacter UnicodeCharacters} are + * considered to be a match by the {@link WordSimMeasure WordSimMeasures}. */ -public record ComparisonContext(String firstString, String secondString, Word firstWord, Word secondWord, boolean lemmatize) { +public record ComparisonContext(String firstString, String secondString, Word firstWord, Word secondWord, boolean lemmatize, + UnicodeCharacterMatchFunctions characterMatch) { + /** + * Constructs a string-based context with a given match function and no lemmatization. + * + * @param firstString the first string + * @param secondString the second string + * @param characterMatch the match function + */ + public ComparisonContext(String firstString, String secondString, UnicodeCharacterMatchFunctions characterMatch) { + this(firstString, secondString, null, null, false, characterMatch); + } + + /** + * Constructs a string-based context with the default match function and no lemmatization. + * + * @param firstString the first string + * @param secondString the second string + */ public ComparisonContext(String firstString, String secondString) { - this(firstString, secondString, null, null, false); + this(firstString, secondString, null, null, false, UnicodeCharacterMatchFunctions.EQUAL); } + /** + * Constructs a string-based context with the default match function. + * + * @param firstString the first string + * @param secondString the second string + * @param lemmatize whether the string should be lemmatized + */ public ComparisonContext(String firstString, String secondString, boolean lemmatize) { - this(firstString, secondString, null, null, lemmatize); + this(firstString, secondString, null, null, lemmatize, UnicodeCharacterMatchFunctions.EQUAL); } + /** + * Constructs a word-based context with the default match function. + * + * @param firstWord the first word + * @param secondWord the second word + * @param lemmatize whether the words should be lemmatized + */ public ComparisonContext(Word firstWord, Word secondWord, boolean lemmatize) { - this(firstWord.getText(), secondWord.getText(), firstWord, secondWord, lemmatize); - } - - public ComparisonContext(String firstString, String secondString, Word firstWord, Word secondWord, boolean lemmatize) { - this.firstString = Objects.requireNonNull(firstString); - this.secondString = Objects.requireNonNull(secondString); - this.firstWord = firstWord; - this.secondWord = secondWord; - this.lemmatize = lemmatize; + this(firstWord.getText(), secondWord.getText(), firstWord, secondWord, lemmatize, UnicodeCharacterMatchFunctions.EQUAL); } /** - * Finds the most appropriate string representation by the first object in this comparison object. This method can - * be used as a shorthand to avoid going through all variables that could possibly represent the first object. + * Finds the most appropriate string representation by the first object in this comparison object. This method can be used as a shorthand to avoid going + * through all variables that could possibly represent the first object. * * @return the most appropriate string presentation of the first object in this comparison */ @@ -44,8 +69,8 @@ public String firstTerm() { } /** - * Finds the most appropriate string representation by the second object in this comparison object. This method can - * be used as a shorthand to avoid going through all variables that could possibly represent the second object. + * Finds the most appropriate string representation by the second object in this comparison object. This method can be used as a shorthand to avoid going + * through all variables that could possibly represent the second object. * * @return the most appropriate string presentation of the second object in this comparison */ diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ConfusablesHelper.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ConfusablesHelper.java new file mode 100644 index 000000000..2ad3fe169 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ConfusablesHelper.java @@ -0,0 +1,102 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.MissingResourceException; +import java.util.stream.Stream; + +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.impl.factory.Lists; +import org.eclipse.collections.impl.list.mutable.FastList; + +/** + * This class provides functionality regarding confusables and homoglyphs of a {@link UnicodeCharacter}. The information is based on the confusablesSummary.txt, that is published as part of the Unicode Technical Standard. + */ +public class ConfusablesHelper { + private ConfusablesHelper() { + throw new IllegalStateException("Cannot be instantiated"); + } + + private static final LinkedHashMap> homoglyphs = new LinkedHashMap<>(); + + private static final String CONFUSABLES_SUMMARY = "/wordsim/confusablesSummary.txt"; + + private static final String SEPARATOR = "\t"; + + static { + parseConfusablesSummary(); + } + + /** + * {@return the list of homoglyphs contained in a line} + * + * @param line the line + */ + static FastList extractHomoglyphsFromLine(String line) { + if (!line.startsWith("#" + SEPARATOR)) + return FastList.newList(); + + MutableList confusables = Lists.mutable.of(line.split("\\R|\\s")); + confusables.remove(0); //Remove leading # symbol + + // TODO skip confusables that consist of multiple unicode characters + // Filter because only homoglyphs are interesting + return FastList.newList(confusables.stream() + .filter(c -> c.codePointCount(0, c.length()) == 1) + .mapToInt(c -> c.codePointAt(0)) + .mapToObj(UnicodeCharacter::valueOf) + .toList()); + } + + /** + * Parses the confusablesSummary.txt line by line and build the confusables map. + */ + private static void parseConfusablesSummary() { + try (InputStream is = ConfusablesHelper.class.getResourceAsStream(CONFUSABLES_SUMMARY)) { + if (is == null) + throw new MissingResourceException("Could not find the resource " + CONFUSABLES_SUMMARY, File.class.getSimpleName(), CONFUSABLES_SUMMARY); + try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + var extracted = extractHomoglyphsFromLine(line); + if (!extracted.isEmpty()) { + for (var unicodeCharacter : extracted) { + homoglyphs.merge(unicodeCharacter, extracted, (oldL, newL) -> FastList.newList(Stream.concat(oldL.stream(), newL.stream()) + .toList())); + } + } + } + } + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * {@return the list of Unicode characters that are considered homoglyphs of the character} + * + * @param unicodeCharacter the character + */ + public static List getHomoglyphs(UnicodeCharacter unicodeCharacter) { + return homoglyphs.getOrDefault(unicodeCharacter, FastList.newList()); + } + + /** + * {@return whether two Unicode characters are considered homoglyphs} Always true for equal characters. The relationship is symmetric, but not transitive. + * + * @param a the first character + * @param b the second character + */ + public static boolean areHomoglyphs(UnicodeCharacter a, UnicodeCharacter b) { + return a.equals(b) || getHomoglyphs(a).contains(b); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacter.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacter.java new file mode 100644 index 000000000..b2ec664ae --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacter.java @@ -0,0 +1,85 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; + +import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.impl.factory.Lists; + +/** + * Represents a Unicode character corresponding to a particular Unicode code point. Refer to the + * Unicode Glossary + * and the Java {@link Character} documentation for an explanation of code points. Java {@link Character} instances and the corresponding primitive {@code char} + * can not represent all unicode characters in a single instance due to historic reasons. + */ +//TODO More documentation +public final class UnicodeCharacter implements Serializable { + private static final LinkedHashMap integerToUnicode = new LinkedHashMap<>(); + + private final int codePoint; + + public int getCodePoint() { + return codePoint; + } + + private final String representation; + + public String getRepresentation() { + return representation; + } + + public static ImmutableList from(String input) { + return Lists.immutable.fromStream(Arrays.stream(input.codePoints().toArray()).mapToObj(UnicodeCharacter::valueOf)); + } + + public static String toString(List unicodeCharacters) { + return unicodeCharacters.stream().map(UnicodeCharacter::toString).reduce("", (a, b) -> a + b); + } + + public static String toUnicodeCharacter(int codePoint) { + return Character.toString(codePoint); + } + + public String toString() { + return toUnicodeCharacter(codePoint); + } + + public static UnicodeCharacter valueOf(int codePoint) { + return integerToUnicode.computeIfAbsent(codePoint, UnicodeCharacter::new); + } + + public static UnicodeCharacter valueOf(String representation) { + if (representation.codePointCount(0, representation.length()) != 1) { + throw new IllegalArgumentException(String.format("%s is not a valid unicode character", representation)); + } + var codePoint = representation.codePointAt(0); + return integerToUnicode.computeIfAbsent(codePoint, UnicodeCharacter::new); + } + + private UnicodeCharacter(int codePoint) { + this(codePoint, toUnicodeCharacter(codePoint)); + } + + private UnicodeCharacter(int codePoint, String representation) { + this.codePoint = codePoint; + this.representation = representation; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof UnicodeCharacter oth) + return this.codePoint == oth.codePoint; + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(codePoint); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacterMatchFunctions.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacterMatchFunctions.java new file mode 100644 index 000000000..4bb407417 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacterMatchFunctions.java @@ -0,0 +1,25 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; + +import java.io.Serializable; +import java.util.function.BiFunction; + +public enum UnicodeCharacterMatchFunctions implements BiFunction, Serializable { + EQUAL(UnicodeCharacter::equals), + + EQUAL_OR_HOMOGLYPH((a, b) -> a.equals(b) || ConfusablesHelper.areHomoglyphs(a, b)); + + private final BiFunctionSerializable function; + + UnicodeCharacterMatchFunctions(BiFunctionSerializable function) { + this.function = function; + } + + @Override + public Boolean apply(UnicodeCharacter unicodeCharacter, UnicodeCharacter unicodeCharacter2) { + return function.apply(unicodeCharacter, unicodeCharacter2); + } + + public interface BiFunctionSerializable extends BiFunction, Serializable { + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacterSequence.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacterSequence.java new file mode 100644 index 000000000..44560fbb3 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/UnicodeCharacterSequence.java @@ -0,0 +1,67 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; + +import java.util.Objects; +import java.util.stream.IntStream; + +import org.eclipse.collections.api.list.ImmutableList; + +/** + * {@link UnicodeCharacter} equivalent of {@link CharSequence}. + * + * @param characters list containing the sequence + */ +public record UnicodeCharacterSequence(ImmutableList characters) { + public static UnicodeCharacterSequence valueOf(String input) { + return new UnicodeCharacterSequence(UnicodeCharacter.from(input)); + } + + public UnicodeCharacter charAt(int index) { + return characters.get(index); + } + + public IntStream codePoints() { + return characters.stream().mapToInt(UnicodeCharacter::getCodePoint); + } + + public int length() { + return characters.size(); + } + + public UnicodeCharacterSequence subSequence(int start, int end) { + return new UnicodeCharacterSequence(characters.subList(start, end)); + } + + @Override + public String toString() { + return UnicodeCharacter.toString(characters.toList()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof UnicodeCharacterSequence oth) + return characters.containsAll(oth.characters.toList()) && characters.size() == oth.characters.size(); + return false; + } + + /** + * {@return whether all characters of both sequence match} + * + * @param oth the other UnicodeCharacterSequence + * @param characterMatch the function applied to determine if two UnicodeCharacters match + */ + public boolean match(UnicodeCharacterSequence oth, UnicodeCharacterMatchFunctions characterMatch) { + if (this == oth) + return true; + if (length() != oth.length()) + return false; + return characters.zip(oth.characters).allSatisfy(p -> characterMatch.apply(p.getOne(), p.getTwo())); + } + + @Override + public int hashCode() { + return Objects.hash(characters); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimMeasure.java index 426f534fb..5e461ebc1 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimMeasure.java @@ -1,10 +1,12 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; +import java.io.Serializable; + /** * A measure that can determine whether two words from a {@link ComparisonContext} are similar. */ -public interface WordSimMeasure { +public interface WordSimMeasure extends Serializable { /** * Evaluates whether the words from the given {@link ComparisonContext} are similar. @@ -14,4 +16,11 @@ public interface WordSimMeasure { */ boolean areWordsSimilar(ComparisonContext ctx); + /** + * Evaluates how similar the words from the given {@link ComparisonContext} are. + * + * @param ctx the context containing the words + * @return Similarity in range [0,1] + */ + double getSimilarity(ComparisonContext ctx); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtils.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtils.java index b3b1650b0..38ded2e26 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtils.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtils.java @@ -1,66 +1,117 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; +import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.Objects; +import java.util.stream.Collectors; import org.eclipse.collections.api.factory.Lists; -import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.list.MutableList; import org.sqlite.SQLiteConfig; import org.sqlite.SQLiteOpenMode; import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.common.util.AbbreviationDisambiguationHelper; +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonTextToolsConfig; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.equality.EqualityMeasure; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy.AverageStrategy; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy.ComparisonStrategy; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy.SimilarityStrategy; /** - * A static class that provides various utility methods to calculate similarity between different kinds of objects. This - * class statically keeps a reference to a fallback {@link ComparisonStrategy} and a fallback list of word similarity - * measures. These fallbacks can be changed with the {@link #setMeasures(Collection)} and - * {@link #setStrategy(ComparisonStrategy)} methods. Any calls to methods that provide their own measures or strategies - * will not utilize these fallbacks. Any calls that do not provide their own measures or strategies will utilize them. - * As of right now, no protections against simultaneous write access from multiple threads exist. Therefore, this class - * is not threadsafe. + * A static class that provides various utility methods to calculate similarity between different kinds of objects. This class statically keeps a reference to a + * fallback {@link ComparisonStrategy} and a fallback list of word similarity measures. These fallbacks can be changed with the {@link #setMeasures(Collection)} + * and {@link #setStrategy(ComparisonStrategy)} methods. Any calls to methods that provide their own measures or strategies will not utilize these fallbacks. + * Any calls that do not provide their own measures or strategies will utilize them. As of right now, no protections against simultaneous write access from + * multiple threads exist. Therefore, this class is not threadsafe. */ -public class WordSimUtils { +public class WordSimUtils implements Serializable { - private static ImmutableList measures = WordSimLoader.loadUsingProperties(); - private static ComparisonStrategy strategy = ComparisonStrategy.AT_LEAST_ONE; + private MutableList measures = Lists.mutable.withAll(WordSimLoader.loadUsingProperties()); + private ComparisonStrategy strategy = ComparisonStrategy.AT_LEAST_ONE; + private SimilarityStrategy similarityStrategy = new AverageStrategy(); + private UnicodeCharacterMatchFunctions characterMatch = UnicodeCharacterMatchFunctions.EQUAL; + private boolean considerAbbreviations = CommonTextToolsConfig.CONSIDER_ABBREVIATIONS; - private WordSimUtils() { + /** + * Sets which measures should be used for similarity comparison. The specified collection of measures will be used for all subsequent comparisons. + * + * @param measures the measures to use + */ + public void setMeasures(Collection measures) { + this.measures = Lists.mutable.withAll(measures); } /** - * Sets which measures should be used for similarity comparison. The specified collection of measures will be used - * for all subsequent comparisons. + * Adds the specified measure to the measures, which should be used for similarity comparison. * - * @param measures the measures to use + * @param measure the measure to add + * @return Whether the measure was added successfully */ - public static void setMeasures(Collection measures) { - WordSimUtils.measures = Lists.immutable.withAll(measures); + public boolean addMeasure(WordSimMeasure measure) { + return this.measures.add(measure); } /** - * Sets the default comparison strategy. The specified strategy will be used for all subsequent comparisons that - * themselves do not specify a strategy. + * Sets the default comparison strategy. The specified strategy will be used for all subsequent comparisons that themselves do not specify a strategy. * * @param strategy the new default strategy */ - public static void setStrategy(ComparisonStrategy strategy) { - WordSimUtils.strategy = strategy; + public void setStrategy(ComparisonStrategy strategy) { + this.strategy = strategy; } /** - * Evaluates whether the words from the given {@link ComparisonContext} are similar using the specified comparison - * strategy. + * Sets the default similarity strategy. The specified strategy will be used for all subsequent comparisons that themselves do not specify a strategy. + * + * @param strategy the new default strategy + */ + public void setStrategy(SimilarityStrategy strategy) { + this.similarityStrategy = strategy; + } + + public void setCharacterMatchFunction(UnicodeCharacterMatchFunctions characterMatch) { + this.characterMatch = characterMatch; + } + + public UnicodeCharacterMatchFunctions getCharacterMatchFunction() { + return this.characterMatch; + } + + public void setConsiderAbbreviations(boolean considerAbbreviations) { + this.considerAbbreviations = considerAbbreviations; + } + + public boolean getConsiderAbbreviations() { + return this.considerAbbreviations; + } + + /** + * Evaluates whether the words from the given {@link ComparisonContext} are similar using the specified comparison strategy. * * @param ctx the context * @param strategy the strategy * @return Returns {@code true} if the given strategy considers the words similar enough. */ - public static boolean areWordsSimilar(ComparisonContext ctx, ComparisonStrategy strategy) { + public boolean areWordsSimilar(ComparisonContext ctx, ComparisonStrategy strategy) { Objects.requireNonNull(ctx); Objects.requireNonNull(strategy); + var firstTerm = ctx.firstTerm(); + var secondTerm = ctx.secondTerm(); + + if (getConsiderAbbreviations()) { + var ambiguatedFirstTerm = AbbreviationDisambiguationHelper.ambiguateAll(firstTerm, true); + var ambiguatedSecondTerm = AbbreviationDisambiguationHelper.ambiguateAll(secondTerm, true); + var different = !ambiguatedFirstTerm.equals(firstTerm) || !ambiguatedSecondTerm.equals(secondTerm); + + if (different && areWordsSimilar(new ComparisonContext(ambiguatedFirstTerm, ambiguatedSecondTerm, null, null, false, ctx.characterMatch()))) { + return true; + } + } + // Currently, we need the split test as it improves results by a lot. In the future, we should try to avoid its requirement if (!splitLengthTest(ctx)) { return false; @@ -69,33 +120,33 @@ public static boolean areWordsSimilar(ComparisonContext ctx, ComparisonStrategy return strategy.areWordsSimilar(ctx, measures.toList()); } - private static boolean splitLengthTest(ComparisonContext ctx) { + private boolean splitLengthTest(ComparisonContext ctx) { var first = ctx.firstTerm().toLowerCase(); var second = ctx.secondTerm().toLowerCase(); return (first.split(" ").length == second.split(" ").length); } /** - * Evaluates whether the words from the given {@link ComparisonContext} are similar using the default comparison - * strategy. The default strategy can be changed with the {@link #setStrategy(ComparisonStrategy)} method. + * Evaluates whether the words from the given {@link ComparisonContext} are similar using the default comparison strategy. The default strategy can be + * changed with the {@link #setStrategy(ComparisonStrategy)} method. * * @param ctx the context * @return Returns {@code true} if the default strategy considers the words similar enough. */ - public static boolean areWordsSimilar(ComparisonContext ctx) { + public boolean areWordsSimilar(ComparisonContext ctx) { Objects.requireNonNull(ctx); return areWordsSimilar(ctx, strategy); } /** - * Evaluates whether the given words are similar using the default comparison strategy. The default strategy can be - * changed with the {@link #setStrategy(ComparisonStrategy)} method. + * Evaluates whether the given words are similar using the default comparison strategy. The default strategy can be changed with the + * {@link #setStrategy(ComparisonStrategy)} method. * * @param firstWord the first word * @param secondWord the second word * @return Returns {@code true} if the default strategy considers the words similar enough. */ - public static boolean areWordsSimilar(String firstWord, String secondWord) { + public boolean areWordsSimilar(String firstWord, String secondWord) { return areWordsSimilar(new ComparisonContext(firstWord, secondWord, false), strategy); } @@ -107,19 +158,19 @@ public static boolean areWordsSimilar(String firstWord, String secondWord) { * @param strategy the strategy to use * @return Returns {@code true} if the given strategy considers the words similar enough. */ - public static boolean areWordsSimilar(String firstWord, String secondWord, ComparisonStrategy strategy) { + public boolean areWordsSimilar(String firstWord, String secondWord, ComparisonStrategy strategy) { return areWordsSimilar(new ComparisonContext(firstWord, secondWord, false), strategy); } /** - * Evaluates whether the given words are similar using the default comparison strategy. The default strategy can be - * changed with the {@link #setStrategy(ComparisonStrategy)} method. + * Evaluates whether the given words are similar using the default comparison strategy. The default strategy can be changed with the + * {@link #setStrategy(ComparisonStrategy)} method. * * @param firstWord the first word * @param secondWord the second word * @return Returns {@code true} if the default strategy considers the words similar enough. */ - public static boolean areWordsSimilar(Word firstWord, Word secondWord) { + public boolean areWordsSimilar(Word firstWord, Word secondWord) { return areWordsSimilar(new ComparisonContext(firstWord, secondWord, false), strategy); } @@ -131,20 +182,20 @@ public static boolean areWordsSimilar(Word firstWord, Word secondWord) { * @param strategy the strategy to use * @return Returns {@code true} if the given strategy considers the words similar enough. */ - public static boolean areWordsSimilar(Word firstWord, Word secondWord, ComparisonStrategy strategy) { + public boolean areWordsSimilar(Word firstWord, Word secondWord, ComparisonStrategy strategy) { return areWordsSimilar(new ComparisonContext(firstWord, secondWord, false), strategy); } /** - * Evaluates whether the given words are similar using the default comparison strategy. The default strategy can be - * changed with the {@link #setStrategy(ComparisonStrategy)} method. + * Evaluates whether the given words are similar using the default comparison strategy. The default strategy can be changed with the + * {@link #setStrategy(ComparisonStrategy)} method. * * @param firstWord the first word * @param secondWord the second word * @return Returns {@code true} if the default strategy considers the words similar enough. */ - public static boolean areWordsSimilar(String firstWord, Word secondWord) { - return areWordsSimilar(new ComparisonContext(firstWord, secondWord.getText(), null, secondWord, false), strategy); + public boolean areWordsSimilar(String firstWord, Word secondWord) { + return areWordsSimilar(new ComparisonContext(firstWord, secondWord.getText(), null, secondWord, false, characterMatch), strategy); } /** @@ -155,8 +206,50 @@ public static boolean areWordsSimilar(String firstWord, Word secondWord) { * @param strategy the strategy to use * @return Returns {@code true} if the given strategy considers the words similar enough. */ - public static boolean areWordsSimilar(String firstWord, Word secondWord, ComparisonStrategy strategy) { - return areWordsSimilar(new ComparisonContext(firstWord, secondWord.getText(), null, secondWord, false), strategy); + public boolean areWordsSimilar(String firstWord, Word secondWord, ComparisonStrategy strategy) { + return areWordsSimilar(new ComparisonContext(firstWord, secondWord.getText(), null, secondWord, false, characterMatch), strategy); + } + + /** + * Evaluates the similarity of the given words using the specified similarity strategy. + * + * @param firstWord the first word + * @param secondWord the second word + * @param strategy the strategy to use + * @param ignoreCase whether to ignore the case during comparison + * @return Returns similarity in range [0,1] + */ + public double getSimilarity(String firstWord, String secondWord, SimilarityStrategy strategy, boolean ignoreCase) { + var allMeasuresExceptDefault = this.measures.stream().filter(m -> !(m instanceof EqualityMeasure)).collect(Collectors.toCollection(ArrayList::new)); + if (allMeasuresExceptDefault.isEmpty()) + allMeasuresExceptDefault.add(new EqualityMeasure()); + + return strategy.getSimilarity(new ComparisonContext(ignoreCase ? firstWord.toLowerCase() : firstWord, ignoreCase ? + secondWord.toLowerCase() : + secondWord, null, null, false, characterMatch), allMeasuresExceptDefault); + } + + /** + * Evaluates the similarity of the given words. + * + * @param firstWord the first word + * @param secondWord the second word + * @return Returns similarity in range [0,1] + */ + public double getSimilarity(String firstWord, String secondWord) { + return getSimilarity(firstWord, secondWord, false); + } + + /** + * Evaluates the similarity of the given words. + * + * @param firstWord the first word + * @param secondWord the second word + * @param ignoreCase whether to ignore the case during comparison + * @return Returns similarity in range [0,1] + */ + public double getSimilarity(String firstWord, String secondWord, boolean ignoreCase) { + return getSimilarity(firstWord, secondWord, similarityStrategy, ignoreCase); } public static SQLiteConfig getSqLiteConfig() { diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/equality/EqualityMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/equality/EqualityMeasure.java index df7179d90..33b4003e3 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/equality/EqualityMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/equality/EqualityMeasure.java @@ -1,18 +1,28 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.equality; +import java.util.Locale; + import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterSequence; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; /** - * This word similarity measure just checks whether the most appropriate string representations of the passed objects - * are equal. + * This word similarity measure just checks whether the most appropriate string representations of the passed objects are equal. + * Equality of two characters is determined using the provided {@link ComparisonContext#characterMatch() Character Match Function}. + * Letter-casing is not considered. */ public class EqualityMeasure implements WordSimMeasure { @Override public boolean areWordsSimilar(ComparisonContext ctx) { - return ctx.firstTerm().equalsIgnoreCase(ctx.secondTerm()); + var firstTerm = UnicodeCharacterSequence.valueOf(ctx.firstTerm().toLowerCase(Locale.ENGLISH)); + var secondTerm = UnicodeCharacterSequence.valueOf(ctx.secondTerm().toLowerCase(Locale.ENGLISH)); + return firstTerm.match(secondTerm, ctx.characterMatch()); } + @Override + public double getSimilarity(ComparisonContext ctx) { + return areWordsSimilar(ctx) ? 1 : 0; + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/glove/GloveMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/glove/GloveMeasure.java index 7713942a7..9346f5d0c 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/glove/GloveMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/glove/GloveMeasure.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.glove; import java.nio.file.Path; @@ -12,11 +12,11 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.vector.RetrieveVectorException; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.vector.VectorBasedWordSimMeasure; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.vector.VectorSqliteDatabase; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.vector.WordVectorDataSource; /** - * This word similarity measures utilizes GloVe trained word vector representations to calculate word similarity. It - * retrieves vectors for each word and compares them using cosine similarity. This measure additionally manages a cache - * to improve lookup speeds. + * This word similarity measures utilizes GloVe trained word vector representations to calculate word similarity. It retrieves vectors for each word and + * compares them using cosine similarity. This measure additionally manages a cache to improve lookup speeds. */ public class GloveMeasure extends VectorBasedWordSimMeasure { @@ -26,23 +26,18 @@ public class GloveMeasure extends VectorBasedWordSimMeasure { /** * Constructs a new {@link GloveMeasure} using the settings provided by {@link CommonTextToolsConfig}. - * - * @throws SQLException if establishing the connection to the data source fails */ - public GloveMeasure() throws SQLException { - this(new VectorSqliteDatabase(Path.of(CommonTextToolsConfig.GLOVE_DB_FILE_PATH)), CommonTextToolsConfig.GLOVE_SIMILARITY_THRESHOLD); + public GloveMeasure() { + this(CommonTextToolsConfig.GLOVE_SIMILARITY_THRESHOLD); } /** * Constructs a new {@link GloveMeasure} instance. * - * @param dataSource the data source from which word vectors are loaded * @param similarityThreshold the threshold above which words are considered similar, between 0 and 1 * @throws IllegalArgumentException if the given threshold is not between 0 and 1 */ - public GloveMeasure(VectorSqliteDatabase dataSource, double similarityThreshold) throws IllegalArgumentException { - super(dataSource); - + public GloveMeasure(double similarityThreshold) throws IllegalArgumentException { this.similarityThreshold = similarityThreshold; if (similarityThreshold < 0.0 || similarityThreshold > 1.0) { @@ -52,15 +47,25 @@ public GloveMeasure(VectorSqliteDatabase dataSource, double similarityThreshold) @Override public boolean areWordsSimilar(ComparisonContext ctx) { - double similarity = Double.NaN; + return getSimilarity(ctx) >= this.similarityThreshold; + } + @Override + public double getSimilarity(ComparisonContext ctx) { try { - similarity = this.compareVectors(ctx.firstTerm(), ctx.secondTerm()); + return this.compareVectors(ctx.firstTerm(), ctx.secondTerm()); } catch (RetrieveVectorException e) { LOGGER.error("Failed to compare glove vectors: " + ctx, e); + return Double.NaN; } - - return similarity >= this.similarityThreshold; } + @Override + protected WordVectorDataSource getVectorDataSource() { + try { + return new VectorSqliteDatabase(Path.of(CommonTextToolsConfig.GLOVE_DB_FILE_PATH)); + } catch (SQLException e) { + throw new IllegalArgumentException(e); + } + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasure.java index d0125d14b..47d1d59cd 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasure.java @@ -1,8 +1,6 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.jarowinkler; -import org.apache.commons.text.similarity.JaroWinklerSimilarity; - import edu.kit.kastel.mcse.ardoco.core.common.util.CommonTextToolsConfig; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; @@ -12,8 +10,6 @@ */ public class JaroWinklerMeasure implements WordSimMeasure { - private final JaroWinklerSimilarity jaroWinklerSimilarity = new JaroWinklerSimilarity(); - private final double similarityThreshold; /** @@ -25,7 +21,7 @@ public JaroWinklerMeasure() { /** * Constructs a new {@link JaroWinklerMeasure}. - * + * * @param similarityThreshold the threshold above which words are considered similar, between 0 and 1 * @throws IllegalArgumentException if the given threshold is not between 0 and 1 */ @@ -39,8 +35,13 @@ public JaroWinklerMeasure(double similarityThreshold) throws IllegalArgumentExce @Override public boolean areWordsSimilar(ComparisonContext ctx) { - double similarity = this.jaroWinklerSimilarity.apply(ctx.firstTerm(), ctx.secondTerm()); + double similarity = getSimilarity(ctx); return similarity >= this.similarityThreshold; } + @Override + public double getSimilarity(ComparisonContext ctx) { + return UnicodeJaroWinklerSimilarity.apply(ctx.firstTerm(), ctx.secondTerm(), ctx.characterMatch()); + } + } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/UnicodeJaroWinklerSimilarity.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/UnicodeJaroWinklerSimilarity.java new file mode 100644 index 000000000..7ddcb493f --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/UnicodeJaroWinklerSimilarity.java @@ -0,0 +1,166 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.jarowinkler; + +import java.io.Serializable; +import java.util.Arrays; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacter; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterSequence; + +/** + * A similarity algorithm indicating the percentage of matched characters between two character sequences. + * + *

+ * The Jaro measure is the weighted sum of percentage of matched characters from each file and transposed characters. Winkler increased this measure for + * matching initial characters. + *

+ * + *

+ * This implementation is based on the Jaro Winkler similarity algorithm from + * http://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance. + *

+ * + *

+ * This code is a modified version of {@link org.apache.commons.text.similarity.JaroWinklerSimilarity} provided by Apache Commons Text. It was adapted to use + * {@link UnicodeCharacter UnicodeCharacters} instead of Java's native {@link Character Characters} and allows for specifying a function that determines a + * character match. + *

+ */ +public final class UnicodeJaroWinklerSimilarity implements Serializable { + /** + * This method returns the Jaro-Winkler string matches, half transpositions, prefix array. + * + * @param first the first string to be matched + * @param second the second string to be matched + * @param characterMatch the function used to determine a match between two {@link UnicodeCharacter UnicodeCharacters} + * @return mtp array containing: matches, half transpositions, and prefix + */ + private static int[] matches(UnicodeCharacterSequence first, UnicodeCharacterSequence second, UnicodeCharacterMatchFunctions characterMatch) { + final UnicodeCharacterSequence max; + final UnicodeCharacterSequence min; + if (first.length() > second.length()) { + max = first; + min = second; + } else { + max = second; + min = first; + } + final int range = Math.max(max.length() / 2 - 1, 0); + final int[] matchIndexes = new int[min.length()]; + Arrays.fill(matchIndexes, -1); + final boolean[] matchFlags = new boolean[max.length()]; + int matches = 0; + for (int mi = 0; mi < min.length(); mi++) { + UnicodeCharacter c1 = min.charAt(mi); + for (int xi = Math.max(mi - range, 0), xn = Math.min(mi + range + 1, max.length()); xi < xn; xi++) { + if (!matchFlags[xi] && characterMatch.apply(c1, max.charAt(xi))) { + matchIndexes[mi] = xi; + matchFlags[xi] = true; + matches++; + break; + } + } + } + final UnicodeCharacter[] ms1 = new UnicodeCharacter[matches]; + final UnicodeCharacter[] ms2 = new UnicodeCharacter[matches]; + for (int i = 0, si = 0; i < min.length(); i++) { + if (matchIndexes[i] != -1) { + ms1[si] = min.charAt(i); + si++; + } + } + for (int i = 0, si = 0; i < max.length(); i++) { + if (matchFlags[i]) { + ms2[si] = max.charAt(i); + si++; + } + } + int halfTranspositions = 0; + for (int mi = 0; mi < ms1.length; mi++) { + if (!characterMatch.apply(ms1[mi], ms2[mi])) { + halfTranspositions++; + } + } + int prefix = 0; + for (int mi = 0; mi < Math.min(4, min.length()); mi++) { + if (!characterMatch.apply(first.charAt(mi), second.charAt(mi))) { + break; + } + prefix++; + } + return new int[] { matches, halfTranspositions, prefix }; + } + + /** + * Computes the Jaro Winkler Similarity between two character sequences. + * + *
+     * sim.apply(null, null) = IllegalArgumentException
+     * sim.apply("foo", null) = IllegalArgumentException
+     * sim.apply(null, "foo") = IllegalArgumentException
+     * sim.apply("", "") = 1.0
+     * sim.apply("foo", "foo") = 1.0
+     * sim.apply("foo", "foo ") = 0.94
+     * sim.apply("foo", "foo ") = 0.91
+     * sim.apply("foo", " foo ") = 0.87
+     * sim.apply("foo", " foo") = 0.51
+     * sim.apply("", "a") = 0.0
+     * sim.apply("aaapppp", "") = 0.0
+     * sim.apply("frog", "fog") = 0.93
+     * sim.apply("fly", "ant") = 0.0
+     * sim.apply("elephant", "hippo") = 0.44
+     * sim.apply("hippo", "elephant") = 0.44
+     * sim.apply("hippo", "zzzzzzzz") = 0.0
+     * sim.apply("hello", "hallo") = 0.88
+     * sim.apply("ABC Corporation", "ABC Corp") = 0.91
+     * sim.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.") = 0.95
+     * sim.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness") = 0.94
+     * sim.apply("PENNSYLVANIA", "PENNCISYLVNIA") = 0.88
+     * 
+ * + * @param left the first UnicodeCharacterSequence, must not be null + * @param right the second UnicodeCharacterSequence, must not be null + * @param characterMatch the function used to determine a match between two {@link UnicodeCharacter UnicodeCharacters} + * @return result similarity + * @throws IllegalArgumentException if either CharSequence input is {@code null} + */ + public static Double apply(UnicodeCharacterSequence left, UnicodeCharacterSequence right, UnicodeCharacterMatchFunctions characterMatch) { + final double defaultScalingFactor = 0.1; + + if (left == null || right == null) { + throw new IllegalArgumentException("UnicodeCharSequences must not be null"); + } + + if (left.match(right, characterMatch)) + return 1d; + + final int[] mtp = matches(left, right, characterMatch); + final double m = mtp[0]; + if (m == 0) { + return 0d; + } + final double j = (m / left.length() + m / right.length() + (m - (double) mtp[1] / 2) / m) / 3; + return j < 0.7d ? j : j + defaultScalingFactor * mtp[2] * (1d - j); + } + + /** + * Computes the Jaro Winkler Similarity between two strings. + * + * @param left the first String, must not be null + * @param right the second String, must not be null + * @param characterMatch the function used to determine a match between two {@link UnicodeCharacter UnicodeCharacters} + * @return result similarity + * @throws IllegalArgumentException if either CharSequence input is {@code null} + */ + public static Double apply(String left, String right, UnicodeCharacterMatchFunctions characterMatch) { + if (left == null || right == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + if (left.equals(right)) + return 1d; + + return apply(UnicodeCharacterSequence.valueOf(left), UnicodeCharacterSequence.valueOf(right), characterMatch); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasure.java index 962def19d..dc6195d00 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasure.java @@ -1,15 +1,14 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.levenshtein; -import org.apache.commons.text.similarity.LevenshteinDistance; - import edu.kit.kastel.mcse.ardoco.core.common.util.CommonTextToolsConfig; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterSequence; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; /** - * This word similarity measure uses the levenshtein distance (also sometimes called edit distance) algorithm to - * calculate word similarity. This measure is configurable through three configuration options: + * This word similarity measure uses the levenshtein distance (also sometimes called edit distance) algorithm to calculate word similarity. This measure is + * configurable through three configuration options: * *
    *
  • maxDistance: Word pairs with a levenshtein distance above this configuration value will not be considered @@ -23,7 +22,7 @@ */ public class LevenshteinMeasure implements WordSimMeasure { - private final LevenshteinDistance levenshteinDistance = new LevenshteinDistance(); + private final UnicodeLevenshteinDistance levenshteinDistance = new UnicodeLevenshteinDistance(); private final int minLength; private final int maxDistance; private final double threshold; @@ -36,8 +35,7 @@ public LevenshteinMeasure() { } /** - * Constructs a new {@link LevenshteinMeasure}. The necessary arguments for this constructor are explained - * {@link LevenshteinMeasure here}. + * Constructs a new {@link LevenshteinMeasure}. The necessary arguments for this constructor are explained {@link LevenshteinMeasure here}. * * @param minLength the min length * @param maxDistance the max distance @@ -63,11 +61,12 @@ public LevenshteinMeasure(int minLength, int maxDistance, double threshold) { @Override public boolean areWordsSimilar(ComparisonContext ctx) { + //FIXME cast to lower case seems unwarranted given that this is delegated to WordSimUtils already String firstWord = ctx.firstTerm().toLowerCase(); String secondWord = ctx.secondTerm().toLowerCase(); int maxDynamicDistance = (int) Math.min(this.maxDistance, this.threshold * Math.min(firstWord.length(), secondWord.length())); - int distance = this.levenshteinDistance.apply(firstWord, secondWord); + int distance = this.levenshteinDistance.apply(firstWord, secondWord, ctx.characterMatch()); if (firstWord.length() <= this.minLength) { return distance <= this.maxDistance && (secondWord.contains(firstWord) || firstWord.contains(secondWord)); @@ -76,4 +75,13 @@ public boolean areWordsSimilar(ComparisonContext ctx) { } } + @Override + public double getSimilarity(ComparisonContext ctx) { + //FIXME cast to lower case seems unwarranted given that this is delegated to WordSimUtils already + var firstWord = UnicodeCharacterSequence.valueOf(ctx.firstTerm().toLowerCase()); + var secondWord = UnicodeCharacterSequence.valueOf(ctx.secondTerm().toLowerCase()); + return 1.0 - this.levenshteinDistance.apply(ctx.firstTerm(), ctx.secondTerm(), ctx.characterMatch()) / (double) Math.max(Math.max(firstWord.length(), + secondWord.length()), 1); + } + } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/UnicodeLevenshteinDistance.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/UnicodeLevenshteinDistance.java new file mode 100644 index 000000000..173fc5d73 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/UnicodeLevenshteinDistance.java @@ -0,0 +1,396 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.levenshtein; + +import java.io.Serializable; +import java.util.Arrays; + +import org.apache.commons.text.similarity.LevenshteinDistance; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacter; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterSequence; + +/** + * An algorithm for measuring the difference between two character sequences. + * + *

    + * This is the number of changes needed to change one sequence into another, where each change is a single character modification (deletion, insertion or + * substitution). + *

    + * + *

    + * This code is a modified version of {@link org.apache.commons.text.similarity.LevenshteinDistance} provided by Apache Commons Text. It was adapted to use + * {@link UnicodeCharacter UnicodeCharacters} instead of Java's native {@link Character Characters} and allows for specifying a function that determines a + * character match. + *

    + * + * @since 1.0 + */ +public class UnicodeLevenshteinDistance implements Serializable { + + /** + * Default instance. + */ + private static final LevenshteinDistance DEFAULT_INSTANCE = new LevenshteinDistance(); + + /** + * Gets the default instance. + * + * @return The default instance + */ + public static LevenshteinDistance getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + /** + * Find the Levenshtein distance between two CharSequences if it's less than or equal to a given threshold. + * + *

    + * This implementation follows from Algorithms on Strings, Trees and Sequences by Dan Gusfield and Chas Emerick's implementation of the Levenshtein distance + * algorithm from http://www.merriampark.com/ld.htm + *

    + * + *
    +     * limitedCompare(null, *, *) = IllegalArgumentException
    +     * limitedCompare(*, null, *) = IllegalArgumentException
    +     * limitedCompare(*, *, -1) = IllegalArgumentException
    +     * limitedCompare("","", 0) = 0
    +     * limitedCompare("aaapppp", "", 8) = 7
    +     * limitedCompare("aaapppp", "", 7) = 7
    +     * limitedCompare("aaapppp", "", 6)) = -1
    +     * limitedCompare("elephant", "hippo", 7) = 7
    +     * limitedCompare("elephant", "hippo", 6) = -1
    +     * limitedCompare("hippo", "elephant", 7) = 7
    +     * limitedCompare("hippo", "elephant", 6) = -1
    +     * 
    + * + * @param left the first UnicodeCharacterSequence, must not be null + * @param right the second UnicodeCharacterSequence, must not be null + * @param threshold the target threshold, must not be negative + * @param characterMatch the function used to determine a match between two {@link UnicodeCharacter UnicodeCharacters} + * @return result distance, or -1 + */ + private static int limitedCompare(UnicodeCharacterSequence left, UnicodeCharacterSequence right, final int threshold, + UnicodeCharacterMatchFunctions characterMatch) { // NOPMD + if (left == null || right == null) { + throw new IllegalArgumentException("CharSequences must not be null"); + } + if (threshold < 0) { + throw new IllegalArgumentException("Threshold must not be negative"); + } + + /* + * This implementation only computes the distance if it's less than or + * equal to the threshold value, returning -1 if it's greater. The + * advantage is performance: unbounded distance is O(nm), but a bound of + * k allows us to reduce it to O(km) time by only computing a diagonal + * stripe of width 2k + 1 of the cost table. It is also possible to use + * this to compute the unbounded Levenshtein distance by starting the + * threshold at 1 and doubling each time until the distance is found; + * this is O(dm), where d is the distance. + * + * One subtlety comes from needing to ignore entries on the border of + * our stripe eg. p[] = |#|#|#|* d[] = *|#|#|#| We must ignore the entry + * to the left of the leftmost member We must ignore the entry above the + * rightmost member + * + * Another subtlety comes from our stripe running off the matrix if the + * strings aren't of the same size. Since string s is always swapped to + * be the shorter of the two, the stripe will always run off to the + * upper right instead of the lower left of the matrix. + * + * As a concrete example, suppose s is of length 5, t is of length 7, + * and our threshold is 1. In this case we're going to walk a stripe of + * length 3. The matrix would look like so: + * + *
    +         *    1 2 3 4 5
    +         * 1 |#|#| | | |
    +         * 2 |#|#|#| | |
    +         * 3 | |#|#|#| |
    +         * 4 | | |#|#|#|
    +         * 5 | | | |#|#|
    +         * 6 | | | | |#|
    +         * 7 | | | | | |
    +         * 
    + * + * Note how the stripe leads off the table as there is no possible way + * to turn a string of length 5 into one of length 7 in edit distance of + * 1. + * + * Additionally, this implementation decreases memory usage by using two + * single-dimensional arrays and swapping them back and forth instead of + * allocating an entire n by m matrix. This requires a few minor + * changes, such as immediately returning when it's detected that the + * stripe has run off the matrix and initially filling the arrays with + * large values so that entries we don't compute are ignored. + * + * See Algorithms on Strings, Trees and Sequences by Dan Gusfield for + * some discussion. + */ + + int n = left.length(); // length of left + int m = right.length(); // length of right + + // if one string is empty, the edit distance is necessarily the length + // of the other + if (n == 0) { + return m <= threshold ? m : -1; + } + if (m == 0) { + return n <= threshold ? n : -1; + } + + if (n > m) { + // swap the two strings to consume less memory + final UnicodeCharacterSequence tmp = left; + left = right; + right = tmp; + n = m; + m = right.length(); + } + + // the edit distance cannot be less than the length difference + if (m - n > threshold) { + return -1; + } + + int[] p = new int[n + 1]; // 'previous' cost array, horizontally + int[] d = new int[n + 1]; // cost array, horizontally + int[] tempD; // placeholder to assist in swapping p and d + + // fill in starting table values + final int boundary = Math.min(n, threshold) + 1; + for (int i = 0; i < boundary; i++) { + p[i] = i; + } + // these fills ensure that the value above the rightmost entry of our + // stripe will be ignored in following loop iterations + Arrays.fill(p, boundary, p.length, Integer.MAX_VALUE); + Arrays.fill(d, Integer.MAX_VALUE); + + // iterates through t + for (int j = 1; j <= m; j++) { + final UnicodeCharacter rightJ = right.charAt(j - 1); // jth character of right + d[0] = j; + + // compute stripe indices, constrain to array size + final int min = Math.max(1, j - threshold); + final int max = j > Integer.MAX_VALUE - threshold ? n : Math.min(n, j + threshold); + + // ignore entry left of leftmost + if (min > 1) { + d[min - 1] = Integer.MAX_VALUE; + } + + int lowerBound = Integer.MAX_VALUE; + // iterates through [min, max] in s + for (int i = min; i <= max; i++) { + if (characterMatch.apply(left.charAt(i - 1), rightJ)) { + // diagonally left and up + d[i] = p[i - 1]; + } else { + // 1 + minimum of cell to the left, to the top, diagonally + // left and up + d[i] = 1 + Math.min(Math.min(d[i - 1], p[i]), p[i - 1]); + } + lowerBound = Math.min(lowerBound, d[i]); + } + // if the lower bound is greater than the threshold, then exit early + if (lowerBound > threshold) { + return -1; + } + + // copy current distance counts to 'previous row' distance counts + tempD = p; + p = d; + d = tempD; + } + + // if p[n] is greater than the threshold, there's no guarantee on it + // being the correct + // distance + if (p[n] <= threshold) { + return p[n]; + } + return -1; + } + + /** + * Finds the Levenshtein distance between two Strings. + * + *

    A higher score indicates a greater distance.

    + * + *

    The previous implementation of the Levenshtein distance algorithm + * was from + * https://web.archive.org/web/20120526085419/http://www.merriampark.com/ldjava.htm

    + * + *

    This implementation only need one single-dimensional arrays of length s.length() + 1

    + * + *
    +     * unlimitedCompare(null, *) = IllegalArgumentException
    +     * unlimitedCompare(*, null) = IllegalArgumentException
    +     * unlimitedCompare("","") = 0
    +     * unlimitedCompare("","a") = 1
    +     * unlimitedCompare("aaapppp", "") = 7
    +     * unlimitedCompare("frog", "fog") = 1
    +     * unlimitedCompare("fly", "ant") = 3
    +     * unlimitedCompare("elephant", "hippo") = 7
    +     * unlimitedCompare("hippo", "elephant") = 7
    +     * unlimitedCompare("hippo", "zzzzzzzz") = 8
    +     * unlimitedCompare("hello", "hallo") = 1
    +     * 
    + * + * @param left the first UnicodeCharacterSequence, must not be null + * @param right the second UnicodeCharacterSequence, must not be null + * @param characterMatch the function used to determine a match between two {@link UnicodeCharacter UnicodeCharacters} + * @return result distance, or -1 + * @throws IllegalArgumentException if either UnicodeCharacterSequence input is {@code null} + */ + private static int unlimitedCompare(UnicodeCharacterSequence left, UnicodeCharacterSequence right, UnicodeCharacterMatchFunctions characterMatch) { + if (left == null || right == null) { + throw new IllegalArgumentException("CharSequences must not be null"); + } + + /* + This implementation use two variable to record the previous cost counts, + So this implementation use less memory than previous impl. + */ + + int n = left.length(); // length of left + int m = right.length(); // length of right + + if (n == 0) { + return m; + } + if (m == 0) { + return n; + } + + if (n > m) { + // swap the input strings to consume less memory + final UnicodeCharacterSequence tmp = left; + left = right; + right = tmp; + n = m; + m = right.length(); + } + + final int[] p = new int[n + 1]; + + // indexes into strings left and right + int i; // iterates through left + int j; // iterates through right + int upperLeft; + int upper; + + UnicodeCharacter rightJ; // jth character of right + int cost; // cost + + for (i = 0; i <= n; i++) { + p[i] = i; + } + + for (j = 1; j <= m; j++) { + upperLeft = p[0]; + rightJ = right.charAt(j - 1); + p[0] = j; + + for (i = 1; i <= n; i++) { + upper = p[i]; + cost = characterMatch.apply(left.charAt(i - 1), rightJ) ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally left and up +cost + p[i] = Math.min(Math.min(p[i - 1] + 1, p[i] + 1), upperLeft + cost); + upperLeft = upper; + } + } + + return p[n]; + } + + /** + * Threshold. + */ + private final Integer threshold; + + /** + * This returns the default instance that uses a version of the algorithm that does not use a threshold parameter. + * + * @see LevenshteinDistance#getDefaultInstance() + */ + public UnicodeLevenshteinDistance() { + this(null); + } + + /** + * If the threshold is not null, distance calculations will be limited to a maximum length. If the threshold is null, the unlimited version of the algorithm + * will be used. + * + * @param threshold If this is null then distances calculations will not be limited. This may not be negative. + */ + public UnicodeLevenshteinDistance(final Integer threshold) { + if (threshold != null && threshold < 0) { + throw new IllegalArgumentException("Threshold must not be negative"); + } + this.threshold = threshold; + } + + /** + * Finds the Levenshtein distance between two Strings. + * + *

    A higher score indicates a greater distance.

    + * + *

    The previous implementation of the Levenshtein distance algorithm + * was from http://www.merriampark.com/ld.htm

    + * + *

    Chas Emerick has written an implementation in Java, which avoids an OutOfMemoryError + * which can occur when my Java implementation is used with very large strings.
    This implementation of the Levenshtein distance algorithm is from http://www.merriampark.com/ldjava.htm

    + * + *
    +     * distance.apply(null, *) = IllegalArgumentException
    +     * distance.apply(*, null) = IllegalArgumentException
    +     * distance.apply("","") = 0
    +     * distance.apply("","a") = 1
    +     * distance.apply("aaapppp", "") = 7
    +     * distance.apply("frog", "fog") = 1
    +     * distance.apply("fly", "ant") = 3
    +     * distance.apply("elephant", "hippo") = 7
    +     * distance.apply("hippo", "elephant") = 7
    +     * distance.apply("hippo", "zzzzzzzz") = 8
    +     * distance.apply("hello", "hallo") = 1
    +     * 
    + * + * @param left the first string, must not be null + * @param right the second string, must not be null + * @return result distance, or -1 + * @throws IllegalArgumentException if either String input {@code null} + */ + public Integer apply(final UnicodeCharacterSequence left, final UnicodeCharacterSequence right, UnicodeCharacterMatchFunctions characterMatch) { + if (threshold != null) { + return limitedCompare(left, right, threshold, characterMatch); + } + return unlimitedCompare(left, right, characterMatch); + } + + public Integer apply(String left, String right, UnicodeCharacterMatchFunctions characterMatch) { + if (left == null || right == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + if (left.equals(right)) + return 0; + + return apply(UnicodeCharacterSequence.valueOf(left), UnicodeCharacterSequence.valueOf(right), characterMatch); + } + + /** + * Gets the distance threshold. + * + * @return The distance threshold + */ + public Integer getThreshold() { + return threshold; + } + +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/ngram/NgramMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/ngram/NgramMeasure.java index bf551ae31..a0d365398 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/ngram/NgramMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/ngram/NgramMeasure.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.ngram; import java.util.Objects; @@ -19,9 +19,8 @@ public class NgramMeasure implements WordSimMeasure { */ public enum Variant { /** - * This variant matches the algorithm included in apache/lucene which is also positional but deviates from the - * original algorithm by using {@link #LUCENE_PREFIX_CHARACTER} as the prefix character and changing the weight - * for the dN function. + * This variant matches the algorithm included in apache/lucene which is also positional but deviates from the original algorithm by using + * {@link #LUCENE_PREFIX_CHARACTER} as the prefix character and changing the weight for the dN function. */ LUCENE, /** @@ -35,8 +34,7 @@ public enum Variant { private final double similarityThreshold; /** - * Constructs a new {@link NgramMeasure} using the settings provided by - * {@link edu.kit.kastel.mcse.ardoco.core.common.util.CommonTextToolsConfig}. + * Constructs a new {@link NgramMeasure} using the settings provided by {@link edu.kit.kastel.mcse.ardoco.core.common.util.CommonTextToolsConfig}. */ public NgramMeasure() { this(Variant.LUCENE, CommonTextToolsConfig.NGRAM_MEASURE_NGRAM_LENGTH, CommonTextToolsConfig.NGRAM_SIMILARITY_THRESHOLD); @@ -67,14 +65,16 @@ public NgramMeasure(Variant variant, int n, double similarityThreshold) throws I @Override public boolean areWordsSimilar(ComparisonContext ctx) { Objects.requireNonNull(ctx); + return getSimilarity(ctx) >= this.similarityThreshold; + } + @Override + public double getSimilarity(ComparisonContext ctx) { double distance = calculateDistance(ctx.firstTerm(), ctx.secondTerm()); double normalizedDistance = distance / Math.max(ctx.firstTerm().length(), ctx.secondTerm().length()); - double similarity = 1.0 - normalizedDistance; - - return similarity >= this.similarityThreshold; + return 1.0 - normalizedDistance; } /** diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/sewordsim/SEWordSimMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/sewordsim/SEWordSimMeasure.java index 10f5e9552..8f9e5e813 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/sewordsim/SEWordSimMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/sewordsim/SEWordSimMeasure.java @@ -1,9 +1,8 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.sewordsim; import java.nio.file.Path; import java.sql.SQLException; -import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,26 +18,22 @@ public class SEWordSimMeasure implements WordSimMeasure { private static final Logger LOGGER = LoggerFactory.getLogger(SEWordSimMeasure.class); - private final SEWordSimDataSource dataSource; + private transient SEWordSimDataSource dataSource; private final double similarityThreshold; /** * Constructs a new {@link SEWordSimMeasure} using the settings provided by {@link CommonTextToolsConfig}. - * - * @throws SQLException if establishing the connection to the data source fails */ - public SEWordSimMeasure() throws SQLException { - this(new SEWordSimDataSource(Path.of(CommonTextToolsConfig.SEWORDSIM_DB_FILE_PATH)), CommonTextToolsConfig.SEWORDSIM_SIMILARITY_THRESHOLD); + public SEWordSimMeasure() { + this(CommonTextToolsConfig.SEWORDSIM_SIMILARITY_THRESHOLD); } /** * Constructs a new {@link SEWordSimMeasure} instance. * - * @param dataSource the data source from which word similarities are loaded * @param similarityThreshold the threshold above which words are considered similar, between 0 and 1 */ - public SEWordSimMeasure(SEWordSimDataSource dataSource, double similarityThreshold) { - this.dataSource = Objects.requireNonNull(dataSource); + public SEWordSimMeasure(double similarityThreshold) { this.similarityThreshold = similarityThreshold; if (similarityThreshold < 0.0 || similarityThreshold > 1.0) { @@ -48,20 +43,31 @@ public SEWordSimMeasure(SEWordSimDataSource dataSource, double similarityThresho @Override public boolean areWordsSimilar(ComparisonContext ctx) { + var similarity = getSimilarity(ctx); + return !Double.isNaN(similarity) && similarity >= this.similarityThreshold; + } + + @Override + public double getSimilarity(ComparisonContext ctx) { double similarity = Double.NaN; try { - similarity = this.dataSource.getSimilarity(ctx.firstTerm(), ctx.secondTerm()).orElse(Double.NaN); + similarity = getDataSource().getSimilarity(ctx.firstTerm(), ctx.secondTerm()).orElse(Double.NaN); } catch (SQLException e) { LOGGER.error("Failed to query the SEWordSim database for word comparison: " + ctx, e); - return false; + return similarity; } + return similarity; // words are probably missing from the database + } - if (Double.isNaN(similarity)) { - return false; // words are probably missing from the database + private SEWordSimDataSource getDataSource() { + if (dataSource == null) { + try { + dataSource = new SEWordSimDataSource(Path.of(CommonTextToolsConfig.SEWORDSIM_DB_FILE_PATH)); + } catch (SQLException e) { + throw new IllegalArgumentException(e); + } } - - return similarity >= this.similarityThreshold; + return dataSource; } - } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/AverageStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/AverageStrategy.java new file mode 100644 index 000000000..7f0c5fb3d --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/AverageStrategy.java @@ -0,0 +1,25 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy; + +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; + +public class AverageStrategy implements SimilarityStrategy { + @Override + public double getSimilarity(ComparisonContext ctx, List measures) { + double sum = 0.0; + int successful = 0; + + for (WordSimMeasure measure : measures) { + var similarity = measure.getSimilarity(ctx); + if (!Double.isNaN(similarity)) { + successful++; + sum += similarity; + } + } + + return successful == 0 ? 0.0 : sum / successful; + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/ComparisonStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/ComparisonStrategy.java index e6ec4ae26..b32c55f20 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/ComparisonStrategy.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/ComparisonStrategy.java @@ -1,6 +1,7 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy; +import java.io.Serializable; import java.util.List; import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; @@ -9,7 +10,7 @@ /** * A comparison strategy determines how the verdicts of multiple WSMs regarding a specific comparison are combined. */ -public interface ComparisonStrategy { +public interface ComparisonStrategy extends Serializable { ComparisonStrategy AT_LEAST_ONE = new AtLeastOneStrategy(); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/MaximumStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/MaximumStrategy.java new file mode 100644 index 000000000..ee7efe0bf --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/MaximumStrategy.java @@ -0,0 +1,23 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy; + +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; + +public class MaximumStrategy implements SimilarityStrategy { + @Override + public double getSimilarity(ComparisonContext ctx, List measures) { + double max = 0.0; + + for (WordSimMeasure measure : measures) { + var similarity = measure.getSimilarity(ctx); + if (!Double.isNaN(similarity)) { + max = Math.max(similarity, max); + } + } + + return max; + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/MedianStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/MedianStrategy.java new file mode 100644 index 000000000..15859a20c --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/MedianStrategy.java @@ -0,0 +1,30 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy; + +import java.util.ArrayList; +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; + +public class MedianStrategy implements SimilarityStrategy { + @Override + public double getSimilarity(ComparisonContext ctx, List measures) { + var values = new ArrayList(); + + for (WordSimMeasure measure : measures) { + var similarity = measure.getSimilarity(ctx); + if (!Double.isNaN(similarity)) { + values.add(similarity); + } + } + values.sort(Double::compare); + + var array = values.toArray(new Double[0]); + if (array.length % 2 == 0) { + return (array[array.length / 2] + array[array.length / 2 - 1]) / 2; + } else { + return array[array.length / 2]; + } + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/SimilarityStrategy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/SimilarityStrategy.java new file mode 100644 index 000000000..4449faa54 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/strategy/SimilarityStrategy.java @@ -0,0 +1,19 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.strategy; + +import java.io.Serializable; +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimMeasure; + +public interface SimilarityStrategy extends Serializable { + /** + * Evaluates how similar the words from the given {@link ComparisonContext} are by combining the verdicts of the specified word similarity measures. + * + * @param ctx the context containing the words + * @param measures the measures to use + * @return Returns similarity in range [0,1] + */ + double getSimilarity(ComparisonContext ctx, List measures); +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/VectorBasedWordSimMeasure.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/VectorBasedWordSimMeasure.java index 9238b39b4..23e407698 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/VectorBasedWordSimMeasure.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/VectorBasedWordSimMeasure.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.vector; import java.util.LinkedHashMap; @@ -17,18 +17,9 @@ public abstract class VectorBasedWordSimMeasure implements WordSimMeasure { private static final float[] ZERO_VECTOR = new float[0]; - - private final WordVectorDataSource vectorDataSource; private final Map vectorCache = new LinkedHashMap<>(); - /** - * Constructs a new {@link VectorBasedWordSimMeasure} instance - * - * @param vectorDataSource the vector database used to get vector representations for words - */ - protected VectorBasedWordSimMeasure(WordVectorDataSource vectorDataSource) { - this.vectorDataSource = Objects.requireNonNull(vectorDataSource); - } + protected abstract WordVectorDataSource getVectorDataSource(); /** * Compares the two given words by computing the cosine similarity between their respective vector representations. @@ -67,7 +58,7 @@ private float[] getVectorFromCacheOrDatabase(String word) throws RetrieveVectorE float[] vector = this.vectorCache.getOrDefault(word, null); if (vector == null) { - vector = this.vectorDataSource.getWordVector(word).orElse(ZERO_VECTOR); + vector = getVectorDataSource().getWordVector(word).orElse(ZERO_VECTOR); this.vectorCache.put(word, vector); } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/WordVectorSqliteImporter.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/WordVectorSqliteImporter.java index a1e24dfd2..85e7b9024 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/WordVectorSqliteImporter.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/vector/WordVectorSqliteImporter.java @@ -1,9 +1,10 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.vector; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.Serializable; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -34,7 +35,7 @@ * {@link #filterWord(String)} methods. Both methods are called for each word and allow filtering/modifying words before * they are inserted into the databse. */ -public class WordVectorSqliteImporter { +public class WordVectorSqliteImporter implements Serializable { private static final int DEFAULT_MAX_WORD_LENGTH = 300; private static final Logger LOGGER = LoggerFactory.getLogger(WordVectorSqliteImporter.class); @@ -49,7 +50,7 @@ public class WordVectorSqliteImporter { * @throws SQLException if a database related error occurs */ public static void main(String[] args) throws SQLException, IOException { - ImportResult result = new WordVectorSqliteImporter(Path.of(args[0]), Path.of(args[1]), Integer.parseInt(args[2])).beginImport(); + ImportResult result = new WordVectorSqliteImporter(args[0], args[1], Integer.parseInt(args[2])).beginImport(); LOGGER.info("Inserted: {}\n", result.inserted); LOGGER.info("Skipped: ({})", result.skippedWords.size()); @@ -59,8 +60,8 @@ public static void main(String[] args) throws SQLException, IOException { record ImportResult(long inserted, ImmutableList skippedWords) { } - private final Path vectorFile; - private final Path dbFile; + private final String vectorFile; + private final String dbFile; private final int dimension; private final long startLine; @@ -75,7 +76,7 @@ record ImportResult(long inserted, ImmutableList skippedWords) { * @param dbFile the path to the sqlite database into which the vector representations will be inserted * @param dimension the dimension of the vectors */ - public WordVectorSqliteImporter(Path vectorFile, Path dbFile, int dimension) { + public WordVectorSqliteImporter(String vectorFile, String dbFile, int dimension) { this(vectorFile, dbFile, dimension, DEFAULT_MAX_WORD_LENGTH, 0, -1L, false); } @@ -92,7 +93,7 @@ public WordVectorSqliteImporter(Path vectorFile, Path dbFile, int dimension) { * @param dryRun whether this importer should actually insert. Use {@code false} to run this importer without * actually inserting anything */ - public WordVectorSqliteImporter(Path vectorFile, Path dbFile, int dimension, int maxWordLength, long startLine, long endLine, boolean dryRun) { + public WordVectorSqliteImporter(String vectorFile, String dbFile, int dimension, int maxWordLength, long startLine, long endLine, boolean dryRun) { this.vectorFile = vectorFile; this.dbFile = dbFile; this.dimension = dimension; @@ -101,11 +102,11 @@ public WordVectorSqliteImporter(Path vectorFile, Path dbFile, int dimension, int this.endLine = endLine; this.dryRun = dryRun; - if (!Files.exists(vectorFile)) { + if (!Files.exists(Path.of(vectorFile))) { throw new IllegalStateException("vectorFile does not exist: " + vectorFile); } - if (!Files.exists(dbFile)) { + if (!Files.exists(Path.of(dbFile))) { throw new IllegalStateException("dbFile does not exist: " + dbFile); } @@ -130,7 +131,7 @@ public ImportResult beginImport() throws SQLException, IOException, IllegalState try (Connection connection = connect(); PreparedStatement statement = prepareSelect(connection); - var in = Files.newInputStream(vectorFile, StandardOpenOption.READ); + var in = Files.newInputStream(Path.of(vectorFile), StandardOpenOption.READ); var bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { ByteBuffer buffer = ByteBuffer.allocate(dimension * 4); @@ -187,7 +188,7 @@ private Connection connect() throws SQLException { cfg.setSynchronous(SQLiteConfig.SynchronousMode.OFF); cfg.setOpenMode(SQLiteOpenMode.NOMUTEX); - return cfg.createConnection("jdbc:sqlite:" + this.dbFile.toAbsolutePath()); + return cfg.createConnection("jdbc:sqlite:" + Path.of(dbFile).toAbsolutePath()); } private PreparedStatement prepareSelect(Connection conn) throws SQLException { diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/AbstractConfigurable.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/AbstractConfigurable.java index 84086688f..d37cca2d6 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/AbstractConfigurable.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/AbstractConfigurable.java @@ -1,6 +1,11 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.configuration; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; @@ -10,14 +15,15 @@ import java.util.SortedMap; import java.util.TreeMap; +import org.apache.commons.lang3.reflect.FieldUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; @Deterministic -public abstract class AbstractConfigurable implements IConfigurable { - protected final Logger logger = LoggerFactory.getLogger(this.getClass()); +public abstract class AbstractConfigurable implements IConfigurable, Serializable { + protected final transient Logger logger = LoggerFactory.getLogger(this.getClass()); public static final String CLASS_ATTRIBUTE_CONNECTOR = "::"; public static final String KEY_VALUE_CONNECTOR = "="; @@ -122,4 +128,21 @@ private Object parse(Field field, Class fieldsClass, String value) { throw new IllegalArgumentException("Could not find a parse method for fields of type: " + fieldsClass); } + + @Serial + private void writeObject(ObjectOutputStream objectOutputStream) throws IOException { + objectOutputStream.defaultWriteObject(); + } + + @Serial + private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + objectInputStream.defaultReadObject(); + try { + var loggerField = Arrays.stream(FieldUtils.getAllFields(getClass())).filter(f -> f.getName().equals("logger")).findFirst().orElseThrow(); + loggerField.setAccessible(true); + loggerField.set(this, LoggerFactory.getLogger(this.getClass())); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/Configurable.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/Configurable.java index a719d6f3e..f2861202f 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/Configurable.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/Configurable.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.configuration; import java.lang.annotation.Documented; @@ -6,7 +6,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.SortedMap; +/** + * Marks a field as configurable. Should be used in conjunction with {@link IConfigurable}. The annotated field should not be marked as final or static, since + * the purpose of the field is to be written using an implementation of {@link IConfigurable#applyConfiguration(SortedMap)}. + */ @Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.FIELD) diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationInstantiatorUtils.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationInstantiatorUtils.java index 1fe8c33f4..b0a00c100 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationInstantiatorUtils.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationInstantiatorUtils.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.configuration; import java.lang.reflect.Constructor; @@ -20,7 +20,7 @@ private ConfigurationInstantiatorUtils() { /** * Create an AbstractConfigurable by Reflection. - * + * * @param clazz the class of the AbstractConfigurable * @return the abstract configurable * @throws InvocationTargetException if constructor execution does not work @@ -46,20 +46,25 @@ public static AbstractConfigurable createObject(Class c.getParameterCount() == 2 && c.getParameterTypes()[0] == String.class && c - .getParameterTypes()[1] == DataRepository.class, new Object[] { null, null }); + .getParameterTypes()[1] == DataRepository.class, new Object[] { null, new DataRepository() }); if (result != null) return result; result = findAndCreate(constructors, c -> c.getParameterCount() == 2 && c.getParameterTypes()[0] == DataRepository.class && c - .getParameterTypes()[1] == List.class, new Object[] { null, List.of() }); + .getParameterTypes()[1] == List.class, new Object[] { new DataRepository(), List.of() }); if (result != null) return result; - throw new IllegalStateException("Not reachable code reached for class " + clazz.getName()); + var c = constructors.stream().findFirst().orElseThrow(() -> new IllegalStateException("Not reachable code reached for class " + clazz.getName())); + var arguments = new Object[c.getParameterCount()]; + for (int i = 0; i < c.getParameterTypes().length; i++) { + var type = c.getParameterTypes()[i]; + arguments[i] = type.isAssignableFrom(DataRepository.class) ? new DataRepository() : null; + } + return (AbstractConfigurable) c.newInstance(arguments); } - @SuppressWarnings("java:S3011") private static AbstractConfigurable findAndCreate(Collection> constructors, Predicate> selector, Object[] parameters) throws InvocationTargetException, InstantiationException, IllegalAccessException { if (constructors.stream().noneMatch(selector)) { diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationUtility.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationUtility.java new file mode 100644 index 000000000..68deded7a --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/configuration/ConfigurationUtility.java @@ -0,0 +1,90 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.configuration; + +import static edu.kit.kastel.mcse.ardoco.core.configuration.AbstractConfigurable.CLASS_ATTRIBUTE_CONNECTOR; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractExecutionStage; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; + +@Deterministic +public class ConfigurationUtility { + private ConfigurationUtility() { + throw new IllegalStateException("Cannot be instantiated"); + } + + /** + * {@return a map of configurations that will enable the specified agents} + * + * @param enabledAgents Set of agents that should be enabled + */ + public static SortedMap enableAgents(Class stage, Set> enabledAgents) { + var map = new TreeMap(); + for (var agent : enabledAgents) { + var listString = List.of(agent.getSimpleName()).toString(); + map.put(stage.getSimpleName() + CLASS_ATTRIBUTE_CONNECTOR + "enabledAgents", listString.substring(1, listString.length() - 1)); + } + return Collections.unmodifiableSortedMap(map); + } + + /** + * {@return a map of configurations that will enable the specified informants} Will also enable the necessary agents. + * + * @param enabledInformant Set of informants that should be enabled + */ + public static SortedMap enableInformants(AbstractExecutionStage stage, Set> enabledInformant) { + var map = new TreeMap(); + var agentsToEnable = new LinkedHashSet>(); + for (var informant : enabledInformant) { + getInformantsMap(stage).entrySet().stream().filter(e -> e.getValue().contains(informant)).forEach(e -> { + agentsToEnable.add(e.getKey()); + var listString = List.of(informant.getSimpleName()).toString(); + map.put(e.getKey().getSimpleName() + CLASS_ATTRIBUTE_CONNECTOR + "enabledInformants", listString.substring(1, listString.length() - 1)); + }); + } + //Make sure we enable the agents which run the informants + map.putAll(enableAgents(stage.getClass(), new LinkedHashSet<>(agentsToEnable))); + return Collections.unmodifiableSortedMap(map); + } + + public static Set> getAgents(AbstractExecutionStage stage) { + var agents = stage.getAgents(); + var clazzes = new ArrayList>(); + for (var agent : agents) { + var clazz = agent.getClass(); + clazzes.add(clazz); + } + return new LinkedHashSet<>(clazzes); + } + + public static Set> getInformants(PipelineAgent agent) { + var informants = agent.getInformants(); + var clazzes = new ArrayList>(); + for (var informant : informants) { + var clazz = informant.getClass(); + clazzes.add(clazz); + } + return new LinkedHashSet<>(clazzes); + } + + public static Map, Set>> getInformantsMap(AbstractExecutionStage stage) { + var map = new LinkedHashMap, Set>>(); + var agents = stage.getAgents(); + for (var agent : agents) { + map.put(agent.getClass(), getInformants(agent)); + } + + return new LinkedHashMap<>(map); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/AbstractState.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/AbstractState.java index afa28ebdb..eb7c3279f 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/AbstractState.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/AbstractState.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.data; import java.util.SortedMap; diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/Confidence.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/Confidence.java index f19876002..86a0d737d 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/Confidence.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/Confidence.java @@ -1,8 +1,10 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.data; +import java.io.Serializable; import java.util.ArrayList; -import java.util.LinkedHashSet; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Objects; import java.util.Set; @@ -15,11 +17,11 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; /** - * This class represents a confidence for a certain (intermediate) result. - * Different {@link Claimant Claimants} can add their confidences that get aggregated via one of the {@link AggregationFunctions} to a single confidence value. + * This class represents a confidence for a certain (intermediate) result. Different {@link Claimant Claimants} can add their confidences that get aggregated + * via one of the {@link AggregationFunctions} to a single confidence value. */ @Deterministic -public final class Confidence implements Comparable, ICopyable { +public final class Confidence implements Comparable, ICopyable, Serializable { private final AggregationFunctions confidenceAggregator; @@ -59,7 +61,7 @@ private Confidence(AggregationFunctions confidenceAggregator, List getClaimants() { - Set identitySet = new LinkedHashSet<>(); + Set identitySet = Collections.newSetFromMap(new IdentityHashMap<>()); for (var confidence : this.agentConfidences) identitySet.add(confidence.first()); return identitySet; diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DataRepository.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DataRepository.java index b7c2eb3a2..e2014b46c 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DataRepository.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DataRepository.java @@ -1,6 +1,7 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.data; +import java.io.Serializable; import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; @@ -8,29 +9,39 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; + /** - * This class represents a data repository that can be used to store and fetch certain data ({@link PipelineStepData}. - * Data can be added and fetched with the help of a data identifier (as string). Fetching also needs the necessary class - * of data that is expected. + * This class represents a data repository that can be used to store and fetch certain data ({@link PipelineStepData}. Data can be added and fetched with the + * help of a data identifier (as string). Fetching also needs the necessary class of data that is expected. */ -public class DataRepository { +public class DataRepository implements Serializable { private static final Logger logger = LoggerFactory.getLogger(DataRepository.class); private final SortedMap data; public DataRepository() { this.data = new TreeMap<>(); + addData(GlobalConfiguration.ID, new GlobalConfiguration()); } /** - * Returns data with the given identifier and casts the {@link PipelineStepData} into the given class, if possible. - * If data with such identifier does not exist or cannot be cast, this method will return an empty Optional + * Returns the {@link GlobalConfiguration} stored within the provided {@link DataRepository}. * + * @return the data + */ + public final GlobalConfiguration getGlobalConfiguration() { + return getData(GlobalConfiguration.ID, GlobalConfiguration.class).orElseThrow(); + } + + /** + * Returns data with the given identifier and casts the {@link PipelineStepData} into the given class, if possible. If data with such identifier does not + * exist or cannot be cast, this method will return an empty Optional + * * @param identifier Data identifier string * @param clazz class that the data should have - * @return Optional containing the requested data cast into the given class. The optional is empty is data could not - * be found or casting was unsuccessful. - * @param Type of data that is expected and cast into + * @param Type of data that is expected and cast into + * @return Optional containing the requested data cast into the given class. The optional is empty is data could not be found or casting was unsuccessful. */ public Optional getData(String identifier, Class clazz) { var possibleData = data.get(identifier); @@ -42,9 +53,8 @@ public Optional getData(String identifier, Class } /** - * Adds data to this repository using the identifier. If data with the given identifier already exists, overwrites - * it. - * + * Adds data to this repository using the identifier. If data with the given identifier already exists, overwrites it. + * * @param identifier Data identifier * @param pipelineStepData Data that should be saved */ @@ -54,4 +64,22 @@ public void addData(String identifier, PipelineStepData pipelineStepData) { } } + /** + * Adds all data to the existing repository using the provided repository. + * + * @param dataRepository data repository + */ + public void addAllData(DataRepository dataRepository) { + this.data.putAll(dataRepository.data); + } + + /** + * Creates a deep copy of the data repository using serialization. + * + * @return deep copy of the data repository + */ + @DeepCopy + public DataRepository deepCopy() { + return DataRepositoryHelper.deepCopy(this); + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DeepCopy.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DeepCopy.java new file mode 100644 index 000000000..9da441d8d --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/DeepCopy.java @@ -0,0 +1,15 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.data; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Marks the method or parameter as using deep copies. If a method is annotated as deep copy, it only returns deep copies that can be freely modified. If a + * parameter is annotated as deep copy, the method will calculate and use a deep copy of it rather than the object itself. + */ +@Documented +@Target({ ElementType.METHOD, ElementType.PARAMETER }) +public @interface DeepCopy { +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/GlobalConfiguration.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/GlobalConfiguration.java new file mode 100644 index 000000000..8e9e3c680 --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/GlobalConfiguration.java @@ -0,0 +1,70 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.data; + +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimUtils; +import edu.kit.kastel.mcse.ardoco.core.pipeline.Pipeline; + +/** + * Contains global configuration about the pipeline which produced this data. + */ +public class GlobalConfiguration implements PipelineStepData { + public static final String ID = "PipelineMetaData"; + private Pipeline pipeline; + private final WordSimUtils wordSimUtils; + private final SimilarityUtils similarityUtils; + + /** + * Constructs a new PipelineMetaData with the given global configuration data + * + * @param pipeline the runner which produced the {@link DataRepository} this data is associated with + * @param wordSimUtils the configured word similarity utility instance that should be used + * @param similarityUtils the configured similarity util instance that should be used + */ + public GlobalConfiguration(Pipeline pipeline, WordSimUtils wordSimUtils, SimilarityUtils similarityUtils) { + this.pipeline = pipeline; + this.wordSimUtils = wordSimUtils; + this.similarityUtils = similarityUtils; + } + + /** + * Constructs a new PipelineMetaData with the given global configuration + * + * @param pipeline the pipeline which produced the {@link DataRepository} this data is associated with + */ + public GlobalConfiguration(Pipeline pipeline) { + this.pipeline = pipeline; + this.wordSimUtils = new WordSimUtils(); + this.similarityUtils = new SimilarityUtils(wordSimUtils); + } + + public GlobalConfiguration() { + this.wordSimUtils = new WordSimUtils(); + this.similarityUtils = new SimilarityUtils(wordSimUtils); + } + + /** + * {@return the runner which produced the DataRepository this data is associated with} + */ + public Pipeline getPipeline() { + return this.pipeline; + } + + public void setPipeline(Pipeline pipeline) { + this.pipeline = pipeline; + } + + /** + * {@return the configured word similarity utility instance} + */ + public WordSimUtils getWordSimUtils() { + return this.wordSimUtils; + } + + /** + * {@return the configured similarity utility instance} + */ + public SimilarityUtils getSimilarityUtils() { + return this.similarityUtils; + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/PipelineStepData.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/PipelineStepData.java index b97967daa..82b46f59b 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/PipelineStepData.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/PipelineStepData.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.data; import java.io.Serializable; @@ -7,10 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import edu.kit.kastel.mcse.ardoco.core.common.JsonHandling; /** * This abstract class defines data that is used for the pipeline steps. @@ -36,7 +33,7 @@ default Optional asPipelineStepData(Class cla } default String serialize() { - var oom = createObjectMapper(); + var oom = JsonHandling.createObjectMapper(); try { return oom.writeValueAsString(this); } catch (Exception e) { @@ -46,7 +43,7 @@ default String serialize() { } default PipelineStepData deserialize(String data) { - var oom = createObjectMapper(); + var oom = JsonHandling.createObjectMapper(); try { return oom.readValue(data, this.getClass()); } catch (Exception e) { @@ -54,17 +51,4 @@ default PipelineStepData deserialize(String data) { return null; } } - - private static ObjectMapper createObjectMapper() { - var objectMapper = new ObjectMapper(); - objectMapper.configure(SerializationFeature.INDENT_OUTPUT, false); - objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - objectMapper.setVisibility(objectMapper.getSerializationConfig() - .getDefaultVisibilityChecker() // - .withFieldVisibility(JsonAutoDetect.Visibility.ANY) // - .withGetterVisibility(JsonAutoDetect.Visibility.NONE) // - .withSetterVisibility(JsonAutoDetect.Visibility.NONE) // - .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); - return objectMapper; - } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/ProjectPipelineData.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/ProjectPipelineData.java index 70dfe67c2..b9f3c8e5e 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/ProjectPipelineData.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/data/ProjectPipelineData.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.data; /** @@ -13,5 +13,4 @@ public interface ProjectPipelineData extends PipelineStepData { * @return the project name */ String getProjectName(); - } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractExecutionStage.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractExecutionStage.java index 6bad5b494..39e11d211 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractExecutionStage.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractExecutionStage.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline; import java.util.List; @@ -19,7 +19,6 @@ * Implementing classes need to implement {@link #initializeState()} that cares for setting up the state for processing. */ public abstract class AbstractExecutionStage extends Pipeline { - private final MutableList agents; @Configurable @@ -28,7 +27,7 @@ public abstract class AbstractExecutionStage extends Pipeline { /** * Constructor for ExecutionStages - * + * * @param agents the agents that could be executed by this pipeline * @param id the id of the stage * @param dataRepository the {@link DataRepository} that should be used @@ -41,14 +40,7 @@ protected AbstractExecutionStage(List agents, String id @Override protected final void preparePipelineSteps() { - initialize(); super.preparePipelineSteps(); - } - - /** - * Initialize the {@link AbstractExecutionStage}. Within this method, cares about all agents that should be executed by this pipeline - */ - protected final void initialize() { initializeState(); for (var agent : agents) { @@ -63,6 +55,29 @@ protected final void initialize() { */ protected abstract void initializeState(); + /** + * Called before all agents + */ + @Override + protected void before() { + //Nothing by default + } + + /** + * Called after all agents + */ + @Override + protected void after() { + //Nothing by default + } + + /** + * {@return the {@link PipelineAgent agents}} + */ + public List getAgents() { + return List.copyOf(agents); + } + @Override protected void delegateApplyConfigurationToInternalObjects(SortedMap additionalConfiguration) { super.delegateApplyConfigurationToInternalObjects(additionalConfiguration); diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractPipelineStep.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractPipelineStep.java index 1f6f154e8..463366944 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractPipelineStep.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/AbstractPipelineStep.java @@ -1,16 +1,17 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline; import edu.kit.kastel.mcse.ardoco.core.configuration.AbstractConfigurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; /** - * This class represents an abstract pipeline step and defines the core functionality. Together with {@link Pipeline} - * and concrete implementations of this class represents a composite pattern. + * This class represents an abstract pipeline step and defines the core functionality. Together + * with {@link Pipeline} and concrete implementations of this class + * represents a composite pattern. */ public abstract class AbstractPipelineStep extends AbstractConfigurable { - private final String id; - private final DataRepository dataRepository; + protected final String id; + protected final DataRepository dataRepository; /** * Constructor for a pipeline step @@ -24,14 +25,32 @@ protected AbstractPipelineStep(String id, DataRepository dataRepository) { } /** - * Run the pipeline step. + * Runs the pipeline step beginning with {@link #before()}, {@link #process()} and finally + * {@link #after()} */ - public abstract void run(); + public void run() { + before(); + process(); + after(); + } /** - * Returns the {@link DataRepository} that is used for saving and fetching data. - * - * @return the repository for used data + * Processes the pipeline step + */ + protected abstract void process(); + + /** + * Called before the pipeline step + */ + protected abstract void before(); + + /** + * Called after the pipeline step + */ + protected abstract void after(); + + /** + * {@return the {@link DataRepository} that is used for saving and fetching data} */ protected DataRepository getDataRepository() { return this.dataRepository; diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/ExecutionStage.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/ExecutionStage.java new file mode 100644 index 000000000..b6486c90b --- /dev/null +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/ExecutionStage.java @@ -0,0 +1,31 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.pipeline; + +import java.util.List; +import java.util.SortedMap; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; + +public abstract class ExecutionStage extends AbstractExecutionStage { + private SortedMap additionalConfigs; + + /** + * Creates an {@link ExecutionStage} and applies the additional configuration to it + * + * @param id the id of the stage + * @param dataRepository the {@link DataRepository} that should be used + * @param agents the pipeline agents this stage supports + * @param additionalConfigs the additional configuration + */ + protected ExecutionStage(List agents, String id, DataRepository dataRepository, SortedMap additionalConfigs) { + super(agents, id, dataRepository); + this.additionalConfigs = additionalConfigs; + } + + @Override + protected void before() { + super.before(); + applyConfiguration(additionalConfigs); + } +} diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/Pipeline.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/Pipeline.java index bfb9aa3ac..6f31dd29c 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/Pipeline.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/Pipeline.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline; import java.time.Duration; @@ -10,12 +10,15 @@ import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; /** - * Class that represents a pipeline that can consist of multiple {@link AbstractPipelineStep AbstractPipelineSteps}. - * Steps are executed consecutively one after another in the order they were added to the pipeline. Execution calls the - * {@link #run()} method of the different {@link AbstractPipelineStep AbstractPipelineSteps}. + * Class that represents a pipeline that can consist of multiple {@link AbstractPipelineStep + * AbstractPipelineSteps}. + * Steps are executed consecutively one after another in the order they were added to the + * pipeline. Execution calls the + * {@link #process()} method of the different {@link AbstractPipelineStep AbstractPipelineSteps}. */ public class Pipeline extends AbstractPipelineStep { private final List pipelineSteps; + private boolean executed = false; /** * Constructs a Pipeline with the given id and {@link DataRepository}. @@ -33,7 +36,8 @@ public Pipeline(String id, DataRepository dataRepository) { * * @param id id for the pipeline * @param dataRepository {@link DataRepository} that should be used for fetching and saving data - * @param pipelineSteps List of {@link AbstractPipelineStep} that should be added to the constructed pipeline + * @param pipelineSteps List of {@link AbstractPipelineStep} that should be added to the + * constructed pipeline */ public Pipeline(String id, DataRepository dataRepository, List pipelineSteps) { super(id, dataRepository); @@ -59,8 +63,15 @@ public boolean addPipelineStep(AbstractPipelineStep pipelineStep) { return this.pipelineSteps.add(pipelineStep); } + /** + * {@return whether the pipeline has finished execution} + */ + public boolean wasExecuted() { + return this.executed; + } + @Override - public final void run() { + public void process() { preparePipelineSteps(); for (var pipelineStep : this.pipelineSteps) { logger.info("Starting {} - {}", this.getId(), pipelineStep.getId()); @@ -86,8 +97,19 @@ public final void run() { } } + @Override + protected void before() { + //Nothing by default + } + + @Override + protected void after() { + executed = true; + } + /** - * This method is called at the start of running the pipeline. Within this method, the added PipelineSteps are prepared. + * This method is called at the start of running the pipeline. Within this method, the added + * PipelineSteps are prepared. * Sub-classes of Pipeline can override it with special cases. * It is recommended that you apply the Map from {@link #getLastAppliedConfiguration()} via {@link #applyConfiguration(SortedMap)} to each pipeline step. * You can do that on your own if you need special treatment or by default call {@link #delegateApplyConfigurationToInternalObjects(SortedMap)}. diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Claimant.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Claimant.java index b1cf0fa5e..3fa9ca809 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Claimant.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Claimant.java @@ -1,8 +1,10 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline.agent; +import java.io.Serializable; + /** * This is a marker interface for classes that claim something, i.e., an intermediate result with usually a certain confidence. */ -public interface Claimant { +public interface Claimant extends Serializable { } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Informant.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Informant.java index a61c39aae..e69381dcc 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Informant.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/Informant.java @@ -1,11 +1,33 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline.agent; +import java.util.SortedMap; + import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; public abstract class Informant extends AbstractPipelineStep implements Claimant { protected Informant(String id, DataRepository dataRepository) { super(id, dataRepository); } + + @Override + protected void before() { + //Nothing by default + } + + @Override + protected void after() { + //Nothing by default + } + + @Override + protected void delegateApplyConfigurationToInternalObjects(SortedMap additionalConfiguration) { + //Nothing by default + } + + protected final GlobalConfiguration getMetaData() { + return getDataRepository().getGlobalConfiguration(); + } } diff --git a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/PipelineAgent.java b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/PipelineAgent.java index ac07b081d..4cdac59bc 100644 --- a/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/PipelineAgent.java +++ b/framework/common/src/main/java/edu/kit/kastel/mcse/ardoco/core/pipeline/agent/PipelineAgent.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline.agent; import java.util.ArrayList; @@ -12,20 +12,26 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.Pipeline; /** - * This class represents a pipeline agent that calculates some results for an {@link AbstractExecutionStage} execution - * stage}. + * This class represents a pipeline agent that calculates some results for an {@link AbstractExecutionStage} execution stage}. * - * Implementing classes need to override. - * Additionally, sub-classes are free to override {@link #initializeState()} to execute code at the beginning of the initialization before the main processing. + * Implementing classes need to override. Additionally, sub-classes are free to override {@link #initializeState()} to execute code at the beginning of the + * initialization before the main processing. */ public abstract class PipelineAgent extends Pipeline implements Agent { - - private final List informants; + private final List informants; @Configurable @ChildClassConfigurable private List enabledInformants; + /** + * Creates a new pipeline agent with the specified id. During execution the pipeline agent sequentially runs its informants on the provided data + * repository. + * + * @param informants the informants in order of execution (all enabled by default) + * @param id the id + * @param dataRepository the data repository + */ protected PipelineAgent(List informants, String id, DataRepository dataRepository) { super(id, dataRepository); this.informants = new ArrayList<>(informants); @@ -34,8 +40,24 @@ protected PipelineAgent(List informants, String id, DataRep @Override protected final void preparePipelineSteps() { - initialize(); super.preparePipelineSteps(); + initialize(); + } + + /** + * Called before all informants + */ + @Override + protected void before() { + //Nothing by default + } + + /** + * Called after all informants + */ + @Override + protected void after() { + //Nothing by default } /** @@ -57,6 +79,13 @@ protected void initializeState() { // do nothing here } + /** + * {@return the informants including disabled} + */ + public List getInformants() { + return List.copyOf(informants); + } + @Override protected void delegateApplyConfigurationToInternalObjects(SortedMap additionalConfiguration) { super.delegateApplyConfigurationToInternalObjects(additionalConfiguration); diff --git a/framework/common/src/main/resources/configs/CommonTextToolsConfig.properties b/framework/common/src/main/resources/configs/CommonTextToolsConfig.properties index 656842f49..9944b93a6 100644 --- a/framework/common/src/main/resources/configs/CommonTextToolsConfig.properties +++ b/framework/common/src/main/resources/configs/CommonTextToolsConfig.properties @@ -2,6 +2,7 @@ separators_ToContain=. :: : _ separators_ToSplit=\\. :: : _ getMostRecommendedIByRef_MinProportion=0.5 getMostRecommendedIByRef_Increase=0.05 +considerAbbreviations=false # Levenshtein levenshtein_Enabled=true levenshtein_MinLength=2 @@ -22,4 +23,7 @@ sewordsim_DatabaseFilePath= glove_Enabled=false glove_SimilarityThreshold=0.75 glove_DatabaseFilePath= +# DE-Sim +de_NM_SimilarityThreshold=0.8 +de_Word_SimilarityThreshold=0.8 diff --git a/framework/common/src/main/resources/wordsim/confusablesSummary.txt b/framework/common/src/main/resources/wordsim/confusablesSummary.txt new file mode 100644 index 000000000..77507c742 --- /dev/null +++ b/framework/common/src/main/resources/wordsim/confusablesSummary.txt @@ -0,0 +1,17207 @@ +# confusablesSummary.txt +# Date: 2022-08-26, 16:49:08 GMT +# © 2022 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see https://www.unicode.org/terms_of_use.html +# +# Unicode Security Mechanisms for UTS #39 +# Version: 15.0.0 +# +# For documentation and usage, see https://www.unicode.org/reports/tr39 +# + +#   
 
                             + (‎ ‎) 0020 SPACE +← (‎   ‎) 1680 OGHAM SPACE MARK +← (‎ 
 ‎) 2028 LINE SEPARATOR +← (‎ 
 ‎) 2029 PARAGRAPH SEPARATOR +← (‎   ‎) 00A0 NO-BREAK SPACE +← (‎   ‎) 2000 EN QUAD +← (‎   ‎) 2001 EM QUAD +← (‎   ‎) 2002 EN SPACE +← (‎   ‎) 2003 EM SPACE +← (‎   ‎) 2004 THREE-PER-EM SPACE +← (‎   ‎) 2005 FOUR-PER-EM SPACE +← (‎   ‎) 2006 SIX-PER-EM SPACE +← (‎   ‎) 2007 FIGURE SPACE +← (‎   ‎) 2008 PUNCTUATION SPACE +← (‎   ‎) 2009 THIN SPACE +← (‎   ‎) 200A HAIR SPACE +← (‎   ‎) 202F NARROW NO-BREAK SPACE +← (‎   ‎) 205F MEDIUM MATHEMATICAL SPACE + +# ! ǃ ⵑ ! + (‎ ! ‎) 0021 EXCLAMATION MARK +← (‎ ǃ ‎) 01C3 LATIN LETTER RETROFLEX CLICK +← (‎ ⵑ ‎) 2D51 TIFINAGH LETTER TUAREG YANG +← (‎ ! ‎) FF01 FULLWIDTH EXCLAMATION MARK # →ǃ→ + +# !! ‼ + (‎ !! ‎) 0021 0021 EXCLAMATION MARK, EXCLAMATION MARK +← (‎ ‼ ‎) 203C DOUBLE EXCLAMATION MARK + +# !? ⁉ + (‎ !? ‎) 0021 003F EXCLAMATION MARK, QUESTION MARK +← (‎ ⁉ ‎) 2049 EXCLAMATION QUESTION MARK + +# '' יי ′′ ‵‵ " ײ ʺ ˮ ״ ˶ ᳓ “ ” ‟ 〃 ˝ ″ ‶ " + (‎ " ‎) 0022 QUOTATION MARK +← (‎ '' ‎) 0027 0027 APOSTROPHE, APOSTROPHE +← (‎ יי ‎) 05D9 05D9 HEBREW LETTER YOD, HEBREW LETTER YOD # →''→ +← (‎ ′′ ‎) 2032 2032 PRIME, PRIME # →″→ +← (‎ ‵‵ ‎) 2035 2035 REVERSED PRIME, REVERSED PRIME # →''→ +← (‎ ײ ‎) 05F2 HEBREW LIGATURE YIDDISH DOUBLE YOD # →‎יי‎→→''→ +← (‎ ʺ ‎) 02BA MODIFIER LETTER DOUBLE PRIME +← (‎ ˮ ‎) 02EE MODIFIER LETTER DOUBLE APOSTROPHE # →″→ +← (‎ ״ ‎) 05F4 HEBREW PUNCTUATION GERSHAYIM +← (‎ ˶ ‎) 02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT # →˝→ +← (‎ ᳓ ‎) 1CD3 VEDIC SIGN NIHSHVASA # →″→ +← (‎ “ ‎) 201C LEFT DOUBLE QUOTATION MARK +← (‎ ” ‎) 201D RIGHT DOUBLE QUOTATION MARK +← (‎ ‟ ‎) 201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK # →“→ +← (‎ 〃 ‎) 3003 DITTO MARK # →″→ +← (‎ ˝ ‎) 02DD DOUBLE ACUTE ACCENT +← (‎ ″ ‎) 2033 DOUBLE PRIME +← (‎ ‶ ‎) 2036 REVERSED DOUBLE PRIME # →‵‵→→''→ +← (‎ " ‎) FF02 FULLWIDTH QUOTATION MARK # →”→ + +# $⃠ 🄏 + (‎ $⃠ ‎) 0024 20E0 DOLLAR SIGN, COMBINING ENCLOSING CIRCLE BACKSLASH +← (‎ 🄏 ‎) 1F10F CIRCLED DOLLAR SIGN WITH OVERLAID BACKSLASH + +# º/₀ ⁰/₀ % ٪ ⁒ + (‎ % ‎) 0025 PERCENT SIGN +← (‎ º/₀ ‎) 00BA 002F 2080 MASCULINE ORDINAL INDICATOR, SOLIDUS, SUBSCRIPT ZERO # →⁰/₀→ +← (‎ ⁰/₀ ‎) 2070 002F 2080 SUPERSCRIPT ZERO, SOLIDUS, SUBSCRIPT ZERO +← (‎ ٪ ‎) 066A ARABIC PERCENT SIGN +← (‎ ⁒ ‎) 2052 COMMERCIAL MINUS SIGN + +# & ꝸ + (‎ & ‎) 0026 AMPERSAND +← (‎ ꝸ ‎) A778 LATIN SMALL LETTER UM + +# ' ` ꞌ ʻ ʼ י ʹ ʽ ʾ ˈ ˊ ˋ ߴ ߵ ᑊ ᛌ 𖽑 𖽒 ׳ ’ ˴ ՚ ՝ ‘ ‛ ′ ‵ ʹ ´ ΄ ᾽ ᾿ ` ´ ῾ ' ` + (‎ ' ‎) 0027 APOSTROPHE +← (‎ ` ‎) 0060 GRAVE ACCENT # →ˋ→→`→→‘→ +← (‎ ꞌ ‎) A78C LATIN SMALL LETTER SALTILLO +← (‎ ʻ ‎) 02BB MODIFIER LETTER TURNED COMMA # →‘→ +← (‎ ʼ ‎) 02BC MODIFIER LETTER APOSTROPHE # →′→ +← (‎ י ‎) 05D9 HEBREW LETTER YOD +← (‎ ʹ ‎) 02B9 MODIFIER LETTER PRIME +← (‎ ʽ ‎) 02BD MODIFIER LETTER REVERSED COMMA # →‘→ +← (‎ ʾ ‎) 02BE MODIFIER LETTER RIGHT HALF RING # →ʼ→→′→ +← (‎ ˈ ‎) 02C8 MODIFIER LETTER VERTICAL LINE +← (‎ ˊ ‎) 02CA MODIFIER LETTER ACUTE ACCENT # →ʹ→→′→ +← (‎ ˋ ‎) 02CB MODIFIER LETTER GRAVE ACCENT # →`→→‘→ +← (‎ ߴ ‎) 07F4 NKO HIGH TONE APOSTROPHE # →’→ +← (‎ ߵ ‎) 07F5 NKO LOW TONE APOSTROPHE # →‘→ +← (‎ ᑊ ‎) 144A CANADIAN SYLLABICS WEST-CREE P # →ˈ→ +← (‎ ᛌ ‎) 16CC RUNIC LETTER SHORT-TWIG-SOL S +← (‎ 𖽑 ‎) 16F51 MIAO SIGN ASPIRATION # →ʼ→→′→ +← (‎ 𖽒 ‎) 16F52 MIAO SIGN REFORMED VOICING # →ʻ→→‘→ +← (‎ ׳ ‎) 05F3 HEBREW PUNCTUATION GERESH +← (‎ ’ ‎) 2019 RIGHT SINGLE QUOTATION MARK +← (‎ ˴ ‎) 02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT # →ˋ→→`→→‘→ +← (‎ ՚ ‎) 055A ARMENIAN APOSTROPHE # →’→ +← (‎ ՝ ‎) 055D ARMENIAN COMMA # →ˋ→→`→→‘→ +← (‎ ‘ ‎) 2018 LEFT SINGLE QUOTATION MARK +← (‎ ‛ ‎) 201B SINGLE HIGH-REVERSED-9 QUOTATION MARK # →′→ +← (‎ ′ ‎) 2032 PRIME +← (‎ ‵ ‎) 2035 REVERSED PRIME # →ʽ→→‘→ +← (‎ ʹ ‎) 0374 GREEK NUMERAL SIGN # →′→ +← (‎ ´ ‎) 00B4 ACUTE ACCENT # →΄→→ʹ→ +← (‎ ΄ ‎) 0384 GREEK TONOS # →ʹ→ +← (‎ ᾽ ‎) 1FBD GREEK KORONIS # →’→ +← (‎ ᾿ ‎) 1FBF GREEK PSILI # →’→ +← (‎ ` ‎) 1FEF GREEK VARIA # →ˋ→→`→→‘→ +← (‎ ´ ‎) 1FFD GREEK OXIA # →´→→΄→→ʹ→ +← (‎ ῾ ‎) 1FFE GREEK DASIA # →‛→→′→ +← (‎ ' ‎) FF07 FULLWIDTH APOSTROPHE # →’→ +← (‎ ` ‎) FF40 FULLWIDTH GRAVE ACCENT # →‘→ + +# ''' ′′′ ‵‵‵ ‴ ‷ + (‎ ''' ‎) 0027 0027 0027 APOSTROPHE, APOSTROPHE, APOSTROPHE +← (‎ ′′′ ‎) 2032 2032 2032 PRIME, PRIME, PRIME +← (‎ ‵‵‵ ‎) 2035 2035 2035 REVERSED PRIME, REVERSED PRIME, REVERSED PRIME +← (‎ ‴ ‎) 2034 TRIPLE PRIME # →′′′→ +← (‎ ‷ ‎) 2037 REVERSED TRIPLE PRIME # →‵‵‵→ + +# '''' ′′′′ ⁗ + (‎ '''' ‎) 0027 0027 0027 0027 APOSTROPHE, APOSTROPHE, APOSTROPHE, APOSTROPHE +← (‎ ′′′′ ‎) 2032 2032 2032 2032 PRIME, PRIME, PRIME, PRIME +← (‎ ⁗ ‎) 2057 QUADRUPLE PRIME # →′′′′→ + +# 'B ʽB Ɓ + (‎ 'B ‎) 0027 0042 APOSTROPHE, LATIN CAPITAL LETTER B +← (‎ ʽB ‎) 02BD 0042 MODIFIER LETTER REVERSED COMMA, LATIN CAPITAL LETTER B +← (‎ Ɓ ‎) 0181 LATIN CAPITAL LETTER B WITH HOOK # →ʽB→ + +# 'D ʽD Ɗ + (‎ 'D ‎) 0027 0044 APOSTROPHE, LATIN CAPITAL LETTER D +← (‎ ʽD ‎) 02BD 0044 MODIFIER LETTER REVERSED COMMA, LATIN CAPITAL LETTER D +← (‎ Ɗ ‎) 018A LATIN CAPITAL LETTER D WITH HOOK # →ʽD→ + +# 'P ʽP Ƥ + (‎ 'P ‎) 0027 0050 APOSTROPHE, LATIN CAPITAL LETTER P +← (‎ ʽP ‎) 02BD 0050 MODIFIER LETTER REVERSED COMMA, LATIN CAPITAL LETTER P +← (‎ Ƥ ‎) 01A4 LATIN CAPITAL LETTER P WITH HOOK # →ʽP→ + +# 'T ʽT Ƭ + (‎ 'T ‎) 0027 0054 APOSTROPHE, LATIN CAPITAL LETTER T +← (‎ ʽT ‎) 02BD 0054 MODIFIER LETTER REVERSED COMMA, LATIN CAPITAL LETTER T +← (‎ Ƭ ‎) 01AC LATIN CAPITAL LETTER T WITH HOOK # →ʽT→ + +# 'Y ʽY Ƴ + (‎ 'Y ‎) 0027 0059 APOSTROPHE, LATIN CAPITAL LETTER Y +← (‎ ʽY ‎) 02BD 0059 MODIFIER LETTER REVERSED COMMA, LATIN CAPITAL LETTER Y +← (‎ Ƴ ‎) 01B3 LATIN CAPITAL LETTER Y WITH HOOK # →ʽY→ + +# 'n ʼn ʼn + (‎ 'n ‎) 0027 006E APOSTROPHE, LATIN SMALL LETTER N +← (‎ ʼn ‎) 02BC 006E MODIFIER LETTER APOSTROPHE, LATIN SMALL LETTER N +← (‎ ʼn ‎) 0149 LATIN SMALL LETTER N PRECEDED BY APOSTROPHE # →ʼn→ + +# ( ❨ ❲ 〔 ﴾ [ + (‎ ( ‎) 0028 LEFT PARENTHESIS +← (‎ ❨ ‎) 2768 MEDIUM LEFT PARENTHESIS ORNAMENT +← (‎ ❲ ‎) 2772 LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT # →〔→ +← (‎ 〔 ‎) 3014 LEFT TORTOISE SHELL BRACKET +← (‎ ﴾ ‎) FD3E ORNATE LEFT PARENTHESIS +← (‎ [ ‎) FF3B FULLWIDTH LEFT SQUARE BRACKET # →〔→ + +# (( ⸨ + (‎ (( ‎) 0028 0028 LEFT PARENTHESIS, LEFT PARENTHESIS +← (‎ ⸨ ‎) 2E28 LEFT DOUBLE PARENTHESIS + +# (l) (I) (1) ⑴ ⒧ 🄘 + (‎ (1) ‎) 0028 0031 0029 LEFT PARENTHESIS, DIGIT ONE, RIGHT PARENTHESIS +← (‎ (l) ‎) 0028 006C 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, RIGHT PARENTHESIS +← (‎ (I) ‎) 0028 0049 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER I, RIGHT PARENTHESIS # →(l)→ +← (‎ ⑴ ‎) 2474 PARENTHESIZED DIGIT ONE +← (‎ ⒧ ‎) 24A7 PARENTHESIZED LATIN SMALL LETTER L # →(l)→ +← (‎ 🄘 ‎) 1F118 PARENTHESIZED LATIN CAPITAL LETTER I # →(I)→→(l)→ + +# (lO) (l0) (10) ⑽ + (‎ (10) ‎) 0028 0031 0030 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT ZERO, RIGHT PARENTHESIS +← (‎ (lO) ‎) 0028 006C 004F 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, RIGHT PARENTHESIS +← (‎ (l0) ‎) 0028 006C 0030 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT ZERO, RIGHT PARENTHESIS +← (‎ ⑽ ‎) 247D PARENTHESIZED NUMBER TEN + +# (ll) (11) ⑾ + (‎ (11) ‎) 0028 0031 0031 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT ONE, RIGHT PARENTHESIS +← (‎ (ll) ‎) 0028 006C 006C 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, LATIN SMALL LETTER L, RIGHT PARENTHESIS +← (‎ ⑾ ‎) 247E PARENTHESIZED NUMBER ELEVEN + +# (l2) (12) ⑿ + (‎ (12) ‎) 0028 0031 0032 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT TWO, RIGHT PARENTHESIS +← (‎ (l2) ‎) 0028 006C 0032 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT TWO, RIGHT PARENTHESIS +← (‎ ⑿ ‎) 247F PARENTHESIZED NUMBER TWELVE + +# (l3) (13) ⒀ + (‎ (13) ‎) 0028 0031 0033 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT THREE, RIGHT PARENTHESIS +← (‎ (l3) ‎) 0028 006C 0033 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT THREE, RIGHT PARENTHESIS +← (‎ ⒀ ‎) 2480 PARENTHESIZED NUMBER THIRTEEN + +# (l4) (14) ⒁ + (‎ (14) ‎) 0028 0031 0034 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT FOUR, RIGHT PARENTHESIS +← (‎ (l4) ‎) 0028 006C 0034 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT FOUR, RIGHT PARENTHESIS +← (‎ ⒁ ‎) 2481 PARENTHESIZED NUMBER FOURTEEN + +# (l5) (15) ⒂ + (‎ (15) ‎) 0028 0031 0035 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT FIVE, RIGHT PARENTHESIS +← (‎ (l5) ‎) 0028 006C 0035 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT FIVE, RIGHT PARENTHESIS +← (‎ ⒂ ‎) 2482 PARENTHESIZED NUMBER FIFTEEN + +# (l6) (16) ⒃ + (‎ (16) ‎) 0028 0031 0036 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT SIX, RIGHT PARENTHESIS +← (‎ (l6) ‎) 0028 006C 0036 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT SIX, RIGHT PARENTHESIS +← (‎ ⒃ ‎) 2483 PARENTHESIZED NUMBER SIXTEEN + +# (l7) (17) ⒄ + (‎ (17) ‎) 0028 0031 0037 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT SEVEN, RIGHT PARENTHESIS +← (‎ (l7) ‎) 0028 006C 0037 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT SEVEN, RIGHT PARENTHESIS +← (‎ ⒄ ‎) 2484 PARENTHESIZED NUMBER SEVENTEEN + +# (l8) (18) ⒅ + (‎ (18) ‎) 0028 0031 0038 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT EIGHT, RIGHT PARENTHESIS +← (‎ (l8) ‎) 0028 006C 0038 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT EIGHT, RIGHT PARENTHESIS +← (‎ ⒅ ‎) 2485 PARENTHESIZED NUMBER EIGHTEEN + +# (l9) (19) ⒆ + (‎ (19) ‎) 0028 0031 0039 0029 LEFT PARENTHESIS, DIGIT ONE, DIGIT NINE, RIGHT PARENTHESIS +← (‎ (l9) ‎) 0028 006C 0039 0029 LEFT PARENTHESIS, LATIN SMALL LETTER L, DIGIT NINE, RIGHT PARENTHESIS +← (‎ ⒆ ‎) 2486 PARENTHESIZED NUMBER NINETEEN + +# (2) ⑵ + (‎ (2) ‎) 0028 0032 0029 LEFT PARENTHESIS, DIGIT TWO, RIGHT PARENTHESIS +← (‎ ⑵ ‎) 2475 PARENTHESIZED DIGIT TWO + +# (2O) (20) ⒇ + (‎ (20) ‎) 0028 0032 0030 0029 LEFT PARENTHESIS, DIGIT TWO, DIGIT ZERO, RIGHT PARENTHESIS +← (‎ (2O) ‎) 0028 0032 004F 0029 LEFT PARENTHESIS, DIGIT TWO, LATIN CAPITAL LETTER O, RIGHT PARENTHESIS +← (‎ ⒇ ‎) 2487 PARENTHESIZED NUMBER TWENTY + +# (3) ⑶ + (‎ (3) ‎) 0028 0033 0029 LEFT PARENTHESIS, DIGIT THREE, RIGHT PARENTHESIS +← (‎ ⑶ ‎) 2476 PARENTHESIZED DIGIT THREE + +# (4) ⑷ + (‎ (4) ‎) 0028 0034 0029 LEFT PARENTHESIS, DIGIT FOUR, RIGHT PARENTHESIS +← (‎ ⑷ ‎) 2477 PARENTHESIZED DIGIT FOUR + +# (5) ⑸ + (‎ (5) ‎) 0028 0035 0029 LEFT PARENTHESIS, DIGIT FIVE, RIGHT PARENTHESIS +← (‎ ⑸ ‎) 2478 PARENTHESIZED DIGIT FIVE + +# (6) ⑹ + (‎ (6) ‎) 0028 0036 0029 LEFT PARENTHESIS, DIGIT SIX, RIGHT PARENTHESIS +← (‎ ⑹ ‎) 2479 PARENTHESIZED DIGIT SIX + +# (7) ⑺ + (‎ (7) ‎) 0028 0037 0029 LEFT PARENTHESIS, DIGIT SEVEN, RIGHT PARENTHESIS +← (‎ ⑺ ‎) 247A PARENTHESIZED DIGIT SEVEN + +# (8) ⑻ + (‎ (8) ‎) 0028 0038 0029 LEFT PARENTHESIS, DIGIT EIGHT, RIGHT PARENTHESIS +← (‎ ⑻ ‎) 247B PARENTHESIZED DIGIT EIGHT + +# (9) ⑼ + (‎ (9) ‎) 0028 0039 0029 LEFT PARENTHESIS, DIGIT NINE, RIGHT PARENTHESIS +← (‎ ⑼ ‎) 247C PARENTHESIZED DIGIT NINE + +# (A) 🄐 + (‎ (A) ‎) 0028 0041 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER A, RIGHT PARENTHESIS +← (‎ 🄐 ‎) 1F110 PARENTHESIZED LATIN CAPITAL LETTER A + +# (B) 🄑 + (‎ (B) ‎) 0028 0042 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER B, RIGHT PARENTHESIS +← (‎ 🄑 ‎) 1F111 PARENTHESIZED LATIN CAPITAL LETTER B + +# (C) 🄒 + (‎ (C) ‎) 0028 0043 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER C, RIGHT PARENTHESIS +← (‎ 🄒 ‎) 1F112 PARENTHESIZED LATIN CAPITAL LETTER C + +# (D) 🄓 + (‎ (D) ‎) 0028 0044 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER D, RIGHT PARENTHESIS +← (‎ 🄓 ‎) 1F113 PARENTHESIZED LATIN CAPITAL LETTER D + +# (E) 🄔 + (‎ (E) ‎) 0028 0045 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER E, RIGHT PARENTHESIS +← (‎ 🄔 ‎) 1F114 PARENTHESIZED LATIN CAPITAL LETTER E + +# (F) 🄕 + (‎ (F) ‎) 0028 0046 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER F, RIGHT PARENTHESIS +← (‎ 🄕 ‎) 1F115 PARENTHESIZED LATIN CAPITAL LETTER F + +# (G) 🄖 + (‎ (G) ‎) 0028 0047 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER G, RIGHT PARENTHESIS +← (‎ 🄖 ‎) 1F116 PARENTHESIZED LATIN CAPITAL LETTER G + +# (H) 🄗 + (‎ (H) ‎) 0028 0048 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER H, RIGHT PARENTHESIS +← (‎ 🄗 ‎) 1F117 PARENTHESIZED LATIN CAPITAL LETTER H + +# (J) 🄙 + (‎ (J) ‎) 0028 004A 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER J, RIGHT PARENTHESIS +← (‎ 🄙 ‎) 1F119 PARENTHESIZED LATIN CAPITAL LETTER J + +# (K) 🄚 + (‎ (K) ‎) 0028 004B 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER K, RIGHT PARENTHESIS +← (‎ 🄚 ‎) 1F11A PARENTHESIZED LATIN CAPITAL LETTER K + +# (L) 🄛 + (‎ (L) ‎) 0028 004C 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER L, RIGHT PARENTHESIS +← (‎ 🄛 ‎) 1F11B PARENTHESIZED LATIN CAPITAL LETTER L + +# (M) 🄜 + (‎ (M) ‎) 0028 004D 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER M, RIGHT PARENTHESIS +← (‎ 🄜 ‎) 1F11C PARENTHESIZED LATIN CAPITAL LETTER M + +# (N) 🄝 + (‎ (N) ‎) 0028 004E 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER N, RIGHT PARENTHESIS +← (‎ 🄝 ‎) 1F11D PARENTHESIZED LATIN CAPITAL LETTER N + +# (O) 🄞 + (‎ (O) ‎) 0028 004F 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER O, RIGHT PARENTHESIS +← (‎ 🄞 ‎) 1F11E PARENTHESIZED LATIN CAPITAL LETTER O + +# (P) 🄟 + (‎ (P) ‎) 0028 0050 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER P, RIGHT PARENTHESIS +← (‎ 🄟 ‎) 1F11F PARENTHESIZED LATIN CAPITAL LETTER P + +# (Q) 🄠 + (‎ (Q) ‎) 0028 0051 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER Q, RIGHT PARENTHESIS +← (‎ 🄠 ‎) 1F120 PARENTHESIZED LATIN CAPITAL LETTER Q + +# (R) 🄡 + (‎ (R) ‎) 0028 0052 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER R, RIGHT PARENTHESIS +← (‎ 🄡 ‎) 1F121 PARENTHESIZED LATIN CAPITAL LETTER R + +# (S) 〔S〕 🄢 🄪 + (‎ (S) ‎) 0028 0053 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER S, RIGHT PARENTHESIS +← (‎ 〔S〕 ‎) 3014 0053 3015 LEFT TORTOISE SHELL BRACKET, LATIN CAPITAL LETTER S, RIGHT TORTOISE SHELL BRACKET +← (‎ 🄢 ‎) 1F122 PARENTHESIZED LATIN CAPITAL LETTER S +← (‎ 🄪 ‎) 1F12A TORTOISE SHELL BRACKETED LATIN CAPITAL LETTER S # →〔S〕→ + +# (T) 🄣 + (‎ (T) ‎) 0028 0054 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER T, RIGHT PARENTHESIS +← (‎ 🄣 ‎) 1F123 PARENTHESIZED LATIN CAPITAL LETTER T + +# (U) 🄤 + (‎ (U) ‎) 0028 0055 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER U, RIGHT PARENTHESIS +← (‎ 🄤 ‎) 1F124 PARENTHESIZED LATIN CAPITAL LETTER U + +# (V) 🄥 + (‎ (V) ‎) 0028 0056 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER V, RIGHT PARENTHESIS +← (‎ 🄥 ‎) 1F125 PARENTHESIZED LATIN CAPITAL LETTER V + +# (W) 🄦 + (‎ (W) ‎) 0028 0057 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER W, RIGHT PARENTHESIS +← (‎ 🄦 ‎) 1F126 PARENTHESIZED LATIN CAPITAL LETTER W + +# (X) 🄧 + (‎ (X) ‎) 0028 0058 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER X, RIGHT PARENTHESIS +← (‎ 🄧 ‎) 1F127 PARENTHESIZED LATIN CAPITAL LETTER X + +# (Y) 🄨 + (‎ (Y) ‎) 0028 0059 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER Y, RIGHT PARENTHESIS +← (‎ 🄨 ‎) 1F128 PARENTHESIZED LATIN CAPITAL LETTER Y + +# (Z) 🄩 + (‎ (Z) ‎) 0028 005A 0029 LEFT PARENTHESIS, LATIN CAPITAL LETTER Z, RIGHT PARENTHESIS +← (‎ 🄩 ‎) 1F129 PARENTHESIZED LATIN CAPITAL LETTER Z + +# (a) ⒜ + (‎ (a) ‎) 0028 0061 0029 LEFT PARENTHESIS, LATIN SMALL LETTER A, RIGHT PARENTHESIS +← (‎ ⒜ ‎) 249C PARENTHESIZED LATIN SMALL LETTER A + +# (b) ⒝ + (‎ (b) ‎) 0028 0062 0029 LEFT PARENTHESIS, LATIN SMALL LETTER B, RIGHT PARENTHESIS +← (‎ ⒝ ‎) 249D PARENTHESIZED LATIN SMALL LETTER B + +# (c) ⒞ + (‎ (c) ‎) 0028 0063 0029 LEFT PARENTHESIS, LATIN SMALL LETTER C, RIGHT PARENTHESIS +← (‎ ⒞ ‎) 249E PARENTHESIZED LATIN SMALL LETTER C + +# (d) ⒟ + (‎ (d) ‎) 0028 0064 0029 LEFT PARENTHESIS, LATIN SMALL LETTER D, RIGHT PARENTHESIS +← (‎ ⒟ ‎) 249F PARENTHESIZED LATIN SMALL LETTER D + +# (e) ⒠ + (‎ (e) ‎) 0028 0065 0029 LEFT PARENTHESIS, LATIN SMALL LETTER E, RIGHT PARENTHESIS +← (‎ ⒠ ‎) 24A0 PARENTHESIZED LATIN SMALL LETTER E + +# (f) ⒡ + (‎ (f) ‎) 0028 0066 0029 LEFT PARENTHESIS, LATIN SMALL LETTER F, RIGHT PARENTHESIS +← (‎ ⒡ ‎) 24A1 PARENTHESIZED LATIN SMALL LETTER F + +# (g) ⒢ + (‎ (g) ‎) 0028 0067 0029 LEFT PARENTHESIS, LATIN SMALL LETTER G, RIGHT PARENTHESIS +← (‎ ⒢ ‎) 24A2 PARENTHESIZED LATIN SMALL LETTER G + +# (h) ⒣ + (‎ (h) ‎) 0028 0068 0029 LEFT PARENTHESIS, LATIN SMALL LETTER H, RIGHT PARENTHESIS +← (‎ ⒣ ‎) 24A3 PARENTHESIZED LATIN SMALL LETTER H + +# (i) ⒤ + (‎ (i) ‎) 0028 0069 0029 LEFT PARENTHESIS, LATIN SMALL LETTER I, RIGHT PARENTHESIS +← (‎ ⒤ ‎) 24A4 PARENTHESIZED LATIN SMALL LETTER I + +# (j) ⒥ + (‎ (j) ‎) 0028 006A 0029 LEFT PARENTHESIS, LATIN SMALL LETTER J, RIGHT PARENTHESIS +← (‎ ⒥ ‎) 24A5 PARENTHESIZED LATIN SMALL LETTER J + +# (k) ⒦ + (‎ (k) ‎) 0028 006B 0029 LEFT PARENTHESIS, LATIN SMALL LETTER K, RIGHT PARENTHESIS +← (‎ ⒦ ‎) 24A6 PARENTHESIZED LATIN SMALL LETTER K + +# (rn) (m) ⒨ + (‎ (m) ‎) 0028 006D 0029 LEFT PARENTHESIS, LATIN SMALL LETTER M, RIGHT PARENTHESIS +← (‎ (rn) ‎) 0028 0072 006E 0029 LEFT PARENTHESIS, LATIN SMALL LETTER R, LATIN SMALL LETTER N, RIGHT PARENTHESIS +← (‎ ⒨ ‎) 24A8 PARENTHESIZED LATIN SMALL LETTER M + +# (n) ⒩ + (‎ (n) ‎) 0028 006E 0029 LEFT PARENTHESIS, LATIN SMALL LETTER N, RIGHT PARENTHESIS +← (‎ ⒩ ‎) 24A9 PARENTHESIZED LATIN SMALL LETTER N + +# (o) ⒪ + (‎ (o) ‎) 0028 006F 0029 LEFT PARENTHESIS, LATIN SMALL LETTER O, RIGHT PARENTHESIS +← (‎ ⒪ ‎) 24AA PARENTHESIZED LATIN SMALL LETTER O + +# (p) ⒫ + (‎ (p) ‎) 0028 0070 0029 LEFT PARENTHESIS, LATIN SMALL LETTER P, RIGHT PARENTHESIS +← (‎ ⒫ ‎) 24AB PARENTHESIZED LATIN SMALL LETTER P + +# (q) ⒬ + (‎ (q) ‎) 0028 0071 0029 LEFT PARENTHESIS, LATIN SMALL LETTER Q, RIGHT PARENTHESIS +← (‎ ⒬ ‎) 24AC PARENTHESIZED LATIN SMALL LETTER Q + +# (r) ⒭ + (‎ (r) ‎) 0028 0072 0029 LEFT PARENTHESIS, LATIN SMALL LETTER R, RIGHT PARENTHESIS +← (‎ ⒭ ‎) 24AD PARENTHESIZED LATIN SMALL LETTER R + +# (s) ⒮ + (‎ (s) ‎) 0028 0073 0029 LEFT PARENTHESIS, LATIN SMALL LETTER S, RIGHT PARENTHESIS +← (‎ ⒮ ‎) 24AE PARENTHESIZED LATIN SMALL LETTER S + +# (t) ⒯ + (‎ (t) ‎) 0028 0074 0029 LEFT PARENTHESIS, LATIN SMALL LETTER T, RIGHT PARENTHESIS +← (‎ ⒯ ‎) 24AF PARENTHESIZED LATIN SMALL LETTER T + +# (u) ⒰ + (‎ (u) ‎) 0028 0075 0029 LEFT PARENTHESIS, LATIN SMALL LETTER U, RIGHT PARENTHESIS +← (‎ ⒰ ‎) 24B0 PARENTHESIZED LATIN SMALL LETTER U + +# (v) ⒱ + (‎ (v) ‎) 0028 0076 0029 LEFT PARENTHESIS, LATIN SMALL LETTER V, RIGHT PARENTHESIS +← (‎ ⒱ ‎) 24B1 PARENTHESIZED LATIN SMALL LETTER V + +# (w) ⒲ + (‎ (w) ‎) 0028 0077 0029 LEFT PARENTHESIS, LATIN SMALL LETTER W, RIGHT PARENTHESIS +← (‎ ⒲ ‎) 24B2 PARENTHESIZED LATIN SMALL LETTER W + +# (x) ⒳ + (‎ (x) ‎) 0028 0078 0029 LEFT PARENTHESIS, LATIN SMALL LETTER X, RIGHT PARENTHESIS +← (‎ ⒳ ‎) 24B3 PARENTHESIZED LATIN SMALL LETTER X + +# (y) ⒴ + (‎ (y) ‎) 0028 0079 0029 LEFT PARENTHESIS, LATIN SMALL LETTER Y, RIGHT PARENTHESIS +← (‎ ⒴ ‎) 24B4 PARENTHESIZED LATIN SMALL LETTER Y + +# (z) ⒵ + (‎ (z) ‎) 0028 007A 0029 LEFT PARENTHESIS, LATIN SMALL LETTER Z, RIGHT PARENTHESIS +← (‎ ⒵ ‎) 24B5 PARENTHESIZED LATIN SMALL LETTER Z + +# (ᄀ) ㈀ + (‎ (ᄀ) ‎) 0028 1100 0029 LEFT PARENTHESIS, HANGUL CHOSEONG KIYEOK, RIGHT PARENTHESIS +← (‎ ㈀ ‎) 3200 PARENTHESIZED HANGUL KIYEOK + +# (ᄂ) ㈁ + (‎ (ᄂ) ‎) 0028 1102 0029 LEFT PARENTHESIS, HANGUL CHOSEONG NIEUN, RIGHT PARENTHESIS +← (‎ ㈁ ‎) 3201 PARENTHESIZED HANGUL NIEUN + +# (ᄃ) ㈂ + (‎ (ᄃ) ‎) 0028 1103 0029 LEFT PARENTHESIS, HANGUL CHOSEONG TIKEUT, RIGHT PARENTHESIS +← (‎ ㈂ ‎) 3202 PARENTHESIZED HANGUL TIKEUT + +# (ᄅ) ㈃ + (‎ (ᄅ) ‎) 0028 1105 0029 LEFT PARENTHESIS, HANGUL CHOSEONG RIEUL, RIGHT PARENTHESIS +← (‎ ㈃ ‎) 3203 PARENTHESIZED HANGUL RIEUL + +# (ᄆ) ㈄ + (‎ (ᄆ) ‎) 0028 1106 0029 LEFT PARENTHESIS, HANGUL CHOSEONG MIEUM, RIGHT PARENTHESIS +← (‎ ㈄ ‎) 3204 PARENTHESIZED HANGUL MIEUM + +# (ᄇ) ㈅ + (‎ (ᄇ) ‎) 0028 1107 0029 LEFT PARENTHESIS, HANGUL CHOSEONG PIEUP, RIGHT PARENTHESIS +← (‎ ㈅ ‎) 3205 PARENTHESIZED HANGUL PIEUP + +# (ᄉ) ㈆ + (‎ (ᄉ) ‎) 0028 1109 0029 LEFT PARENTHESIS, HANGUL CHOSEONG SIOS, RIGHT PARENTHESIS +← (‎ ㈆ ‎) 3206 PARENTHESIZED HANGUL SIOS + +# (ᄋ) ㈇ + (‎ (ᄋ) ‎) 0028 110B 0029 LEFT PARENTHESIS, HANGUL CHOSEONG IEUNG, RIGHT PARENTHESIS +← (‎ ㈇ ‎) 3207 PARENTHESIZED HANGUL IEUNG + +# (ᄌ) ㈈ + (‎ (ᄌ) ‎) 0028 110C 0029 LEFT PARENTHESIS, HANGUL CHOSEONG CIEUC, RIGHT PARENTHESIS +← (‎ ㈈ ‎) 3208 PARENTHESIZED HANGUL CIEUC + +# (ᄎ) ㈉ + (‎ (ᄎ) ‎) 0028 110E 0029 LEFT PARENTHESIS, HANGUL CHOSEONG CHIEUCH, RIGHT PARENTHESIS +← (‎ ㈉ ‎) 3209 PARENTHESIZED HANGUL CHIEUCH + +# (ᄏ) ㈊ + (‎ (ᄏ) ‎) 0028 110F 0029 LEFT PARENTHESIS, HANGUL CHOSEONG KHIEUKH, RIGHT PARENTHESIS +← (‎ ㈊ ‎) 320A PARENTHESIZED HANGUL KHIEUKH + +# (ᄐ) ㈋ + (‎ (ᄐ) ‎) 0028 1110 0029 LEFT PARENTHESIS, HANGUL CHOSEONG THIEUTH, RIGHT PARENTHESIS +← (‎ ㈋ ‎) 320B PARENTHESIZED HANGUL THIEUTH + +# (ᄑ) ㈌ + (‎ (ᄑ) ‎) 0028 1111 0029 LEFT PARENTHESIS, HANGUL CHOSEONG PHIEUPH, RIGHT PARENTHESIS +← (‎ ㈌ ‎) 320C PARENTHESIZED HANGUL PHIEUPH + +# (ᄒ) ㈍ + (‎ (ᄒ) ‎) 0028 1112 0029 LEFT PARENTHESIS, HANGUL CHOSEONG HIEUH, RIGHT PARENTHESIS +← (‎ ㈍ ‎) 320D PARENTHESIZED HANGUL HIEUH + +# (ー) (一) ㈠ + (‎ (ー) ‎) 0028 30FC 0029 LEFT PARENTHESIS, KATAKANA-HIRAGANA PROLONGED SOUND MARK, RIGHT PARENTHESIS +← (‎ (一) ‎) 0028 4E00 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4E00, RIGHT PARENTHESIS +← (‎ ㈠ ‎) 3220 PARENTHESIZED IDEOGRAPH ONE # →(一)→ + +# (七) ㈦ + (‎ (七) ‎) 0028 4E03 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4E03, RIGHT PARENTHESIS +← (‎ ㈦ ‎) 3226 PARENTHESIZED IDEOGRAPH SEVEN + +# (三) 〔三〕 ㈢ 🉁 + (‎ (三) ‎) 0028 4E09 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4E09, RIGHT PARENTHESIS +← (‎ 〔三〕 ‎) 3014 4E09 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-4E09, RIGHT TORTOISE SHELL BRACKET +← (‎ ㈢ ‎) 3222 PARENTHESIZED IDEOGRAPH THREE +← (‎ 🉁 ‎) 1F241 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-4E09 # →〔三〕→ + +# (九) ㈨ + (‎ (九) ‎) 0028 4E5D 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4E5D, RIGHT PARENTHESIS +← (‎ ㈨ ‎) 3228 PARENTHESIZED IDEOGRAPH NINE + +# (二) 〔二〕 ㈡ 🉂 + (‎ (二) ‎) 0028 4E8C 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4E8C, RIGHT PARENTHESIS +← (‎ 〔二〕 ‎) 3014 4E8C 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-4E8C, RIGHT TORTOISE SHELL BRACKET +← (‎ ㈡ ‎) 3221 PARENTHESIZED IDEOGRAPH TWO +← (‎ 🉂 ‎) 1F242 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-4E8C # →〔二〕→ + +# (五) ㈤ + (‎ (五) ‎) 0028 4E94 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4E94, RIGHT PARENTHESIS +← (‎ ㈤ ‎) 3224 PARENTHESIZED IDEOGRAPH FIVE + +# (代) ㈹ + (‎ (代) ‎) 0028 4EE3 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4EE3, RIGHT PARENTHESIS +← (‎ ㈹ ‎) 3239 PARENTHESIZED IDEOGRAPH REPRESENT + +# (企) ㈽ + (‎ (企) ‎) 0028 4F01 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4F01, RIGHT PARENTHESIS +← (‎ ㈽ ‎) 323D PARENTHESIZED IDEOGRAPH ENTERPRISE + +# (休) ㉁ + (‎ (休) ‎) 0028 4F11 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-4F11, RIGHT PARENTHESIS +← (‎ ㉁ ‎) 3241 PARENTHESIZED IDEOGRAPH REST + +# (八) ㈧ + (‎ (八) ‎) 0028 516B 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-516B, RIGHT PARENTHESIS +← (‎ ㈧ ‎) 3227 PARENTHESIZED IDEOGRAPH EIGHT + +# (六) ㈥ + (‎ (六) ‎) 0028 516D 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-516D, RIGHT PARENTHESIS +← (‎ ㈥ ‎) 3225 PARENTHESIZED IDEOGRAPH SIX + +# (労) ㈸ + (‎ (労) ‎) 0028 52B4 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-52B4, RIGHT PARENTHESIS +← (‎ ㈸ ‎) 3238 PARENTHESIZED IDEOGRAPH LABOR + +# (勝) 〔勝〕 🉇 + (‎ (勝) ‎) 0028 52DD 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-52DD, RIGHT PARENTHESIS +← (‎ 〔勝〕 ‎) 3014 52DD 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-52DD, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉇 ‎) 1F247 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-52DD # →〔勝〕→ + +# (十) ㈩ + (‎ (十) ‎) 0028 5341 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-5341, RIGHT PARENTHESIS +← (‎ ㈩ ‎) 3229 PARENTHESIZED IDEOGRAPH TEN + +# (協) ㈿ + (‎ (協) ‎) 0028 5354 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-5354, RIGHT PARENTHESIS +← (‎ ㈿ ‎) 323F PARENTHESIZED IDEOGRAPH ALLIANCE + +# (名) ㈴ + (‎ (名) ‎) 0028 540D 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-540D, RIGHT PARENTHESIS +← (‎ ㈴ ‎) 3234 PARENTHESIZED IDEOGRAPH NAME + +# (呼) ㈺ + (‎ (呼) ‎) 0028 547C 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-547C, RIGHT PARENTHESIS +← (‎ ㈺ ‎) 323A PARENTHESIZED IDEOGRAPH CALL + +# (四) ㈣ + (‎ (四) ‎) 0028 56DB 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-56DB, RIGHT PARENTHESIS +← (‎ ㈣ ‎) 3223 PARENTHESIZED IDEOGRAPH FOUR + +# (土) ㈯ + (‎ (土) ‎) 0028 571F 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-571F, RIGHT PARENTHESIS +← (‎ ㈯ ‎) 322F PARENTHESIZED IDEOGRAPH EARTH + +# (学) ㈻ + (‎ (学) ‎) 0028 5B66 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-5B66, RIGHT PARENTHESIS +← (‎ ㈻ ‎) 323B PARENTHESIZED IDEOGRAPH STUDY + +# (安) 〔安〕 🉃 + (‎ (安) ‎) 0028 5B89 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-5B89, RIGHT PARENTHESIS +← (‎ 〔安〕 ‎) 3014 5B89 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-5B89, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉃 ‎) 1F243 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-5B89 # →〔安〕→ + +# (打) 〔打〕 🉅 + (‎ (打) ‎) 0028 6253 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-6253, RIGHT PARENTHESIS +← (‎ 〔打〕 ‎) 3014 6253 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-6253, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉅 ‎) 1F245 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6253 # →〔打〕→ + +# (敗) 〔敗〕 🉈 + (‎ (敗) ‎) 0028 6557 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-6557, RIGHT PARENTHESIS +← (‎ 〔敗〕 ‎) 3014 6557 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-6557, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉈 ‎) 1F248 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6557 # →〔敗〕→ + +# (日) ㈰ + (‎ (日) ‎) 0028 65E5 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-65E5, RIGHT PARENTHESIS +← (‎ ㈰ ‎) 3230 PARENTHESIZED IDEOGRAPH SUN + +# (月) ㈪ + (‎ (月) ‎) 0028 6708 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-6708, RIGHT PARENTHESIS +← (‎ ㈪ ‎) 322A PARENTHESIZED IDEOGRAPH MOON + +# (有) ㈲ + (‎ (有) ‎) 0028 6709 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-6709, RIGHT PARENTHESIS +← (‎ ㈲ ‎) 3232 PARENTHESIZED IDEOGRAPH HAVE + +# (木) ㈭ + (‎ (木) ‎) 0028 6728 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-6728, RIGHT PARENTHESIS +← (‎ ㈭ ‎) 322D PARENTHESIZED IDEOGRAPH WOOD + +# (本) 〔本〕 🉀 + (‎ (本) ‎) 0028 672C 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-672C, RIGHT PARENTHESIS +← (‎ 〔本〕 ‎) 3014 672C 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-672C, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉀 ‎) 1F240 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-672C # →〔本〕→ + +# (株) ㈱ + (‎ (株) ‎) 0028 682A 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-682A, RIGHT PARENTHESIS +← (‎ ㈱ ‎) 3231 PARENTHESIZED IDEOGRAPH STOCK + +# (水) ㈬ + (‎ (水) ‎) 0028 6C34 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-6C34, RIGHT PARENTHESIS +← (‎ ㈬ ‎) 322C PARENTHESIZED IDEOGRAPH WATER + +# (火) ㈫ + (‎ (火) ‎) 0028 706B 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-706B, RIGHT PARENTHESIS +← (‎ ㈫ ‎) 322B PARENTHESIZED IDEOGRAPH FIRE + +# (点) 〔点〕 🉄 + (‎ (点) ‎) 0028 70B9 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-70B9, RIGHT PARENTHESIS +← (‎ 〔点〕 ‎) 3014 70B9 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-70B9, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉄 ‎) 1F244 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-70B9 # →〔点〕→ + +# (特) ㈵ + (‎ (特) ‎) 0028 7279 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-7279, RIGHT PARENTHESIS +← (‎ ㈵ ‎) 3235 PARENTHESIZED IDEOGRAPH SPECIAL + +# (盗) 〔盗〕 🉆 + (‎ (盗) ‎) 0028 76D7 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-76D7, RIGHT PARENTHESIS +← (‎ 〔盗〕 ‎) 3014 76D7 3015 LEFT TORTOISE SHELL BRACKET, CJK UNIFIED IDEOGRAPH-76D7, RIGHT TORTOISE SHELL BRACKET +← (‎ 🉆 ‎) 1F246 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-76D7 # →〔盗〕→ + +# (監) ㈼ + (‎ (監) ‎) 0028 76E3 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-76E3, RIGHT PARENTHESIS +← (‎ ㈼ ‎) 323C PARENTHESIZED IDEOGRAPH SUPERVISE + +# (社) ㈳ + (‎ (社) ‎) 0028 793E 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-793E, RIGHT PARENTHESIS +← (‎ ㈳ ‎) 3233 PARENTHESIZED IDEOGRAPH SOCIETY + +# (祝) ㈷ + (‎ (祝) ‎) 0028 795D 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-795D, RIGHT PARENTHESIS +← (‎ ㈷ ‎) 3237 PARENTHESIZED IDEOGRAPH CONGRATULATION + +# (祭) ㉀ + (‎ (祭) ‎) 0028 796D 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-796D, RIGHT PARENTHESIS +← (‎ ㉀ ‎) 3240 PARENTHESIZED IDEOGRAPH FESTIVAL + +# (自) ㉂ + (‎ (自) ‎) 0028 81EA 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-81EA, RIGHT PARENTHESIS +← (‎ ㉂ ‎) 3242 PARENTHESIZED IDEOGRAPH SELF + +# (至) ㉃ + (‎ (至) ‎) 0028 81F3 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-81F3, RIGHT PARENTHESIS +← (‎ ㉃ ‎) 3243 PARENTHESIZED IDEOGRAPH REACH + +# (財) ㈶ + (‎ (財) ‎) 0028 8CA1 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-8CA1, RIGHT PARENTHESIS +← (‎ ㈶ ‎) 3236 PARENTHESIZED IDEOGRAPH FINANCIAL + +# (資) ㈾ + (‎ (資) ‎) 0028 8CC7 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-8CC7, RIGHT PARENTHESIS +← (‎ ㈾ ‎) 323E PARENTHESIZED IDEOGRAPH RESOURCE + +# (金) ㈮ + (‎ (金) ‎) 0028 91D1 0029 LEFT PARENTHESIS, CJK UNIFIED IDEOGRAPH-91D1, RIGHT PARENTHESIS +← (‎ ㈮ ‎) 322E PARENTHESIZED IDEOGRAPH METAL + +# (가) ㈎ + (‎ (가) ‎) 0028 AC00 0029 LEFT PARENTHESIS, HANGUL SYLLABLE GA, RIGHT PARENTHESIS +← (‎ ㈎ ‎) 320E PARENTHESIZED HANGUL KIYEOK A + +# (나) ㈏ + (‎ (나) ‎) 0028 B098 0029 LEFT PARENTHESIS, HANGUL SYLLABLE NA, RIGHT PARENTHESIS +← (‎ ㈏ ‎) 320F PARENTHESIZED HANGUL NIEUN A + +# (다) ㈐ + (‎ (다) ‎) 0028 B2E4 0029 LEFT PARENTHESIS, HANGUL SYLLABLE DA, RIGHT PARENTHESIS +← (‎ ㈐ ‎) 3210 PARENTHESIZED HANGUL TIKEUT A + +# (라) ㈑ + (‎ (라) ‎) 0028 B77C 0029 LEFT PARENTHESIS, HANGUL SYLLABLE RA, RIGHT PARENTHESIS +← (‎ ㈑ ‎) 3211 PARENTHESIZED HANGUL RIEUL A + +# (마) ㈒ + (‎ (마) ‎) 0028 B9C8 0029 LEFT PARENTHESIS, HANGUL SYLLABLE MA, RIGHT PARENTHESIS +← (‎ ㈒ ‎) 3212 PARENTHESIZED HANGUL MIEUM A + +# (바) ㈓ + (‎ (바) ‎) 0028 BC14 0029 LEFT PARENTHESIS, HANGUL SYLLABLE BA, RIGHT PARENTHESIS +← (‎ ㈓ ‎) 3213 PARENTHESIZED HANGUL PIEUP A + +# (사) ㈔ + (‎ (사) ‎) 0028 C0AC 0029 LEFT PARENTHESIS, HANGUL SYLLABLE SA, RIGHT PARENTHESIS +← (‎ ㈔ ‎) 3214 PARENTHESIZED HANGUL SIOS A + +# (아) ㈕ + (‎ (아) ‎) 0028 C544 0029 LEFT PARENTHESIS, HANGUL SYLLABLE A, RIGHT PARENTHESIS +← (‎ ㈕ ‎) 3215 PARENTHESIZED HANGUL IEUNG A + +# (오전) ㈝ + (‎ (오전) ‎) 0028 C624 C804 0029 LEFT PARENTHESIS, HANGUL SYLLABLE O, HANGUL SYLLABLE JEON, RIGHT PARENTHESIS +← (‎ ㈝ ‎) 321D PARENTHESIZED KOREAN CHARACTER OJEON + +# (오후) ㈞ + (‎ (오후) ‎) 0028 C624 D6C4 0029 LEFT PARENTHESIS, HANGUL SYLLABLE O, HANGUL SYLLABLE HU, RIGHT PARENTHESIS +← (‎ ㈞ ‎) 321E PARENTHESIZED KOREAN CHARACTER O HU + +# (자) ㈖ + (‎ (자) ‎) 0028 C790 0029 LEFT PARENTHESIS, HANGUL SYLLABLE JA, RIGHT PARENTHESIS +← (‎ ㈖ ‎) 3216 PARENTHESIZED HANGUL CIEUC A + +# (주) ㈜ + (‎ (주) ‎) 0028 C8FC 0029 LEFT PARENTHESIS, HANGUL SYLLABLE JU, RIGHT PARENTHESIS +← (‎ ㈜ ‎) 321C PARENTHESIZED HANGUL CIEUC U + +# (차) ㈗ + (‎ (차) ‎) 0028 CC28 0029 LEFT PARENTHESIS, HANGUL SYLLABLE CA, RIGHT PARENTHESIS +← (‎ ㈗ ‎) 3217 PARENTHESIZED HANGUL CHIEUCH A + +# (카) ㈘ + (‎ (카) ‎) 0028 CE74 0029 LEFT PARENTHESIS, HANGUL SYLLABLE KA, RIGHT PARENTHESIS +← (‎ ㈘ ‎) 3218 PARENTHESIZED HANGUL KHIEUKH A + +# (타) ㈙ + (‎ (타) ‎) 0028 D0C0 0029 LEFT PARENTHESIS, HANGUL SYLLABLE TA, RIGHT PARENTHESIS +← (‎ ㈙ ‎) 3219 PARENTHESIZED HANGUL THIEUTH A + +# (파) ㈚ + (‎ (파) ‎) 0028 D30C 0029 LEFT PARENTHESIS, HANGUL SYLLABLE PA, RIGHT PARENTHESIS +← (‎ ㈚ ‎) 321A PARENTHESIZED HANGUL PHIEUPH A + +# (하) ㈛ + (‎ (하) ‎) 0028 D558 0029 LEFT PARENTHESIS, HANGUL SYLLABLE HA, RIGHT PARENTHESIS +← (‎ ㈛ ‎) 321B PARENTHESIZED HANGUL HIEUH A + +# ) ❩ ❳ 〕 ﴿ ] + (‎ ) ‎) 0029 RIGHT PARENTHESIS +← (‎ ❩ ‎) 2769 MEDIUM RIGHT PARENTHESIS ORNAMENT +← (‎ ❳ ‎) 2773 LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT # →〕→ +← (‎ 〕 ‎) 3015 RIGHT TORTOISE SHELL BRACKET +← (‎ ﴿ ‎) FD3F ORNATE RIGHT PARENTHESIS +← (‎ ] ‎) FF3D FULLWIDTH RIGHT SQUARE BRACKET # →〕→ + +# )) ⸩ + (‎ )) ‎) 0029 0029 RIGHT PARENTHESIS, RIGHT PARENTHESIS +← (‎ ⸩ ‎) 2E29 RIGHT DOUBLE PARENTHESIS + +# * ٭ ⁎ ∗ 𐌟 + (‎ * ‎) 002A ASTERISK +← (‎ ٭ ‎) 066D ARABIC FIVE POINTED STAR +← (‎ ⁎ ‎) 204E LOW ASTERISK +← (‎ ∗ ‎) 2217 ASTERISK OPERATOR +← (‎ 𐌟 ‎) 1031F OLD ITALIC LETTER ESS + +# + 𐊛 ᛭ ➕ + (‎ + ‎) 002B PLUS SIGN +← (‎ 𐊛 ‎) 1029B LYCIAN LETTER H +← (‎ ᛭ ‎) 16ED RUNIC CROSS PUNCTUATION +← (‎ ➕ ‎) 2795 HEAVY PLUS SIGN + +# +̂ ⨣ + (‎ +̂ ‎) 002B 0302 PLUS SIGN, COMBINING CIRCUMFLEX ACCENT +← (‎ ⨣ ‎) 2A23 PLUS SIGN WITH CIRCUMFLEX ACCENT ABOVE + +# +̃ ⨤ + (‎ +̃ ‎) 002B 0303 PLUS SIGN, COMBINING TILDE +← (‎ ⨤ ‎) 2A24 PLUS SIGN WITH TILDE ABOVE + +# +̇ ∔ + (‎ +̇ ‎) 002B 0307 PLUS SIGN, COMBINING DOT ABOVE +← (‎ ∔ ‎) 2214 DOT PLUS + +# +̊ ⨢ + (‎ +̊ ‎) 002B 030A PLUS SIGN, COMBINING RING ABOVE +← (‎ ⨢ ‎) 2A22 PLUS SIGN WITH SMALL CIRCLE ABOVE + +# +̣ ⨥ + (‎ +̣ ‎) 002B 0323 PLUS SIGN, COMBINING DOT BELOW +← (‎ ⨥ ‎) 2A25 PLUS SIGN WITH DOT BELOW + +# +̰ ⨦ + (‎ +̰ ‎) 002B 0330 PLUS SIGN, COMBINING TILDE BELOW +← (‎ ⨦ ‎) 2A26 PLUS SIGN WITH TILDE BELOW + +# +₂ ⨧ + (‎ +₂ ‎) 002B 2082 PLUS SIGN, SUBSCRIPT TWO +← (‎ ⨧ ‎) 2A27 PLUS SIGN WITH SUBSCRIPT TWO + +# , ꓹ ؍ ٫ ‚ ¸ + (‎ , ‎) 002C COMMA +← (‎ ꓹ ‎) A4F9 LISU LETTER TONE NA PO +← (‎ ؍ ‎) 060D ARABIC DATE SEPARATOR # →‎٫‎→ +← (‎ ٫ ‎) 066B ARABIC DECIMAL SEPARATOR +← (‎ ‚ ‎) 201A SINGLE LOW-9 QUOTATION MARK +← (‎ ¸ ‎) 00B8 CEDILLA + +# - Ⲻ ‐ ˗ ۔ ‒ – ⁃ − ➖ ‑ ﹘ + (‎ - ‎) 002D HYPHEN-MINUS +← (‎ Ⲻ ‎) 2CBA COPTIC CAPITAL LETTER DIALECT-P NI # →‒→ +← (‎ ‐ ‎) 2010 HYPHEN +← (‎ ˗ ‎) 02D7 MODIFIER LETTER MINUS SIGN +← (‎ ۔ ‎) 06D4 ARABIC FULL STOP # →‐→ +← (‎ ‒ ‎) 2012 FIGURE DASH +← (‎ – ‎) 2013 EN DASH +← (‎ ⁃ ‎) 2043 HYPHEN BULLET # →‐→ +← (‎ − ‎) 2212 MINUS SIGN +← (‎ ➖ ‎) 2796 HEAVY MINUS SIGN # →−→ +← (‎ ‑ ‎) 2011 NON-BREAKING HYPHEN +← (‎ ﹘ ‎) FE58 SMALL EM DASH + +# -. ꓾ + (‎ -. ‎) 002D 002E HYPHEN-MINUS, FULL STOP +← (‎ ꓾ ‎) A4FE LISU PUNCTUATION COMMA + +# -̇ −̇ ∸ ﬩ + (‎ -̇ ‎) 002D 0307 HYPHEN-MINUS, COMBINING DOT ABOVE +← (‎ −̇ ‎) 2212 0307 MINUS SIGN, COMBINING DOT ABOVE +← (‎ ∸ ‎) 2238 DOT MINUS # →−̇→ +← (‎ ﬩ ‎) FB29 HEBREW LETTER ALTERNATIVE PLUS SIGN # →∸→→−̇→ + +# -̈ ⸚ + (‎ -̈ ‎) 002D 0308 HYPHEN-MINUS, COMBINING DIAERESIS +← (‎ ⸚ ‎) 2E1A HYPHEN WITH DIAERESIS + +# -̓ −̓ ⨩ + (‎ -̓ ‎) 002D 0313 HYPHEN-MINUS, COMBINING COMMA ABOVE +← (‎ −̓ ‎) 2212 0313 MINUS SIGN, COMBINING COMMA ABOVE +← (‎ ⨩ ‎) 2A29 MINUS SIGN WITH COMMA ABOVE # →−̓→ + +# -̣ −̣ ⨪ + (‎ -̣ ‎) 002D 0323 HYPHEN-MINUS, COMBINING DOT BELOW +← (‎ −̣ ‎) 2212 0323 MINUS SIGN, COMBINING DOT BELOW +← (‎ ⨪ ‎) 2A2A MINUS SIGN WITH DOT BELOW # →−̣→ + +# . ٠ ۰ ꓸ 𝅭 ܁ ܂ ꘎ 𐩐 ․ + (‎ . ‎) 002E FULL STOP +← (‎ ٠ ‎) 0660 ARABIC-INDIC DIGIT ZERO +← (‎ ۰ ‎) 06F0 EXTENDED ARABIC-INDIC DIGIT ZERO # →‎٠‎→ +← (‎ ꓸ ‎) A4F8 LISU LETTER TONE MYA TI +← (‎ 𝅭 ‎) 1D16D MUSICAL SYMBOL COMBINING AUGMENTATION DOT +← (‎ ܁ ‎) 0701 SYRIAC SUPRALINEAR FULL STOP +← (‎ ܂ ‎) 0702 SYRIAC SUBLINEAR FULL STOP +← (‎ ꘎ ‎) A60E VAI FULL STOP +← (‎ 𐩐 ‎) 10A50 KHAROSHTHI PUNCTUATION DOT +← (‎ ․ ‎) 2024 ONE DOT LEADER + +# ., ꓻ + (‎ ., ‎) 002E 002C FULL STOP, COMMA +← (‎ ꓻ ‎) A4FB LISU LETTER TONE MYA BO + +# .. ꓺ ‥ + (‎ .. ‎) 002E 002E FULL STOP, FULL STOP +← (‎ ꓺ ‎) A4FA LISU LETTER TONE MYA CYA +← (‎ ‥ ‎) 2025 TWO DOT LEADER + +# ... … + (‎ ... ‎) 002E 002E 002E FULL STOP, FULL STOP, FULL STOP +← (‎ … ‎) 2026 HORIZONTAL ELLIPSIS + +# / ノ 丿 Ⳇ 〳 ᜵ ⁁ ⁄ ∕ ╱ ⟋ ⧸ ㇓ 𝈺 ⼃ + (‎ / ‎) 002F SOLIDUS +← (‎ ノ ‎) 30CE KATAKANA LETTER NO # →⼃→ +← (‎ 丿 ‎) 4E3F CJK UNIFIED IDEOGRAPH-4E3F # →⼃→ +← (‎ Ⳇ ‎) 2CC6 COPTIC CAPITAL LETTER OLD COPTIC ESH +← (‎ 〳 ‎) 3033 VERTICAL KANA REPEAT MARK UPPER HALF +← (‎ ᜵ ‎) 1735 PHILIPPINE SINGLE PUNCTUATION +← (‎ ⁁ ‎) 2041 CARET INSERTION POINT +← (‎ ⁄ ‎) 2044 FRACTION SLASH +← (‎ ∕ ‎) 2215 DIVISION SLASH +← (‎ ╱ ‎) 2571 BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT +← (‎ ⟋ ‎) 27CB MATHEMATICAL RISING DIAGONAL +← (‎ ⧸ ‎) 29F8 BIG SOLIDUS +← (‎ ㇓ ‎) 31D3 CJK STROKE SP # →⼃→ +← (‎ 𝈺 ‎) 1D23A GREEK INSTRUMENTAL NOTATION SYMBOL-47 +← (‎ ⼃ ‎) 2F03 KANGXI RADICAL SLASH + +# // ⫽ + (‎ // ‎) 002F 002F SOLIDUS, SOLIDUS +← (‎ ⫽ ‎) 2AFD DOUBLE SOLIDUS OPERATOR + +# /// ⫻ + (‎ /// ‎) 002F 002F 002F SOLIDUS, SOLIDUS, SOLIDUS +← (‎ ⫻ ‎) 2AFB TRIPLE SOLIDUS BINARY RELATION + +# /̄ ⧶ + (‎ /̄ ‎) 002F 0304 SOLIDUS, COMBINING MACRON +← (‎ ⧶ ‎) 29F6 SOLIDUS WITH OVERBAR + +# O 0 ০ ଠ ୦ ዐ 〇 Ο О Օ ߀ Ⲟ ⵔ ꓳ 𐊒 𐊫 𐐄 𐔖 𑓐 𑢵 𑣠 𐓂 🯰 O 𝐎 𝑂 𝑶 𝒪 𝓞 𝔒 𝕆 𝕺 𝖮 𝗢 𝘖 𝙊 𝙾 𝚶 𝛰 𝜪 𝝤 𝞞 𝟎 𝟘 𝟢 𝟬 𝟶 + (‎ 0 ‎) 0030 DIGIT ZERO +← (‎ O ‎) 004F LATIN CAPITAL LETTER O +← (‎ ০ ‎) 09E6 BENGALI DIGIT ZERO +← (‎ ଠ ‎) 0B20 ORIYA LETTER TTHA # →୦→ +← (‎ ୦ ‎) 0B66 ORIYA DIGIT ZERO +← (‎ ዐ ‎) 12D0 ETHIOPIC SYLLABLE PHARYNGEAL A # →Օ→→О→ +← (‎ 〇 ‎) 3007 IDEOGRAPHIC NUMBER ZERO # →O→ +← (‎ Ο ‎) 039F GREEK CAPITAL LETTER OMICRON +← (‎ О ‎) 041E CYRILLIC CAPITAL LETTER O +← (‎ Օ ‎) 0555 ARMENIAN CAPITAL LETTER OH # →О→ +← (‎ ߀ ‎) 07C0 NKO DIGIT ZERO +← (‎ Ⲟ ‎) 2C9E COPTIC CAPITAL LETTER O # →О→ +← (‎ ⵔ ‎) 2D54 TIFINAGH LETTER YAR # →О→ +← (‎ ꓳ ‎) A4F3 LISU LETTER O # →O→ +← (‎ 𐊒 ‎) 10292 LYCIAN LETTER U # →O→ +← (‎ 𐊫 ‎) 102AB CARIAN LETTER O # →O→ +← (‎ 𐐄 ‎) 10404 DESERET CAPITAL LETTER LONG O # →O→ +← (‎ 𐔖 ‎) 10516 ELBASAN LETTER O # →O→ +← (‎ 𑓐 ‎) 114D0 TIRHUTA DIGIT ZERO # →০→ +← (‎ 𑢵 ‎) 118B5 WARANG CITI CAPITAL LETTER AT # →O→ +← (‎ 𑣠 ‎) 118E0 WARANG CITI DIGIT ZERO +← (‎ 𐓂 ‎) 104C2 OSAGE CAPITAL LETTER O # →O→ +← (‎ 🯰 ‎) 1FBF0 SEGMENTED DIGIT ZERO +← (‎ O ‎) FF2F FULLWIDTH LATIN CAPITAL LETTER O # →О→ +← (‎ 𝐎 ‎) 1D40E MATHEMATICAL BOLD CAPITAL O # →O→ +← (‎ 𝑂 ‎) 1D442 MATHEMATICAL ITALIC CAPITAL O # →O→ +← (‎ 𝑶 ‎) 1D476 MATHEMATICAL BOLD ITALIC CAPITAL O # →O→ +← (‎ 𝒪 ‎) 1D4AA MATHEMATICAL SCRIPT CAPITAL O # →O→ +← (‎ 𝓞 ‎) 1D4DE MATHEMATICAL BOLD SCRIPT CAPITAL O # →O→ +← (‎ 𝔒 ‎) 1D512 MATHEMATICAL FRAKTUR CAPITAL O # →O→ +← (‎ 𝕆 ‎) 1D546 MATHEMATICAL DOUBLE-STRUCK CAPITAL O # →O→ +← (‎ 𝕺 ‎) 1D57A MATHEMATICAL BOLD FRAKTUR CAPITAL O # →O→ +← (‎ 𝖮 ‎) 1D5AE MATHEMATICAL SANS-SERIF CAPITAL O # →O→ +← (‎ 𝗢 ‎) 1D5E2 MATHEMATICAL SANS-SERIF BOLD CAPITAL O # →O→ +← (‎ 𝘖 ‎) 1D616 MATHEMATICAL SANS-SERIF ITALIC CAPITAL O # →O→ +← (‎ 𝙊 ‎) 1D64A MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL O # →O→ +← (‎ 𝙾 ‎) 1D67E MATHEMATICAL MONOSPACE CAPITAL O # →O→ +← (‎ 𝚶 ‎) 1D6B6 MATHEMATICAL BOLD CAPITAL OMICRON # →Ο→ +← (‎ 𝛰 ‎) 1D6F0 MATHEMATICAL ITALIC CAPITAL OMICRON # →Ο→ +← (‎ 𝜪 ‎) 1D72A MATHEMATICAL BOLD ITALIC CAPITAL OMICRON # →Ο→ +← (‎ 𝝤 ‎) 1D764 MATHEMATICAL SANS-SERIF BOLD CAPITAL OMICRON # →Ο→ +← (‎ 𝞞 ‎) 1D79E MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMICRON # →Ο→ +← (‎ 𝟎 ‎) 1D7CE MATHEMATICAL BOLD DIGIT ZERO +← (‎ 𝟘 ‎) 1D7D8 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO +← (‎ 𝟢 ‎) 1D7E2 MATHEMATICAL SANS-SERIF DIGIT ZERO +← (‎ 𝟬 ‎) 1D7EC MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO +← (‎ 𝟶 ‎) 1D7F6 MATHEMATICAL MONOSPACE DIGIT ZERO + +# O, 0, 🄁 + (‎ 0, ‎) 0030 002C DIGIT ZERO, COMMA +← (‎ O, ‎) 004F 002C LATIN CAPITAL LETTER O, COMMA +← (‎ 🄁 ‎) 1F101 DIGIT ZERO COMMA + +# O. 0. 🄀 + (‎ 0. ‎) 0030 002E DIGIT ZERO, FULL STOP +← (‎ O. ‎) 004F 002E LATIN CAPITAL LETTER O, FULL STOP +← (‎ 🄀 ‎) 1F100 DIGIT ZERO FULL STOP + +# O̵ O̶ 0̵ О̵ Ɵ Ꝋ θ Θ Ө Ѳ Ꮎ Ꮻ ⴱ ⊖ ⊝ ⍬ 𝈚 🜔 ϴ ϑ 𝚯 𝚹 𝛉 𝛝 𝛩 𝛳 𝜃 𝜗 𝜣 𝜭 𝜽 𝝑 𝝝 𝝧 𝝷 𝞋 𝞗 𝞡 𝞱 𝟅 + (‎ 0̵ ‎) 0030 0335 DIGIT ZERO, COMBINING SHORT STROKE OVERLAY +← (‎ O̵ ‎) 004F 0335 LATIN CAPITAL LETTER O, COMBINING SHORT STROKE OVERLAY # →О̵→ +← (‎ O̶ ‎) 004F 0336 LATIN CAPITAL LETTER O, COMBINING LONG STROKE OVERLAY # →O̵→→О̵→ +← (‎ О̵ ‎) 041E 0335 CYRILLIC CAPITAL LETTER O, COMBINING SHORT STROKE OVERLAY +← (‎ Ɵ ‎) 019F LATIN CAPITAL LETTER O WITH MIDDLE TILDE # →Ѳ→→О̵→ +← (‎ Ꝋ ‎) A74A LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY # →O̶→→O̵→→О̵→ +← (‎ θ ‎) 03B8 GREEK SMALL LETTER THETA # →Ꮎ→→O̵→→О̵→ +← (‎ Θ ‎) 0398 GREEK CAPITAL LETTER THETA # →Ѳ→→О̵→ +← (‎ Ө ‎) 04E8 CYRILLIC CAPITAL LETTER BARRED O # →Ѳ→→О̵→ +← (‎ Ѳ ‎) 0472 CYRILLIC CAPITAL LETTER FITA # →О̵→ +← (‎ Ꮎ ‎) 13BE CHEROKEE LETTER NA # →O̵→→О̵→ +← (‎ Ꮻ ‎) 13EB CHEROKEE LETTER WI # →Ѳ→→О̵→ +← (‎ ⴱ ‎) 2D31 TIFINAGH LETTER YAB # →ϴ→→Ѳ→→О̵→ +← (‎ ⊖ ‎) 2296 CIRCLED MINUS # →Θ→→Ѳ→→О̵→ +← (‎ ⊝ ‎) 229D CIRCLED DASH # →⊖→→Θ→→Ѳ→→О̵→ +← (‎ ⍬ ‎) 236C APL FUNCTIONAL SYMBOL ZILDE # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝈚 ‎) 1D21A GREEK VOCAL NOTATION SYMBOL-52 # →Ꝋ→→O̶→→O̵→→О̵→ +← (‎ 🜔 ‎) 1F714 ALCHEMICAL SYMBOL FOR SALT # →Ɵ→→Ѳ→→О̵→ +← (‎ ϴ ‎) 03F4 GREEK CAPITAL THETA SYMBOL # →Ѳ→→О̵→ +← (‎ ϑ ‎) 03D1 GREEK THETA SYMBOL # →⊖→→Θ→→Ѳ→→О̵→ +← (‎ 𝚯 ‎) 1D6AF MATHEMATICAL BOLD CAPITAL THETA # →Θ→→Ѳ→→О̵→ +← (‎ 𝚹 ‎) 1D6B9 MATHEMATICAL BOLD CAPITAL THETA SYMBOL # →Θ→→Ѳ→→О̵→ +← (‎ 𝛉 ‎) 1D6C9 MATHEMATICAL BOLD SMALL THETA # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝛝 ‎) 1D6DD MATHEMATICAL BOLD THETA SYMBOL # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝛩 ‎) 1D6E9 MATHEMATICAL ITALIC CAPITAL THETA # →Θ→→Ѳ→→О̵→ +← (‎ 𝛳 ‎) 1D6F3 MATHEMATICAL ITALIC CAPITAL THETA SYMBOL # →Θ→→Ѳ→→О̵→ +← (‎ 𝜃 ‎) 1D703 MATHEMATICAL ITALIC SMALL THETA # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝜗 ‎) 1D717 MATHEMATICAL ITALIC THETA SYMBOL # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝜣 ‎) 1D723 MATHEMATICAL BOLD ITALIC CAPITAL THETA # →Θ→→Ѳ→→О̵→ +← (‎ 𝜭 ‎) 1D72D MATHEMATICAL BOLD ITALIC CAPITAL THETA SYMBOL # →Θ→→Ѳ→→О̵→ +← (‎ 𝜽 ‎) 1D73D MATHEMATICAL BOLD ITALIC SMALL THETA # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝝑 ‎) 1D751 MATHEMATICAL BOLD ITALIC THETA SYMBOL # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝝝 ‎) 1D75D MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA # →Θ→→Ѳ→→О̵→ +← (‎ 𝝧 ‎) 1D767 MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA SYMBOL # →Θ→→Ѳ→→О̵→ +← (‎ 𝝷 ‎) 1D777 MATHEMATICAL SANS-SERIF BOLD SMALL THETA # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝞋 ‎) 1D78B MATHEMATICAL SANS-SERIF BOLD THETA SYMBOL # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝞗 ‎) 1D797 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA # →Θ→→Ѳ→→О̵→ +← (‎ 𝞡 ‎) 1D7A1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA SYMBOL # →Θ→→Ѳ→→О̵→ +← (‎ 𝞱 ‎) 1D7B1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL THETA # →θ→→Ꮎ→→O̵→→О̵→ +← (‎ 𝟅 ‎) 1D7C5 MATHEMATICAL SANS-SERIF BOLD ITALIC THETA SYMBOL # →θ→→Ꮎ→→O̵→→О̵→ + +# O点 0点 ㍘ + (‎ 0点 ‎) 0030 70B9 DIGIT ZERO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ O点 ‎) 004F 70B9 LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍘ ‎) 3358 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ZERO + +# l I 1 | Ɩ ǀ ו ן ا ١ ۱ Ι І Ӏ ߊ ᛁ Ⲓ ⵏ ꓲ 𐊊 𐌉 𖼨 ׀ ∣ 𐌠 𞣇 ⏽ 🯱 Ⅰ ⅼ I l ℐ ℑ ℓ 𞸀 𞺀 ﺍ ﺎ 𝐈 𝐥 𝐼 𝑙 𝑰 𝒍 𝓁 𝓘 𝓵 𝔩 𝕀 𝕝 𝕴 𝖑 𝖨 𝗅 𝗜 𝗹 𝘐 𝘭 𝙄 𝙡 𝙸 𝚕 𝚰 𝛪 𝜤 𝝞 𝞘 𝟏 𝟙 𝟣 𝟭 𝟷 │ + (‎ 1 ‎) 0031 DIGIT ONE +← (‎ l ‎) 006C LATIN SMALL LETTER L +← (‎ I ‎) 0049 LATIN CAPITAL LETTER I +← (‎ | ‎) 007C VERTICAL LINE # →l→ +← (‎ Ɩ ‎) 0196 LATIN CAPITAL LETTER IOTA # →I→ +← (‎ ǀ ‎) 01C0 LATIN LETTER DENTAL CLICK # →I→ +← (‎ ו ‎) 05D5 HEBREW LETTER VAV # →l→ +← (‎ ן ‎) 05DF HEBREW LETTER FINAL NUN # →l→ +← (‎ ا ‎) 0627 ARABIC LETTER ALEF +← (‎ ١ ‎) 0661 ARABIC-INDIC DIGIT ONE +← (‎ ۱ ‎) 06F1 EXTENDED ARABIC-INDIC DIGIT ONE +← (‎ Ι ‎) 0399 GREEK CAPITAL LETTER IOTA # →I→ +← (‎ І ‎) 0406 CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I # →I→ +← (‎ Ӏ ‎) 04C0 CYRILLIC LETTER PALOCHKA # →I→ +← (‎ ߊ ‎) 07CA NKO LETTER A # →∣→→ǀ→→I→ +← (‎ ᛁ ‎) 16C1 RUNIC LETTER ISAZ IS ISS I # →I→ +← (‎ Ⲓ ‎) 2C92 COPTIC CAPITAL LETTER IAUDA # →I→ +← (‎ ⵏ ‎) 2D4F TIFINAGH LETTER YAN # →I→ +← (‎ ꓲ ‎) A4F2 LISU LETTER I # →I→ +← (‎ 𐊊 ‎) 1028A LYCIAN LETTER J # →I→ +← (‎ 𐌉 ‎) 10309 OLD ITALIC LETTER I # →I→ +← (‎ 𖼨 ‎) 16F28 MIAO LETTER GHA # →I→ +← (‎ ׀ ‎) 05C0 HEBREW PUNCTUATION PASEQ # →|→→l→ +← (‎ ∣ ‎) 2223 DIVIDES # →ǀ→→I→ +← (‎ 𐌠 ‎) 10320 OLD ITALIC NUMERAL ONE # →𐌉→→I→ +← (‎ 𞣇 ‎) 1E8C7 MENDE KIKAKUI DIGIT ONE # →l→ +← (‎ ⏽ ‎) 23FD POWER ON SYMBOL # →I→ +← (‎ 🯱 ‎) 1FBF1 SEGMENTED DIGIT ONE +← (‎ Ⅰ ‎) 2160 ROMAN NUMERAL ONE # →I→ +← (‎ ⅼ ‎) 217C SMALL ROMAN NUMERAL FIFTY # →l→ +← (‎ I ‎) FF29 FULLWIDTH LATIN CAPITAL LETTER I # →Ӏ→→I→ +← (‎ l ‎) FF4C FULLWIDTH LATIN SMALL LETTER L # →Ⅰ→→I→ +← (‎ ℐ ‎) 2110 SCRIPT CAPITAL I # →I→ +← (‎ ℑ ‎) 2111 BLACK-LETTER CAPITAL I # →I→ +← (‎ ℓ ‎) 2113 SCRIPT SMALL L # →l→ +← (‎ 𞸀 ‎) 1EE00 ARABIC MATHEMATICAL ALEF # →‎ا‎→ +← (‎ 𞺀 ‎) 1EE80 ARABIC MATHEMATICAL LOOPED ALEF # →‎ا‎→ +← (‎ ﺍ ‎) FE8D ARABIC LETTER ALEF ISOLATED FORM # →‎ا‎→ +← (‎ ﺎ ‎) FE8E ARABIC LETTER ALEF FINAL FORM # →‎ا‎→ +← (‎ 𝐈 ‎) 1D408 MATHEMATICAL BOLD CAPITAL I # →I→ +← (‎ 𝐥 ‎) 1D425 MATHEMATICAL BOLD SMALL L # →l→ +← (‎ 𝐼 ‎) 1D43C MATHEMATICAL ITALIC CAPITAL I # →I→ +← (‎ 𝑙 ‎) 1D459 MATHEMATICAL ITALIC SMALL L # →l→ +← (‎ 𝑰 ‎) 1D470 MATHEMATICAL BOLD ITALIC CAPITAL I # →I→ +← (‎ 𝒍 ‎) 1D48D MATHEMATICAL BOLD ITALIC SMALL L # →l→ +← (‎ 𝓁 ‎) 1D4C1 MATHEMATICAL SCRIPT SMALL L # →l→ +← (‎ 𝓘 ‎) 1D4D8 MATHEMATICAL BOLD SCRIPT CAPITAL I # →I→ +← (‎ 𝓵 ‎) 1D4F5 MATHEMATICAL BOLD SCRIPT SMALL L # →l→ +← (‎ 𝔩 ‎) 1D529 MATHEMATICAL FRAKTUR SMALL L # →l→ +← (‎ 𝕀 ‎) 1D540 MATHEMATICAL DOUBLE-STRUCK CAPITAL I # →I→ +← (‎ 𝕝 ‎) 1D55D MATHEMATICAL DOUBLE-STRUCK SMALL L # →l→ +← (‎ 𝕴 ‎) 1D574 MATHEMATICAL BOLD FRAKTUR CAPITAL I # →I→ +← (‎ 𝖑 ‎) 1D591 MATHEMATICAL BOLD FRAKTUR SMALL L # →l→ +← (‎ 𝖨 ‎) 1D5A8 MATHEMATICAL SANS-SERIF CAPITAL I # →I→ +← (‎ 𝗅 ‎) 1D5C5 MATHEMATICAL SANS-SERIF SMALL L # →l→ +← (‎ 𝗜 ‎) 1D5DC MATHEMATICAL SANS-SERIF BOLD CAPITAL I # →I→ +← (‎ 𝗹 ‎) 1D5F9 MATHEMATICAL SANS-SERIF BOLD SMALL L # →l→ +← (‎ 𝘐 ‎) 1D610 MATHEMATICAL SANS-SERIF ITALIC CAPITAL I # →I→ +← (‎ 𝘭 ‎) 1D62D MATHEMATICAL SANS-SERIF ITALIC SMALL L # →l→ +← (‎ 𝙄 ‎) 1D644 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL I # →I→ +← (‎ 𝙡 ‎) 1D661 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL L # →l→ +← (‎ 𝙸 ‎) 1D678 MATHEMATICAL MONOSPACE CAPITAL I # →I→ +← (‎ 𝚕 ‎) 1D695 MATHEMATICAL MONOSPACE SMALL L # →l→ +← (‎ 𝚰 ‎) 1D6B0 MATHEMATICAL BOLD CAPITAL IOTA # →𝐈→→I→ +← (‎ 𝛪 ‎) 1D6EA MATHEMATICAL ITALIC CAPITAL IOTA # →Ι→→I→ +← (‎ 𝜤 ‎) 1D724 MATHEMATICAL BOLD ITALIC CAPITAL IOTA # →𝑰→→I→ +← (‎ 𝝞 ‎) 1D75E MATHEMATICAL SANS-SERIF BOLD CAPITAL IOTA # →Ι→→I→ +← (‎ 𝞘 ‎) 1D798 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL IOTA # →Ι→→I→ +← (‎ 𝟏 ‎) 1D7CF MATHEMATICAL BOLD DIGIT ONE +← (‎ 𝟙 ‎) 1D7D9 MATHEMATICAL DOUBLE-STRUCK DIGIT ONE +← (‎ 𝟣 ‎) 1D7E3 MATHEMATICAL SANS-SERIF DIGIT ONE +← (‎ 𝟭 ‎) 1D7ED MATHEMATICAL SANS-SERIF BOLD DIGIT ONE +← (‎ 𝟷 ‎) 1D7F7 MATHEMATICAL MONOSPACE DIGIT ONE +← (‎ │ ‎) FFE8 HALFWIDTH FORMS LIGHT VERTICAL # →|→→l→ + +# l' 1' וי ױ + (‎ 1' ‎) 0031 0027 DIGIT ONE, APOSTROPHE +← (‎ l' ‎) 006C 0027 LATIN SMALL LETTER L, APOSTROPHE # →‎וי‎→ +← (‎ וי ‎) 05D5 05D9 HEBREW LETTER VAV, HEBREW LETTER YOD +← (‎ ױ ‎) 05F1 HEBREW LIGATURE YIDDISH VAV YOD # →‎וי‎→ + +# l, 1, 🄂 + (‎ 1, ‎) 0031 002C DIGIT ONE, COMMA +← (‎ l, ‎) 006C 002C LATIN SMALL LETTER L, COMMA +← (‎ 🄂 ‎) 1F102 DIGIT ONE COMMA + +# l. 1. ⒈ + (‎ 1. ‎) 0031 002E DIGIT ONE, FULL STOP +← (‎ l. ‎) 006C 002E LATIN SMALL LETTER L, FULL STOP +← (‎ ⒈ ‎) 2488 DIGIT ONE FULL STOP + +# lO. l0. 10. ⒑ + (‎ 10. ‎) 0031 0030 002E DIGIT ONE, DIGIT ZERO, FULL STOP +← (‎ lO. ‎) 006C 004F 002E LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, FULL STOP +← (‎ l0. ‎) 006C 0030 002E LATIN SMALL LETTER L, DIGIT ZERO, FULL STOP +← (‎ ⒑ ‎) 2491 NUMBER TEN FULL STOP + +# lO日 l0日 10日 ㏩ + (‎ 10日 ‎) 0031 0030 65E5 DIGIT ONE, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ lO日 ‎) 006C 004F 65E5 LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l0日 ‎) 006C 0030 65E5 LATIN SMALL LETTER L, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏩ ‎) 33E9 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TEN + +# lO月 l0月 10月 ㋉ + (‎ 10月 ‎) 0031 0030 6708 DIGIT ONE, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-6708 +← (‎ lO月 ‎) 006C 004F 6708 LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-6708 +← (‎ l0月 ‎) 006C 0030 6708 LATIN SMALL LETTER L, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋉ ‎) 32C9 IDEOGRAPHIC TELEGRAPH SYMBOL FOR OCTOBER + +# lO点 l0点 10点 ㍢ + (‎ 10点 ‎) 0031 0030 70B9 DIGIT ONE, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ lO点 ‎) 006C 004F 70B9 LATIN SMALL LETTER L, LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l0点 ‎) 006C 0030 70B9 LATIN SMALL LETTER L, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍢ ‎) 3362 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TEN + +# ll II 11 || וו ǁ װ ‖ ∥ Ⅱ + (‎ 11 ‎) 0031 0031 DIGIT ONE, DIGIT ONE +← (‎ ll ‎) 006C 006C LATIN SMALL LETTER L, LATIN SMALL LETTER L # →‎וו‎→ +← (‎ II ‎) 0049 0049 LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I # →ll→→‎וו‎→ +← (‎ || ‎) 007C 007C VERTICAL LINE, VERTICAL LINE # →ll→→‎וו‎→ +← (‎ וו ‎) 05D5 05D5 HEBREW LETTER VAV, HEBREW LETTER VAV +← (‎ ǁ ‎) 01C1 LATIN LETTER LATERAL CLICK # →‖→→∥→→||→→ll→→‎וו‎→ +← (‎ װ ‎) 05F0 HEBREW LIGATURE YIDDISH DOUBLE VAV # →‎וו‎→ +← (‎ ‖ ‎) 2016 DOUBLE VERTICAL LINE # →∥→→||→→ll→→‎וו‎→ +← (‎ ∥ ‎) 2225 PARALLEL TO # →||→→ll→→‎וו‎→ +← (‎ Ⅱ ‎) 2161 ROMAN NUMERAL TWO # →II→→ll→→‎וו‎→ + +# ll. 11. ⒒ + (‎ 11. ‎) 0031 0031 002E DIGIT ONE, DIGIT ONE, FULL STOP +← (‎ ll. ‎) 006C 006C 002E LATIN SMALL LETTER L, LATIN SMALL LETTER L, FULL STOP +← (‎ ⒒ ‎) 2492 NUMBER ELEVEN FULL STOP + +# ll日 11日 ㏪ + (‎ 11日 ‎) 0031 0031 65E5 DIGIT ONE, DIGIT ONE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ll日 ‎) 006C 006C 65E5 LATIN SMALL LETTER L, LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏪ ‎) 33EA IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY ELEVEN + +# ll月 11月 ㋊ + (‎ 11月 ‎) 0031 0031 6708 DIGIT ONE, DIGIT ONE, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ll月 ‎) 006C 006C 6708 LATIN SMALL LETTER L, LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋊ ‎) 32CA IDEOGRAPHIC TELEGRAPH SYMBOL FOR NOVEMBER + +# ll点 11点 ㍣ + (‎ 11点 ‎) 0031 0031 70B9 DIGIT ONE, DIGIT ONE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ll点 ‎) 006C 006C 70B9 LATIN SMALL LETTER L, LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍣ ‎) 3363 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ELEVEN + +# l2. 12. ⒓ + (‎ 12. ‎) 0031 0032 002E DIGIT ONE, DIGIT TWO, FULL STOP +← (‎ l2. ‎) 006C 0032 002E LATIN SMALL LETTER L, DIGIT TWO, FULL STOP +← (‎ ⒓ ‎) 2493 NUMBER TWELVE FULL STOP + +# l2日 12日 ㏫ + (‎ 12日 ‎) 0031 0032 65E5 DIGIT ONE, DIGIT TWO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l2日 ‎) 006C 0032 65E5 LATIN SMALL LETTER L, DIGIT TWO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏫ ‎) 33EB IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWELVE + +# l2月 12月 ㋋ + (‎ 12月 ‎) 0031 0032 6708 DIGIT ONE, DIGIT TWO, CJK UNIFIED IDEOGRAPH-6708 +← (‎ l2月 ‎) 006C 0032 6708 LATIN SMALL LETTER L, DIGIT TWO, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋋ ‎) 32CB IDEOGRAPHIC TELEGRAPH SYMBOL FOR DECEMBER + +# l2点 12点 ㍤ + (‎ 12点 ‎) 0031 0032 70B9 DIGIT ONE, DIGIT TWO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l2点 ‎) 006C 0032 70B9 LATIN SMALL LETTER L, DIGIT TWO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍤ ‎) 3364 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWELVE + +# l3. 13. ⒔ + (‎ 13. ‎) 0031 0033 002E DIGIT ONE, DIGIT THREE, FULL STOP +← (‎ l3. ‎) 006C 0033 002E LATIN SMALL LETTER L, DIGIT THREE, FULL STOP +← (‎ ⒔ ‎) 2494 NUMBER THIRTEEN FULL STOP + +# l3日 13日 ㏬ + (‎ 13日 ‎) 0031 0033 65E5 DIGIT ONE, DIGIT THREE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l3日 ‎) 006C 0033 65E5 LATIN SMALL LETTER L, DIGIT THREE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏬ ‎) 33EC IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTEEN + +# l3点 13点 ㍥ + (‎ 13点 ‎) 0031 0033 70B9 DIGIT ONE, DIGIT THREE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l3点 ‎) 006C 0033 70B9 LATIN SMALL LETTER L, DIGIT THREE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍥ ‎) 3365 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR THIRTEEN + +# l4. 14. ⒕ + (‎ 14. ‎) 0031 0034 002E DIGIT ONE, DIGIT FOUR, FULL STOP +← (‎ l4. ‎) 006C 0034 002E LATIN SMALL LETTER L, DIGIT FOUR, FULL STOP +← (‎ ⒕ ‎) 2495 NUMBER FOURTEEN FULL STOP + +# l4日 14日 ㏭ + (‎ 14日 ‎) 0031 0034 65E5 DIGIT ONE, DIGIT FOUR, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l4日 ‎) 006C 0034 65E5 LATIN SMALL LETTER L, DIGIT FOUR, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏭ ‎) 33ED IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FOURTEEN + +# l4点 14点 ㍦ + (‎ 14点 ‎) 0031 0034 70B9 DIGIT ONE, DIGIT FOUR, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l4点 ‎) 006C 0034 70B9 LATIN SMALL LETTER L, DIGIT FOUR, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍦ ‎) 3366 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FOURTEEN + +# l5. 15. ⒖ + (‎ 15. ‎) 0031 0035 002E DIGIT ONE, DIGIT FIVE, FULL STOP +← (‎ l5. ‎) 006C 0035 002E LATIN SMALL LETTER L, DIGIT FIVE, FULL STOP +← (‎ ⒖ ‎) 2496 NUMBER FIFTEEN FULL STOP + +# l5日 15日 ㏮ + (‎ 15日 ‎) 0031 0035 65E5 DIGIT ONE, DIGIT FIVE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l5日 ‎) 006C 0035 65E5 LATIN SMALL LETTER L, DIGIT FIVE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏮ ‎) 33EE IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FIFTEEN + +# l5点 15点 ㍧ + (‎ 15点 ‎) 0031 0035 70B9 DIGIT ONE, DIGIT FIVE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l5点 ‎) 006C 0035 70B9 LATIN SMALL LETTER L, DIGIT FIVE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍧ ‎) 3367 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FIFTEEN + +# l6. 16. ⒗ + (‎ 16. ‎) 0031 0036 002E DIGIT ONE, DIGIT SIX, FULL STOP +← (‎ l6. ‎) 006C 0036 002E LATIN SMALL LETTER L, DIGIT SIX, FULL STOP +← (‎ ⒗ ‎) 2497 NUMBER SIXTEEN FULL STOP + +# l6日 16日 ㏯ + (‎ 16日 ‎) 0031 0036 65E5 DIGIT ONE, DIGIT SIX, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l6日 ‎) 006C 0036 65E5 LATIN SMALL LETTER L, DIGIT SIX, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏯ ‎) 33EF IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SIXTEEN + +# l6点 16点 ㍨ + (‎ 16点 ‎) 0031 0036 70B9 DIGIT ONE, DIGIT SIX, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l6点 ‎) 006C 0036 70B9 LATIN SMALL LETTER L, DIGIT SIX, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍨ ‎) 3368 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SIXTEEN + +# l7. 17. ⒘ + (‎ 17. ‎) 0031 0037 002E DIGIT ONE, DIGIT SEVEN, FULL STOP +← (‎ l7. ‎) 006C 0037 002E LATIN SMALL LETTER L, DIGIT SEVEN, FULL STOP +← (‎ ⒘ ‎) 2498 NUMBER SEVENTEEN FULL STOP + +# l7日 17日 ㏰ + (‎ 17日 ‎) 0031 0037 65E5 DIGIT ONE, DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l7日 ‎) 006C 0037 65E5 LATIN SMALL LETTER L, DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏰ ‎) 33F0 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SEVENTEEN + +# l7点 17点 ㍩ + (‎ 17点 ‎) 0031 0037 70B9 DIGIT ONE, DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l7点 ‎) 006C 0037 70B9 LATIN SMALL LETTER L, DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍩ ‎) 3369 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SEVENTEEN + +# l8. 18. ⒙ + (‎ 18. ‎) 0031 0038 002E DIGIT ONE, DIGIT EIGHT, FULL STOP +← (‎ l8. ‎) 006C 0038 002E LATIN SMALL LETTER L, DIGIT EIGHT, FULL STOP +← (‎ ⒙ ‎) 2499 NUMBER EIGHTEEN FULL STOP + +# l8日 18日 ㏱ + (‎ 18日 ‎) 0031 0038 65E5 DIGIT ONE, DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l8日 ‎) 006C 0038 65E5 LATIN SMALL LETTER L, DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏱ ‎) 33F1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY EIGHTEEN + +# l8点 18点 ㍪ + (‎ 18点 ‎) 0031 0038 70B9 DIGIT ONE, DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l8点 ‎) 006C 0038 70B9 LATIN SMALL LETTER L, DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍪ ‎) 336A IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR EIGHTEEN + +# l9. 19. ⒚ + (‎ 19. ‎) 0031 0039 002E DIGIT ONE, DIGIT NINE, FULL STOP +← (‎ l9. ‎) 006C 0039 002E LATIN SMALL LETTER L, DIGIT NINE, FULL STOP +← (‎ ⒚ ‎) 249A NUMBER NINETEEN FULL STOP + +# l9日 19日 ㏲ + (‎ 19日 ‎) 0031 0039 65E5 DIGIT ONE, DIGIT NINE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l9日 ‎) 006C 0039 65E5 LATIN SMALL LETTER L, DIGIT NINE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏲ ‎) 33F2 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY NINETEEN + +# l9点 19点 ㍫ + (‎ 19点 ‎) 0031 0039 70B9 DIGIT ONE, DIGIT NINE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l9点 ‎) 006C 0039 70B9 LATIN SMALL LETTER L, DIGIT NINE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍫ ‎) 336B IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR NINETEEN + +# l̋ 1̋ اً ﴼ ﴽ + (‎ 1̋ ‎) 0031 030B DIGIT ONE, COMBINING DOUBLE ACUTE ACCENT +← (‎ l̋ ‎) 006C 030B LATIN SMALL LETTER L, COMBINING DOUBLE ACUTE ACCENT # →‎اً‎→ +← (‎ اً ‎) 0627 064B ARABIC LETTER ALEF, ARABIC FATHATAN +← (‎ ﴼ ‎) FD3C ARABIC LIGATURE ALEF WITH FATHATAN FINAL FORM # →‎اً‎→ +← (‎ ﴽ ‎) FD3D ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM # →‎اً‎→ + +# lكبر 1كبر اكبر ﷳ + (‎ 1كبر ‎) 0031 0643 0628 0631 DIGIT ONE, ARABIC LETTER KAF, ARABIC LETTER BEH, ARABIC LETTER REH +← (‎ lكبر ‎) 006C 0643 0628 0631 LATIN SMALL LETTER L, ARABIC LETTER KAF, ARABIC LETTER BEH, ARABIC LETTER REH # →‎اكبر‎→ +← (‎ اكبر ‎) 0627 0643 0628 0631 ARABIC LETTER ALEF, ARABIC LETTER KAF, ARABIC LETTER BEH, ARABIC LETTER REH +← (‎ ﷳ ‎) FDF3 ARABIC LIGATURE AKBAR ISOLATED FORM # →‎اكبر‎→ + +# lللّٰo lللّٰه 1للّٰه اللّٰه lللّo lللّه 1للّه اللّه lللo lلله 1لله الله ﷲ + (‎ 1لله ‎) 0031 0644 0644 0647 DIGIT ONE, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ lللّٰo ‎) 006C 0644 0644 0651 0670 006F LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER SUPERSCRIPT ALEF, LATIN SMALL LETTER O # →‎اللّٰه‎→→‎ﷲ‎→→‎الله‎→ +← (‎ lللّٰه ‎) 006C 0644 0644 0651 0670 0647 LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER SUPERSCRIPT ALEF, ARABIC LETTER HEH # →‎1للّٰه‎→→‎اللّٰه‎→→‎ﷲ‎→→‎الله‎→ +← (‎ 1للّٰه ‎) 0031 0644 0644 0651 0670 0647 DIGIT ONE, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER SUPERSCRIPT ALEF, ARABIC LETTER HEH # →‎اللّٰه‎→→‎ﷲ‎→→‎الله‎→ +← (‎ اللّٰه ‎) 0627 0644 0644 0651 0670 0647 ARABIC LETTER ALEF, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER SUPERSCRIPT ALEF, ARABIC LETTER HEH # →‎ﷲ‎→→‎الله‎→ +← (‎ lللّo ‎) 006C 0644 0644 0651 006F LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, LATIN SMALL LETTER O # →‎اللّه‎→→‎ﷲ‎→→‎الله‎→ +← (‎ lللّه ‎) 006C 0644 0644 0651 0647 LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER HEH # →‎1للّه‎→→‎اللّه‎→→‎ﷲ‎→→‎الله‎→ +← (‎ 1للّه ‎) 0031 0644 0644 0651 0647 DIGIT ONE, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER HEH # →‎اللّه‎→→‎ﷲ‎→→‎الله‎→ +← (‎ اللّه ‎) 0627 0644 0644 0651 0647 ARABIC LETTER ALEF, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC SHADDA, ARABIC LETTER HEH # →‎ﷲ‎→→‎الله‎→ +← (‎ lللo ‎) 006C 0644 0644 006F LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, LATIN SMALL LETTER O # →‎الله‎→ +← (‎ lلله ‎) 006C 0644 0644 0647 LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ الله ‎) 0627 0644 0644 0647 ARABIC LETTER ALEF, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ ﷲ ‎) FDF2 ARABIC LIGATURE ALLAH ISOLATED FORM # →‎الله‎→ + +# lٕ 1ٕ اٟ ٳ إ ﺇ ﺈ + (‎ 1ٕ ‎) 0031 0655 DIGIT ONE, ARABIC HAMZA BELOW +← (‎ lٕ ‎) 006C 0655 LATIN SMALL LETTER L, ARABIC HAMZA BELOW # →‎اٟ‎→ +← (‎ اٟ ‎) 0627 065F ARABIC LETTER ALEF, ARABIC WAVY HAMZA BELOW +← (‎ ٳ ‎) 0673 ARABIC LETTER ALEF WITH WAVY HAMZA BELOW # →‎اٟ‎→ +← (‎ إ ‎) 0625 ARABIC LETTER ALEF WITH HAMZA BELOW # →‎ٳ‎→→‎اٟ‎→ +← (‎ ﺇ ‎) FE87 ARABIC LETTER ALEF WITH HAMZA BELOW ISOLATED FORM # →‎إ‎→→‎ٳ‎→→‎اٟ‎→ +← (‎ ﺈ ‎) FE88 ARABIC LETTER ALEF WITH HAMZA BELOW FINAL FORM # →‎إ‎→→‎ٳ‎→→‎اٟ‎→ + +# lٴ ٴl 1ٴ اٴ ٴ1 ٴا ٲ أ ٵ ﺃ ﺄ + (‎ 1ٴ ‎) 0031 0674 DIGIT ONE, ARABIC LETTER HIGH HAMZA +← (‎ lٴ ‎) 006C 0674 LATIN SMALL LETTER L, ARABIC LETTER HIGH HAMZA # →‎اٴ‎→ +← (‎ ٴl ‎) 0674 006C ARABIC LETTER HIGH HAMZA, LATIN SMALL LETTER L # →‎ٴا‎→→‎ٵ‎→→‎اٴ‎→ +← (‎ اٴ ‎) 0627 0674 ARABIC LETTER ALEF, ARABIC LETTER HIGH HAMZA +← (‎ ٴ1 ‎) 0674 0031 ARABIC LETTER HIGH HAMZA, DIGIT ONE # →‎ٴا‎→→‎ٵ‎→→‎اٴ‎→ +← (‎ ٴا ‎) 0674 0627 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF # →‎ٵ‎→→‎اٴ‎→ +← (‎ ٲ ‎) 0672 ARABIC LETTER ALEF WITH WAVY HAMZA ABOVE # →‎أ‎→→‎ٵ‎→→‎اٴ‎→ +← (‎ أ ‎) 0623 ARABIC LETTER ALEF WITH HAMZA ABOVE # →‎ٵ‎→→‎اٴ‎→ +← (‎ ٵ ‎) 0675 ARABIC LETTER HIGH HAMZA ALEF # →‎اٴ‎→ +← (‎ ﺃ ‎) FE83 ARABIC LETTER ALEF WITH HAMZA ABOVE ISOLATED FORM # →‎ٵ‎→→‎اٴ‎→ +← (‎ ﺄ ‎) FE84 ARABIC LETTER ALEF WITH HAMZA ABOVE FINAL FORM # →‎أ‎→→‎ٵ‎→→‎اٴ‎→ + +# l· L· 1ᐧ Ꮮ· ᒪ· ᒪᐧ ᒷ Ŀ ŀ + (‎ 1ᐧ ‎) 0031 1427 DIGIT ONE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ l· ‎) 006C 00B7 LATIN SMALL LETTER L, MIDDLE DOT +← (‎ L· ‎) 004C 00B7 LATIN CAPITAL LETTER L, MIDDLE DOT # →ᒪ·→→ᒪᐧ→→ᒷ→ +← (‎ Ꮮ· ‎) 13DE 00B7 CHEROKEE LETTER TLE, MIDDLE DOT # →ᒪ·→→ᒪᐧ→→ᒷ→ +← (‎ ᒪ· ‎) 14AA 00B7 CANADIAN SYLLABICS MA, MIDDLE DOT # →ᒪᐧ→→ᒷ→ +← (‎ ᒪᐧ ‎) 14AA 1427 CANADIAN SYLLABICS MA, CANADIAN SYLLABICS FINAL MIDDLE DOT # →ᒷ→ +← (‎ ᒷ ‎) 14B7 CANADIAN SYLLABICS WEST-CREE MWA +← (‎ Ŀ ‎) 013F LATIN CAPITAL LETTER L WITH MIDDLE DOT # →L·→→ᒪ·→→ᒪᐧ→→ᒷ→ +← (‎ ŀ ‎) 0140 LATIN SMALL LETTER L WITH MIDDLE DOT # →l·→ + +# l日 1日 ㏠ + (‎ 1日 ‎) 0031 65E5 DIGIT ONE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ l日 ‎) 006C 65E5 LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏠ ‎) 33E0 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY ONE + +# l月 1月 ㋀ + (‎ 1月 ‎) 0031 6708 DIGIT ONE, CJK UNIFIED IDEOGRAPH-6708 +← (‎ l月 ‎) 006C 6708 LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋀ ‎) 32C0 IDEOGRAPHIC TELEGRAPH SYMBOL FOR JANUARY + +# l点 1点 ㍙ + (‎ 1点 ‎) 0031 70B9 DIGIT ONE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ l点 ‎) 006C 70B9 LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍙ ‎) 3359 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ONE + +# 2 Ƨ Ꝛ Ϩ ᒿ Ꙅ ꛯ 🯲 𝟐 𝟚 𝟤 𝟮 𝟸 + (‎ 2 ‎) 0032 DIGIT TWO +← (‎ Ƨ ‎) 01A7 LATIN CAPITAL LETTER TONE TWO +← (‎ Ꝛ ‎) A75A LATIN CAPITAL LETTER R ROTUNDA +← (‎ Ϩ ‎) 03E8 COPTIC CAPITAL LETTER HORI # →Ƨ→ +← (‎ ᒿ ‎) 14BF CANADIAN SYLLABICS SAYISI M +← (‎ Ꙅ ‎) A644 CYRILLIC CAPITAL LETTER REVERSED DZE # →Ƨ→ +← (‎ ꛯ ‎) A6EF BAMUM LETTER KOGHOM # →Ƨ→ +← (‎ 🯲 ‎) 1FBF2 SEGMENTED DIGIT TWO +← (‎ 𝟐 ‎) 1D7D0 MATHEMATICAL BOLD DIGIT TWO +← (‎ 𝟚 ‎) 1D7DA MATHEMATICAL DOUBLE-STRUCK DIGIT TWO +← (‎ 𝟤 ‎) 1D7E4 MATHEMATICAL SANS-SERIF DIGIT TWO +← (‎ 𝟮 ‎) 1D7EE MATHEMATICAL SANS-SERIF BOLD DIGIT TWO +← (‎ 𝟸 ‎) 1D7F8 MATHEMATICAL MONOSPACE DIGIT TWO + +# 2, 🄃 + (‎ 2, ‎) 0032 002C DIGIT TWO, COMMA +← (‎ 🄃 ‎) 1F103 DIGIT TWO COMMA + +# 2. ⒉ + (‎ 2. ‎) 0032 002E DIGIT TWO, FULL STOP +← (‎ ⒉ ‎) 2489 DIGIT TWO FULL STOP + +# 2O. 20. ⒛ + (‎ 20. ‎) 0032 0030 002E DIGIT TWO, DIGIT ZERO, FULL STOP +← (‎ 2O. ‎) 0032 004F 002E DIGIT TWO, LATIN CAPITAL LETTER O, FULL STOP +← (‎ ⒛ ‎) 249B NUMBER TWENTY FULL STOP + +# 2O日 20日 ㏳ + (‎ 20日 ‎) 0032 0030 65E5 DIGIT TWO, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ 2O日 ‎) 0032 004F 65E5 DIGIT TWO, LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏳ ‎) 33F3 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY + +# 2O点 20点 ㍬ + (‎ 20点 ‎) 0032 0030 70B9 DIGIT TWO, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ 2O点 ‎) 0032 004F 70B9 DIGIT TWO, LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍬ ‎) 336C IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY + +# 2l日 21日 ㏴ + (‎ 21日 ‎) 0032 0031 65E5 DIGIT TWO, DIGIT ONE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ 2l日 ‎) 0032 006C 65E5 DIGIT TWO, LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏴ ‎) 33F4 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-ONE + +# 2l点 21点 ㍭ + (‎ 21点 ‎) 0032 0031 70B9 DIGIT TWO, DIGIT ONE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ 2l点 ‎) 0032 006C 70B9 DIGIT TWO, LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍭ ‎) 336D IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-ONE + +# 22日 ㏵ + (‎ 22日 ‎) 0032 0032 65E5 DIGIT TWO, DIGIT TWO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏵ ‎) 33F5 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-TWO + +# 22点 ㍮ + (‎ 22点 ‎) 0032 0032 70B9 DIGIT TWO, DIGIT TWO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍮ ‎) 336E IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-TWO + +# 23日 ㏶ + (‎ 23日 ‎) 0032 0033 65E5 DIGIT TWO, DIGIT THREE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏶ ‎) 33F6 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-THREE + +# 23点 ㍯ + (‎ 23点 ‎) 0032 0033 70B9 DIGIT TWO, DIGIT THREE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍯ ‎) 336F IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-THREE + +# 24日 ㏷ + (‎ 24日 ‎) 0032 0034 65E5 DIGIT TWO, DIGIT FOUR, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏷ ‎) 33F7 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-FOUR + +# 24点 ㍰ + (‎ 24点 ‎) 0032 0034 70B9 DIGIT TWO, DIGIT FOUR, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍰ ‎) 3370 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-FOUR + +# 25日 ㏸ + (‎ 25日 ‎) 0032 0035 65E5 DIGIT TWO, DIGIT FIVE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏸ ‎) 33F8 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-FIVE + +# 26日 ㏹ + (‎ 26日 ‎) 0032 0036 65E5 DIGIT TWO, DIGIT SIX, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏹ ‎) 33F9 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-SIX + +# 27日 ㏺ + (‎ 27日 ‎) 0032 0037 65E5 DIGIT TWO, DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏺ ‎) 33FA IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-SEVEN + +# 28日 ㏻ + (‎ 28日 ‎) 0032 0038 65E5 DIGIT TWO, DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏻ ‎) 33FB IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-EIGHT + +# 29日 ㏼ + (‎ 29日 ‎) 0032 0039 65E5 DIGIT TWO, DIGIT NINE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏼ ‎) 33FC IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-NINE + +# 2̵ ƻ + (‎ 2̵ ‎) 0032 0335 DIGIT TWO, COMBINING SHORT STROKE OVERLAY +← (‎ ƻ ‎) 01BB LATIN LETTER TWO WITH STROKE + +# 2日 ㏡ + (‎ 2日 ‎) 0032 65E5 DIGIT TWO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏡ ‎) 33E1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWO + +# 2月 ㋁ + (‎ 2月 ‎) 0032 6708 DIGIT TWO, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋁ ‎) 32C1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR FEBRUARY + +# 2点 ㍚ + (‎ 2点 ‎) 0032 70B9 DIGIT TWO, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍚ ‎) 335A IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWO + +# 3 Ʒ Ȝ Ꝫ З Ӡ Ⳍ 𖼻 𝈆 Ɜ 𑣊 🯳 𝟑 𝟛 𝟥 𝟯 𝟹 + (‎ 3 ‎) 0033 DIGIT THREE +← (‎ Ʒ ‎) 01B7 LATIN CAPITAL LETTER EZH +← (‎ Ȝ ‎) 021C LATIN CAPITAL LETTER YOGH # →Ʒ→ +← (‎ Ꝫ ‎) A76A LATIN CAPITAL LETTER ET +← (‎ З ‎) 0417 CYRILLIC CAPITAL LETTER ZE +← (‎ Ӡ ‎) 04E0 CYRILLIC CAPITAL LETTER ABKHASIAN DZE # →Ʒ→ +← (‎ Ⳍ ‎) 2CCC COPTIC CAPITAL LETTER OLD COPTIC HORI # →Ȝ→→Ʒ→ +← (‎ 𖼻 ‎) 16F3B MIAO LETTER ZA # →Ʒ→ +← (‎ 𝈆 ‎) 1D206 GREEK VOCAL NOTATION SYMBOL-7 +← (‎ Ɜ ‎) A7AB LATIN CAPITAL LETTER REVERSED OPEN E +← (‎ 𑣊 ‎) 118CA WARANG CITI SMALL LETTER ANG +← (‎ 🯳 ‎) 1FBF3 SEGMENTED DIGIT THREE +← (‎ 𝟑 ‎) 1D7D1 MATHEMATICAL BOLD DIGIT THREE +← (‎ 𝟛 ‎) 1D7DB MATHEMATICAL DOUBLE-STRUCK DIGIT THREE +← (‎ 𝟥 ‎) 1D7E5 MATHEMATICAL SANS-SERIF DIGIT THREE +← (‎ 𝟯 ‎) 1D7EF MATHEMATICAL SANS-SERIF BOLD DIGIT THREE +← (‎ 𝟹 ‎) 1D7F9 MATHEMATICAL MONOSPACE DIGIT THREE + +# 3, 🄄 + (‎ 3, ‎) 0033 002C DIGIT THREE, COMMA +← (‎ 🄄 ‎) 1F104 DIGIT THREE COMMA + +# 3. ⒊ + (‎ 3. ‎) 0033 002E DIGIT THREE, FULL STOP +← (‎ ⒊ ‎) 248A DIGIT THREE FULL STOP + +# 3O日 30日 ㏽ + (‎ 30日 ‎) 0033 0030 65E5 DIGIT THREE, DIGIT ZERO, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ 3O日 ‎) 0033 004F 65E5 DIGIT THREE, LATIN CAPITAL LETTER O, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏽ ‎) 33FD IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTY + +# 3l日 31日 ㏾ + (‎ 31日 ‎) 0033 0031 65E5 DIGIT THREE, DIGIT ONE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ 3l日 ‎) 0033 006C 65E5 DIGIT THREE, LATIN SMALL LETTER L, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏾ ‎) 33FE IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTY-ONE + +# 3̦ З̦ З̧ Ҙ + (‎ 3̦ ‎) 0033 0326 DIGIT THREE, COMBINING COMMA BELOW +← (‎ З̦ ‎) 0417 0326 CYRILLIC CAPITAL LETTER ZE, COMBINING COMMA BELOW # →З̧→ +← (‎ З̧ ‎) 0417 0327 CYRILLIC CAPITAL LETTER ZE, COMBINING CEDILLA +← (‎ Ҙ ‎) 0498 CYRILLIC CAPITAL LETTER ZE WITH DESCENDER # →З̧→ + +# 3日 ㏢ + (‎ 3日 ‎) 0033 65E5 DIGIT THREE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏢ ‎) 33E2 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THREE + +# 3月 ㋂ + (‎ 3月 ‎) 0033 6708 DIGIT THREE, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋂ ‎) 32C2 IDEOGRAPHIC TELEGRAPH SYMBOL FOR MARCH + +# 3点 ㍛ + (‎ 3点 ‎) 0033 70B9 DIGIT THREE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍛ ‎) 335B IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR THREE + +# 4 Ꮞ 𑢯 🯴 𝟒 𝟜 𝟦 𝟰 𝟺 + (‎ 4 ‎) 0034 DIGIT FOUR +← (‎ Ꮞ ‎) 13CE CHEROKEE LETTER SE +← (‎ 𑢯 ‎) 118AF WARANG CITI CAPITAL LETTER UC +← (‎ 🯴 ‎) 1FBF4 SEGMENTED DIGIT FOUR +← (‎ 𝟒 ‎) 1D7D2 MATHEMATICAL BOLD DIGIT FOUR +← (‎ 𝟜 ‎) 1D7DC MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR +← (‎ 𝟦 ‎) 1D7E6 MATHEMATICAL SANS-SERIF DIGIT FOUR +← (‎ 𝟰 ‎) 1D7F0 MATHEMATICAL SANS-SERIF BOLD DIGIT FOUR +← (‎ 𝟺 ‎) 1D7FA MATHEMATICAL MONOSPACE DIGIT FOUR + +# 4, 🄅 + (‎ 4, ‎) 0034 002C DIGIT FOUR, COMMA +← (‎ 🄅 ‎) 1F105 DIGIT FOUR COMMA + +# 4. ⒋ + (‎ 4. ‎) 0034 002E DIGIT FOUR, FULL STOP +← (‎ ⒋ ‎) 248B DIGIT FOUR FULL STOP + +# 4· 4ᐧ ᔰ + (‎ 4· ‎) 0034 00B7 DIGIT FOUR, MIDDLE DOT +← (‎ 4ᐧ ‎) 0034 1427 DIGIT FOUR, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔰ ‎) 1530 CANADIAN SYLLABICS WEST-CREE YWE # →4ᐧ→ + +# 4日 ㏣ + (‎ 4日 ‎) 0034 65E5 DIGIT FOUR, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏣ ‎) 33E3 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FOUR + +# 4月 ㋃ + (‎ 4月 ‎) 0034 6708 DIGIT FOUR, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋃ ‎) 32C3 IDEOGRAPHIC TELEGRAPH SYMBOL FOR APRIL + +# 4点 ㍜ + (‎ 4点 ‎) 0034 70B9 DIGIT FOUR, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍜ ‎) 335C IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FOUR + +# 5 Ƽ 𑢻 🯵 𝟓 𝟝 𝟧 𝟱 𝟻 + (‎ 5 ‎) 0035 DIGIT FIVE +← (‎ Ƽ ‎) 01BC LATIN CAPITAL LETTER TONE FIVE +← (‎ 𑢻 ‎) 118BB WARANG CITI CAPITAL LETTER HORR +← (‎ 🯵 ‎) 1FBF5 SEGMENTED DIGIT FIVE +← (‎ 𝟓 ‎) 1D7D3 MATHEMATICAL BOLD DIGIT FIVE +← (‎ 𝟝 ‎) 1D7DD MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE +← (‎ 𝟧 ‎) 1D7E7 MATHEMATICAL SANS-SERIF DIGIT FIVE +← (‎ 𝟱 ‎) 1D7F1 MATHEMATICAL SANS-SERIF BOLD DIGIT FIVE +← (‎ 𝟻 ‎) 1D7FB MATHEMATICAL MONOSPACE DIGIT FIVE + +# 5, 🄆 + (‎ 5, ‎) 0035 002C DIGIT FIVE, COMMA +← (‎ 🄆 ‎) 1F106 DIGIT FIVE COMMA + +# 5. ⒌ + (‎ 5. ‎) 0035 002E DIGIT FIVE, FULL STOP +← (‎ ⒌ ‎) 248C DIGIT FIVE FULL STOP + +# 5日 ㏤ + (‎ 5日 ‎) 0035 65E5 DIGIT FIVE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏤ ‎) 33E4 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FIVE + +# 5月 ㋄ + (‎ 5月 ‎) 0035 6708 DIGIT FIVE, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋄ ‎) 32C4 IDEOGRAPHIC TELEGRAPH SYMBOL FOR MAY + +# 5点 ㍝ + (‎ 5点 ‎) 0035 70B9 DIGIT FIVE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍝ ‎) 335D IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FIVE + +# 6 б Ꮾ Ⳓ 𑣕 🯶 𝟔 𝟞 𝟨 𝟲 𝟼 + (‎ 6 ‎) 0036 DIGIT SIX +← (‎ б ‎) 0431 CYRILLIC SMALL LETTER BE +← (‎ Ꮾ ‎) 13EE CHEROKEE LETTER WV +← (‎ Ⳓ ‎) 2CD2 COPTIC CAPITAL LETTER OLD COPTIC HEI +← (‎ 𑣕 ‎) 118D5 WARANG CITI SMALL LETTER AT +← (‎ 🯶 ‎) 1FBF6 SEGMENTED DIGIT SIX +← (‎ 𝟔 ‎) 1D7D4 MATHEMATICAL BOLD DIGIT SIX +← (‎ 𝟞 ‎) 1D7DE MATHEMATICAL DOUBLE-STRUCK DIGIT SIX +← (‎ 𝟨 ‎) 1D7E8 MATHEMATICAL SANS-SERIF DIGIT SIX +← (‎ 𝟲 ‎) 1D7F2 MATHEMATICAL SANS-SERIF BOLD DIGIT SIX +← (‎ 𝟼 ‎) 1D7FC MATHEMATICAL MONOSPACE DIGIT SIX + +# 6, 🄇 + (‎ 6, ‎) 0036 002C DIGIT SIX, COMMA +← (‎ 🄇 ‎) 1F107 DIGIT SIX COMMA + +# 6. ⒍ + (‎ 6. ‎) 0036 002E DIGIT SIX, FULL STOP +← (‎ ⒍ ‎) 248D DIGIT SIX FULL STOP + +# 6日 ㏥ + (‎ 6日 ‎) 0036 65E5 DIGIT SIX, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏥ ‎) 33E5 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SIX + +# 6月 ㋅ + (‎ 6月 ‎) 0036 6708 DIGIT SIX, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋅ ‎) 32C5 IDEOGRAPHIC TELEGRAPH SYMBOL FOR JUNE + +# 6点 ㍞ + (‎ 6点 ‎) 0036 70B9 DIGIT SIX, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍞ ‎) 335E IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SIX + +# 7 𝈒 𑣆 𐓒 🯷 𝟕 𝟟 𝟩 𝟳 𝟽 + (‎ 7 ‎) 0037 DIGIT SEVEN +← (‎ 𝈒 ‎) 1D212 GREEK VOCAL NOTATION SYMBOL-19 +← (‎ 𑣆 ‎) 118C6 WARANG CITI SMALL LETTER II +← (‎ 𐓒 ‎) 104D2 OSAGE CAPITAL LETTER ZA +← (‎ 🯷 ‎) 1FBF7 SEGMENTED DIGIT SEVEN +← (‎ 𝟕 ‎) 1D7D5 MATHEMATICAL BOLD DIGIT SEVEN +← (‎ 𝟟 ‎) 1D7DF MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN +← (‎ 𝟩 ‎) 1D7E9 MATHEMATICAL SANS-SERIF DIGIT SEVEN +← (‎ 𝟳 ‎) 1D7F3 MATHEMATICAL SANS-SERIF BOLD DIGIT SEVEN +← (‎ 𝟽 ‎) 1D7FD MATHEMATICAL MONOSPACE DIGIT SEVEN + +# 7, 🄈 + (‎ 7, ‎) 0037 002C DIGIT SEVEN, COMMA +← (‎ 🄈 ‎) 1F108 DIGIT SEVEN COMMA + +# 7. ⒎ + (‎ 7. ‎) 0037 002E DIGIT SEVEN, FULL STOP +← (‎ ⒎ ‎) 248E DIGIT SEVEN FULL STOP + +# 7日 ㏦ + (‎ 7日 ‎) 0037 65E5 DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏦ ‎) 33E6 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SEVEN + +# 7月 ㋆ + (‎ 7月 ‎) 0037 6708 DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋆ ‎) 32C6 IDEOGRAPHIC TELEGRAPH SYMBOL FOR JULY + +# 7点 ㍟ + (‎ 7点 ‎) 0037 70B9 DIGIT SEVEN, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍟ ‎) 335F IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SEVEN + +# 8 Ȣ ȣ ৪ ੪ ଃ 𐌚 𞣋 🯸 𝟖 𝟠 𝟪 𝟴 𝟾 + (‎ 8 ‎) 0038 DIGIT EIGHT +← (‎ Ȣ ‎) 0222 LATIN CAPITAL LETTER OU +← (‎ ȣ ‎) 0223 LATIN SMALL LETTER OU +← (‎ ৪ ‎) 09EA BENGALI DIGIT FOUR +← (‎ ੪ ‎) 0A6A GURMUKHI DIGIT FOUR +← (‎ ଃ ‎) 0B03 ORIYA SIGN VISARGA +← (‎ 𐌚 ‎) 1031A OLD ITALIC LETTER EF +← (‎ 𞣋 ‎) 1E8CB MENDE KIKAKUI DIGIT FIVE +← (‎ 🯸 ‎) 1FBF8 SEGMENTED DIGIT EIGHT +← (‎ 𝟖 ‎) 1D7D6 MATHEMATICAL BOLD DIGIT EIGHT +← (‎ 𝟠 ‎) 1D7E0 MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT +← (‎ 𝟪 ‎) 1D7EA MATHEMATICAL SANS-SERIF DIGIT EIGHT +← (‎ 𝟴 ‎) 1D7F4 MATHEMATICAL SANS-SERIF BOLD DIGIT EIGHT +← (‎ 𝟾 ‎) 1D7FE MATHEMATICAL MONOSPACE DIGIT EIGHT + +# 8, 🄉 + (‎ 8, ‎) 0038 002C DIGIT EIGHT, COMMA +← (‎ 🄉 ‎) 1F109 DIGIT EIGHT COMMA + +# 8. ⒏ + (‎ 8. ‎) 0038 002E DIGIT EIGHT, FULL STOP +← (‎ ⒏ ‎) 248F DIGIT EIGHT FULL STOP + +# 8日 ㏧ + (‎ 8日 ‎) 0038 65E5 DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏧ ‎) 33E7 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY EIGHT + +# 8月 ㋇ + (‎ 8月 ‎) 0038 6708 DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋇ ‎) 32C7 IDEOGRAPHIC TELEGRAPH SYMBOL FOR AUGUST + +# 8点 ㍠ + (‎ 8点 ‎) 0038 70B9 DIGIT EIGHT, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍠ ‎) 3360 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR EIGHT + +# 9 Ꝯ ৭ ੧ ୨ ൭ Ⳋ 𑢬 𑣌 𑣖 🯹 𝟗 𝟡 𝟫 𝟵 𝟿 + (‎ 9 ‎) 0039 DIGIT NINE +← (‎ Ꝯ ‎) A76E LATIN CAPITAL LETTER CON +← (‎ ৭ ‎) 09ED BENGALI DIGIT SEVEN +← (‎ ੧ ‎) 0A67 GURMUKHI DIGIT ONE +← (‎ ୨ ‎) 0B68 ORIYA DIGIT TWO +← (‎ ൭ ‎) 0D6D MALAYALAM DIGIT SEVEN +← (‎ Ⳋ ‎) 2CCA COPTIC CAPITAL LETTER DIALECT-P HORI +← (‎ 𑢬 ‎) 118AC WARANG CITI CAPITAL LETTER KO +← (‎ 𑣌 ‎) 118CC WARANG CITI SMALL LETTER KO +← (‎ 𑣖 ‎) 118D6 WARANG CITI SMALL LETTER AM +← (‎ 🯹 ‎) 1FBF9 SEGMENTED DIGIT NINE +← (‎ 𝟗 ‎) 1D7D7 MATHEMATICAL BOLD DIGIT NINE +← (‎ 𝟡 ‎) 1D7E1 MATHEMATICAL DOUBLE-STRUCK DIGIT NINE +← (‎ 𝟫 ‎) 1D7EB MATHEMATICAL SANS-SERIF DIGIT NINE +← (‎ 𝟵 ‎) 1D7F5 MATHEMATICAL SANS-SERIF BOLD DIGIT NINE +← (‎ 𝟿 ‎) 1D7FF MATHEMATICAL MONOSPACE DIGIT NINE + +# 9, 🄊 + (‎ 9, ‎) 0039 002C DIGIT NINE, COMMA +← (‎ 🄊 ‎) 1F10A DIGIT NINE COMMA + +# 9. ⒐ + (‎ 9. ‎) 0039 002E DIGIT NINE, FULL STOP +← (‎ ⒐ ‎) 2490 DIGIT NINE FULL STOP + +# 9日 ㏨ + (‎ 9日 ‎) 0039 65E5 DIGIT NINE, CJK UNIFIED IDEOGRAPH-65E5 +← (‎ ㏨ ‎) 33E8 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY NINE + +# 9月 ㋈ + (‎ 9月 ‎) 0039 6708 DIGIT NINE, CJK UNIFIED IDEOGRAPH-6708 +← (‎ ㋈ ‎) 32C8 IDEOGRAPHIC TELEGRAPH SYMBOL FOR SEPTEMBER + +# 9点 ㍡ + (‎ 9点 ‎) 0039 70B9 DIGIT NINE, CJK UNIFIED IDEOGRAPH-70B9 +← (‎ ㍡ ‎) 3361 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR NINE + +# : ः ઃ ː ꓽ ˸ ։ ׃ ܃ ܄ ᛬ ᠃ ᠉ ⁚ ∶ ꞉ ︰ : + (‎ : ‎) 003A COLON +← (‎ ः ‎) 0903 DEVANAGARI SIGN VISARGA +← (‎ ઃ ‎) 0A83 GUJARATI SIGN VISARGA +← (‎ ː ‎) 02D0 MODIFIER LETTER TRIANGULAR COLON +← (‎ ꓽ ‎) A4FD LISU LETTER TONE MYA JEU +← (‎ ˸ ‎) 02F8 MODIFIER LETTER RAISED COLON +← (‎ ։ ‎) 0589 ARMENIAN FULL STOP +← (‎ ׃ ‎) 05C3 HEBREW PUNCTUATION SOF PASUQ +← (‎ ܃ ‎) 0703 SYRIAC SUPRALINEAR COLON +← (‎ ܄ ‎) 0704 SYRIAC SUBLINEAR COLON +← (‎ ᛬ ‎) 16EC RUNIC MULTIPLE PUNCTUATION +← (‎ ᠃ ‎) 1803 MONGOLIAN FULL STOP +← (‎ ᠉ ‎) 1809 MONGOLIAN MANCHU FULL STOP +← (‎ ⁚ ‎) 205A TWO DOT PUNCTUATION +← (‎ ∶ ‎) 2236 RATIO +← (‎ ꞉ ‎) A789 MODIFIER LETTER COLON +← (‎ ︰ ‎) FE30 PRESENTATION FORM FOR VERTICAL TWO DOT LEADER +← (‎ : ‎) FF1A FULLWIDTH COLON # →︰→ + +# ::= ⩴ + (‎ ::= ‎) 003A 003A 003D COLON, COLON, EQUALS SIGN +← (‎ ⩴ ‎) 2A74 DOUBLE COLON EQUAL + +# :→ ⧴ + (‎ :→ ‎) 003A 2192 COLON, RIGHTWARDS ARROW +← (‎ ⧴ ‎) 29F4 RULE-DELAYED + +# ; ; + (‎ ; ‎) 003B SEMICOLON +← (‎ ; ‎) 037E GREEK QUESTION MARK + +# < ᐸ ᚲ ˂ ‹ ❮ 𝈶 + (‎ < ‎) 003C LESS-THAN SIGN +← (‎ ᐸ ‎) 1438 CANADIAN SYLLABICS PA +← (‎ ᚲ ‎) 16B2 RUNIC LETTER KAUNA +← (‎ ˂ ‎) 02C2 MODIFIER LETTER LEFT ARROWHEAD +← (‎ ‹ ‎) 2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK +← (‎ ❮ ‎) 276E HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT # →‹→ +← (‎ 𝈶 ‎) 1D236 GREEK INSTRUMENTAL NOTATION SYMBOL-40 + +# << ≪ + (‎ << ‎) 003C 003C LESS-THAN SIGN, LESS-THAN SIGN +← (‎ ≪ ‎) 226A MUCH LESS-THAN + +# <<< ⋘ + (‎ <<< ‎) 003C 003C 003C LESS-THAN SIGN, LESS-THAN SIGN, LESS-THAN SIGN +← (‎ ⋘ ‎) 22D8 VERY MUCH LESS-THAN + +# <· ᐸᐧ ᑅ Ⲵ ⋖ + (‎ <· ‎) 003C 00B7 LESS-THAN SIGN, MIDDLE DOT +← (‎ ᐸᐧ ‎) 1438 1427 CANADIAN SYLLABICS PA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑅ ‎) 1445 CANADIAN SYLLABICS WEST-CREE PWA # →ᐸᐧ→ +← (‎ Ⲵ ‎) 2CB4 COPTIC CAPITAL LETTER OLD COPTIC AIN # →ᑅ→→ᐸᐧ→ +← (‎ ⋖ ‎) 22D6 LESS-THAN WITH DOT # →ᑅ→→ᐸᐧ→ + +# = ゠ ᐀ ꓿ ⹀ + (‎ = ‎) 003D EQUALS SIGN +← (‎ ゠ ‎) 30A0 KATAKANA-HIRAGANA DOUBLE HYPHEN +← (‎ ᐀ ‎) 1400 CANADIAN SYLLABICS HYPHEN +← (‎ ꓿ ‎) A4FF LISU PUNCTUATION FULL STOP +← (‎ ⹀ ‎) 2E40 DOUBLE HYPHEN + +# == ⩵ + (‎ == ‎) 003D 003D EQUALS SIGN, EQUALS SIGN +← (‎ ⩵ ‎) 2A75 TWO CONSECUTIVE EQUALS SIGNS + +# === ⩶ + (‎ === ‎) 003D 003D 003D EQUALS SIGN, EQUALS SIGN, EQUALS SIGN +← (‎ ⩶ ‎) 2A76 THREE CONSECUTIVE EQUALS SIGNS + +# =̂ ≙ + (‎ =̂ ‎) 003D 0302 EQUALS SIGN, COMBINING CIRCUMFLEX ACCENT +← (‎ ≙ ‎) 2259 ESTIMATES + +# =̆ =̌ ≚ + (‎ =̆ ‎) 003D 0306 EQUALS SIGN, COMBINING BREVE +← (‎ =̌ ‎) 003D 030C EQUALS SIGN, COMBINING CARON +← (‎ ≚ ‎) 225A EQUIANGULAR TO # →=̌→ + +# =̇ ≐ + (‎ =̇ ‎) 003D 0307 EQUALS SIGN, COMBINING DOT ABOVE +← (‎ ≐ ‎) 2250 APPROACHES THE LIMIT + +# =̣̇ ≐̣ ≑ + (‎ =̣̇ ‎) 003D 0307 0323 EQUALS SIGN, COMBINING DOT ABOVE, COMBINING DOT BELOW +← (‎ ≐̣ ‎) 2250 0323 APPROACHES THE LIMIT, COMBINING DOT BELOW +← (‎ ≑ ‎) 2251 GEOMETRICALLY EQUAL TO # →≐̣→ + +# =̊ ≗ + (‎ =̊ ‎) 003D 030A EQUALS SIGN, COMBINING RING ABOVE +← (‎ ≗ ‎) 2257 RING EQUAL TO + +# =ͫ ≞ + (‎ =ͫ ‎) 003D 036B EQUALS SIGN, COMBINING LATIN SMALL LETTER M +← (‎ ≞ ‎) 225E MEASURED BY + +# =⃰ ⩮ + (‎ =⃰ ‎) 003D 20F0 EQUALS SIGN, COMBINING ASTERISK ABOVE +← (‎ ⩮ ‎) 2A6E EQUALS WITH ASTERISK + +# > ᐳ 𖼿 ˃ › ❯ 𝈷 + (‎ > ‎) 003E GREATER-THAN SIGN +← (‎ ᐳ ‎) 1433 CANADIAN SYLLABICS PO +← (‎ 𖼿 ‎) 16F3F MIAO LETTER ARCHAIC ZZA +← (‎ ˃ ‎) 02C3 MODIFIER LETTER RIGHT ARROWHEAD +← (‎ › ‎) 203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +← (‎ ❯ ‎) 276F HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT # →›→ +← (‎ 𝈷 ‎) 1D237 GREEK INSTRUMENTAL NOTATION SYMBOL-42 + +# >< ⪥ + (‎ >< ‎) 003E 003C GREATER-THAN SIGN, LESS-THAN SIGN +← (‎ ⪥ ‎) 2AA5 GREATER-THAN BESIDE LESS-THAN + +# >> ≫ ⨠ + (‎ >> ‎) 003E 003E GREATER-THAN SIGN, GREATER-THAN SIGN +← (‎ ≫ ‎) 226B MUCH GREATER-THAN +← (‎ ⨠ ‎) 2A20 Z NOTATION SCHEMA PIPING # →≫→ + +# >>> ⋙ + (‎ >>> ‎) 003E 003E 003E GREATER-THAN SIGN, GREATER-THAN SIGN, GREATER-THAN SIGN +← (‎ ⋙ ‎) 22D9 VERY MUCH GREATER-THAN + +# >· ᐳᐧ ᑁ + (‎ >· ‎) 003E 00B7 GREATER-THAN SIGN, MIDDLE DOT +← (‎ ᐳᐧ ‎) 1433 1427 CANADIAN SYLLABICS PO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑁ ‎) 1441 CANADIAN SYLLABICS WEST-CREE PWO # →ᐳᐧ→ + +# ? Ɂ ʔ ॽ Ꭾ ꛫ + (‎ ? ‎) 003F QUESTION MARK +← (‎ Ɂ ‎) 0241 LATIN CAPITAL LETTER GLOTTAL STOP # →ʔ→ +← (‎ ʔ ‎) 0294 LATIN LETTER GLOTTAL STOP +← (‎ ॽ ‎) 097D DEVANAGARI LETTER GLOTTAL STOP +← (‎ Ꭾ ‎) 13AE CHEROKEE LETTER HE # →Ɂ→→ʔ→ +← (‎ ꛫ ‎) A6EB BAMUM LETTER NTUU # →ʔ→ + +# ?! ⁈ + (‎ ?! ‎) 003F 0021 QUESTION MARK, EXCLAMATION MARK +← (‎ ⁈ ‎) 2048 QUESTION EXCLAMATION MARK + +# ?? ⁇ + (‎ ?? ‎) 003F 003F QUESTION MARK, QUESTION MARK +← (‎ ⁇ ‎) 2047 DOUBLE QUESTION MARK + +# A Α А Ꭺ ᗅ ꓮ 𐊠 𖽀 A 𝐀 𝐴 𝑨 𝒜 𝓐 𝔄 𝔸 𝕬 𝖠 𝗔 𝘈 𝘼 𝙰 𝚨 𝛢 𝜜 𝝖 𝞐 + (‎ A ‎) 0041 LATIN CAPITAL LETTER A +← (‎ Α ‎) 0391 GREEK CAPITAL LETTER ALPHA +← (‎ А ‎) 0410 CYRILLIC CAPITAL LETTER A +← (‎ Ꭺ ‎) 13AA CHEROKEE LETTER GO +← (‎ ᗅ ‎) 15C5 CANADIAN SYLLABICS CARRIER GHO +← (‎ ꓮ ‎) A4EE LISU LETTER A +← (‎ 𐊠 ‎) 102A0 CARIAN LETTER A +← (‎ 𖽀 ‎) 16F40 MIAO LETTER ZZYA +← (‎ A ‎) FF21 FULLWIDTH LATIN CAPITAL LETTER A # →А→ +← (‎ 𝐀 ‎) 1D400 MATHEMATICAL BOLD CAPITAL A +← (‎ 𝐴 ‎) 1D434 MATHEMATICAL ITALIC CAPITAL A +← (‎ 𝑨 ‎) 1D468 MATHEMATICAL BOLD ITALIC CAPITAL A +← (‎ 𝒜 ‎) 1D49C MATHEMATICAL SCRIPT CAPITAL A +← (‎ 𝓐 ‎) 1D4D0 MATHEMATICAL BOLD SCRIPT CAPITAL A +← (‎ 𝔄 ‎) 1D504 MATHEMATICAL FRAKTUR CAPITAL A +← (‎ 𝔸 ‎) 1D538 MATHEMATICAL DOUBLE-STRUCK CAPITAL A +← (‎ 𝕬 ‎) 1D56C MATHEMATICAL BOLD FRAKTUR CAPITAL A +← (‎ 𝖠 ‎) 1D5A0 MATHEMATICAL SANS-SERIF CAPITAL A +← (‎ 𝗔 ‎) 1D5D4 MATHEMATICAL SANS-SERIF BOLD CAPITAL A +← (‎ 𝘈 ‎) 1D608 MATHEMATICAL SANS-SERIF ITALIC CAPITAL A +← (‎ 𝘼 ‎) 1D63C MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL A +← (‎ 𝙰 ‎) 1D670 MATHEMATICAL MONOSPACE CAPITAL A +← (‎ 𝚨 ‎) 1D6A8 MATHEMATICAL BOLD CAPITAL ALPHA # →𝐀→ +← (‎ 𝛢 ‎) 1D6E2 MATHEMATICAL ITALIC CAPITAL ALPHA # →Α→ +← (‎ 𝜜 ‎) 1D71C MATHEMATICAL BOLD ITALIC CAPITAL ALPHA # →Α→ +← (‎ 𝝖 ‎) 1D756 MATHEMATICAL SANS-SERIF BOLD CAPITAL ALPHA # →Α→ +← (‎ 𝞐 ‎) 1D790 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ALPHA # →Α→ + +# AA Ꜳ + (‎ AA ‎) 0041 0041 LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER A +← (‎ Ꜳ ‎) A732 LATIN CAPITAL LETTER AA + +# AE Æ Ӕ + (‎ AE ‎) 0041 0045 LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER E +← (‎ Æ ‎) 00C6 LATIN CAPITAL LETTER AE +← (‎ Ӕ ‎) 04D4 CYRILLIC CAPITAL LIGATURE A IE # →Æ→ + +# AO Ꜵ + (‎ AO ‎) 0041 004F LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER O +← (‎ Ꜵ ‎) A734 LATIN CAPITAL LETTER AO + +# AR 🜇 + (‎ AR ‎) 0041 0052 LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER R +← (‎ 🜇 ‎) 1F707 ALCHEMICAL SYMBOL FOR AQUA REGIA-2 + +# AU Ꜷ + (‎ AU ‎) 0041 0055 LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER U +← (‎ Ꜷ ‎) A736 LATIN CAPITAL LETTER AU + +# AV Ꜹ Ꜻ + (‎ AV ‎) 0041 0056 LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER V +← (‎ Ꜹ ‎) A738 LATIN CAPITAL LETTER AV +← (‎ Ꜻ ‎) A73A LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR + +# AY Ꜽ + (‎ AY ‎) 0041 0059 LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER Y +← (‎ Ꜽ ‎) A73C LATIN CAPITAL LETTER AY + +# B Β В Ᏼ ᗷ ꓐ 𐊂 𐊡 𐌁 Ꞵ B ℬ 𝐁 𝐵 𝑩 𝓑 𝔅 𝔹 𝕭 𝖡 𝗕 𝘉 𝘽 𝙱 𝚩 𝛣 𝜝 𝝗 𝞑 + (‎ B ‎) 0042 LATIN CAPITAL LETTER B +← (‎ Β ‎) 0392 GREEK CAPITAL LETTER BETA +← (‎ В ‎) 0412 CYRILLIC CAPITAL LETTER VE +← (‎ Ᏼ ‎) 13F4 CHEROKEE LETTER YV +← (‎ ᗷ ‎) 15F7 CANADIAN SYLLABICS CARRIER KHE +← (‎ ꓐ ‎) A4D0 LISU LETTER BA +← (‎ 𐊂 ‎) 10282 LYCIAN LETTER B +← (‎ 𐊡 ‎) 102A1 CARIAN LETTER P2 +← (‎ 𐌁 ‎) 10301 OLD ITALIC LETTER BE +← (‎ Ꞵ ‎) A7B4 LATIN CAPITAL LETTER BETA +← (‎ B ‎) FF22 FULLWIDTH LATIN CAPITAL LETTER B # →Β→ +← (‎ ℬ ‎) 212C SCRIPT CAPITAL B +← (‎ 𝐁 ‎) 1D401 MATHEMATICAL BOLD CAPITAL B +← (‎ 𝐵 ‎) 1D435 MATHEMATICAL ITALIC CAPITAL B +← (‎ 𝑩 ‎) 1D469 MATHEMATICAL BOLD ITALIC CAPITAL B +← (‎ 𝓑 ‎) 1D4D1 MATHEMATICAL BOLD SCRIPT CAPITAL B +← (‎ 𝔅 ‎) 1D505 MATHEMATICAL FRAKTUR CAPITAL B +← (‎ 𝔹 ‎) 1D539 MATHEMATICAL DOUBLE-STRUCK CAPITAL B +← (‎ 𝕭 ‎) 1D56D MATHEMATICAL BOLD FRAKTUR CAPITAL B +← (‎ 𝖡 ‎) 1D5A1 MATHEMATICAL SANS-SERIF CAPITAL B +← (‎ 𝗕 ‎) 1D5D5 MATHEMATICAL SANS-SERIF BOLD CAPITAL B +← (‎ 𝘉 ‎) 1D609 MATHEMATICAL SANS-SERIF ITALIC CAPITAL B +← (‎ 𝘽 ‎) 1D63D MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL B +← (‎ 𝙱 ‎) 1D671 MATHEMATICAL MONOSPACE CAPITAL B +← (‎ 𝚩 ‎) 1D6A9 MATHEMATICAL BOLD CAPITAL BETA # →Β→ +← (‎ 𝛣 ‎) 1D6E3 MATHEMATICAL ITALIC CAPITAL BETA # →Β→ +← (‎ 𝜝 ‎) 1D71D MATHEMATICAL BOLD ITALIC CAPITAL BETA # →Β→ +← (‎ 𝝗 ‎) 1D757 MATHEMATICAL SANS-SERIF BOLD CAPITAL BETA # →Β→ +← (‎ 𝞑 ‎) 1D791 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL BETA # →Β→ + +# C С Ꮯ Ⲥ ꓚ 𐊢 𐌂 𐐕 🝌 𐔜 𑣩 𑣲 Ⅽ C ℂ ℭ Ϲ 𝐂 𝐶 𝑪 𝒞 𝓒 𝕮 𝖢 𝗖 𝘊 𝘾 𝙲 + (‎ C ‎) 0043 LATIN CAPITAL LETTER C +← (‎ С ‎) 0421 CYRILLIC CAPITAL LETTER ES +← (‎ Ꮯ ‎) 13DF CHEROKEE LETTER TLI +← (‎ Ⲥ ‎) 2CA4 COPTIC CAPITAL LETTER SIMA # →Ϲ→ +← (‎ ꓚ ‎) A4DA LISU LETTER CA +← (‎ 𐊢 ‎) 102A2 CARIAN LETTER D +← (‎ 𐌂 ‎) 10302 OLD ITALIC LETTER KE +← (‎ 𐐕 ‎) 10415 DESERET CAPITAL LETTER CHEE +← (‎ 🝌 ‎) 1F74C ALCHEMICAL SYMBOL FOR CALX +← (‎ 𐔜 ‎) 1051C ELBASAN LETTER SHE +← (‎ 𑣩 ‎) 118E9 WARANG CITI DIGIT NINE +← (‎ 𑣲 ‎) 118F2 WARANG CITI NUMBER NINETY +← (‎ Ⅽ ‎) 216D ROMAN NUMERAL ONE HUNDRED +← (‎ C ‎) FF23 FULLWIDTH LATIN CAPITAL LETTER C # →С→ +← (‎ ℂ ‎) 2102 DOUBLE-STRUCK CAPITAL C +← (‎ ℭ ‎) 212D BLACK-LETTER CAPITAL C +← (‎ Ϲ ‎) 03F9 GREEK CAPITAL LUNATE SIGMA SYMBOL +← (‎ 𝐂 ‎) 1D402 MATHEMATICAL BOLD CAPITAL C +← (‎ 𝐶 ‎) 1D436 MATHEMATICAL ITALIC CAPITAL C +← (‎ 𝑪 ‎) 1D46A MATHEMATICAL BOLD ITALIC CAPITAL C +← (‎ 𝒞 ‎) 1D49E MATHEMATICAL SCRIPT CAPITAL C +← (‎ 𝓒 ‎) 1D4D2 MATHEMATICAL BOLD SCRIPT CAPITAL C +← (‎ 𝕮 ‎) 1D56E MATHEMATICAL BOLD FRAKTUR CAPITAL C +← (‎ 𝖢 ‎) 1D5A2 MATHEMATICAL SANS-SERIF CAPITAL C +← (‎ 𝗖 ‎) 1D5D6 MATHEMATICAL SANS-SERIF BOLD CAPITAL C +← (‎ 𝘊 ‎) 1D60A MATHEMATICAL SANS-SERIF ITALIC CAPITAL C +← (‎ 𝘾 ‎) 1D63E MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL C +← (‎ 𝙲 ‎) 1D672 MATHEMATICAL MONOSPACE CAPITAL C + +# C' Cʽ Ƈ + (‎ C' ‎) 0043 0027 LATIN CAPITAL LETTER C, APOSTROPHE +← (‎ Cʽ ‎) 0043 02BD LATIN CAPITAL LETTER C, MODIFIER LETTER REVERSED COMMA +← (‎ Ƈ ‎) 0187 LATIN CAPITAL LETTER C WITH HOOK # →Cʽ→ + +# C̦ С̦ С̡ Ҫ Ç + (‎ C̦ ‎) 0043 0326 LATIN CAPITAL LETTER C, COMBINING COMMA BELOW +← (‎ С̦ ‎) 0421 0326 CYRILLIC CAPITAL LETTER ES, COMBINING COMMA BELOW # →С̡→ +← (‎ С̡ ‎) 0421 0321 CYRILLIC CAPITAL LETTER ES, COMBINING PALATALIZED HOOK BELOW +← (‎ Ҫ ‎) 04AA CYRILLIC CAPITAL LETTER ES WITH DESCENDER # →С̡→ +← (‎ Ç ‎) 00C7 LATIN CAPITAL LETTER C WITH CEDILLA # →Ҫ→→С̡→ + +# C⃠ 🅮 + (‎ C⃠ ‎) 0043 20E0 LATIN CAPITAL LETTER C, COMBINING ENCLOSING CIRCLE BACKSLASH +← (‎ 🅮 ‎) 1F16E CIRCLED C WITH OVERLAID BACKSLASH + +# C⃫ ₡ + (‎ C⃫ ‎) 0043 20EB LATIN CAPITAL LETTER C, COMBINING LONG DOUBLE SOLIDUS OVERLAY +← (‎ ₡ ‎) 20A1 COLON SIGN + +# D Ꭰ ᗞ ᗪ ꓓ Ⅾ ⅅ 𝐃 𝐷 𝑫 𝒟 𝓓 𝔇 𝔻 𝕯 𝖣 𝗗 𝘋 𝘿 𝙳 + (‎ D ‎) 0044 LATIN CAPITAL LETTER D +← (‎ Ꭰ ‎) 13A0 CHEROKEE LETTER A +← (‎ ᗞ ‎) 15DE CANADIAN SYLLABICS CARRIER THE +← (‎ ᗪ ‎) 15EA CANADIAN SYLLABICS CARRIER PE # →ᗞ→ +← (‎ ꓓ ‎) A4D3 LISU LETTER DA +← (‎ Ⅾ ‎) 216E ROMAN NUMERAL FIVE HUNDRED +← (‎ ⅅ ‎) 2145 DOUBLE-STRUCK ITALIC CAPITAL D +← (‎ 𝐃 ‎) 1D403 MATHEMATICAL BOLD CAPITAL D +← (‎ 𝐷 ‎) 1D437 MATHEMATICAL ITALIC CAPITAL D +← (‎ 𝑫 ‎) 1D46B MATHEMATICAL BOLD ITALIC CAPITAL D +← (‎ 𝒟 ‎) 1D49F MATHEMATICAL SCRIPT CAPITAL D +← (‎ 𝓓 ‎) 1D4D3 MATHEMATICAL BOLD SCRIPT CAPITAL D +← (‎ 𝔇 ‎) 1D507 MATHEMATICAL FRAKTUR CAPITAL D +← (‎ 𝔻 ‎) 1D53B MATHEMATICAL DOUBLE-STRUCK CAPITAL D +← (‎ 𝕯 ‎) 1D56F MATHEMATICAL BOLD FRAKTUR CAPITAL D +← (‎ 𝖣 ‎) 1D5A3 MATHEMATICAL SANS-SERIF CAPITAL D +← (‎ 𝗗 ‎) 1D5D7 MATHEMATICAL SANS-SERIF BOLD CAPITAL D +← (‎ 𝘋 ‎) 1D60B MATHEMATICAL SANS-SERIF ITALIC CAPITAL D +← (‎ 𝘿 ‎) 1D63F MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL D +← (‎ 𝙳 ‎) 1D673 MATHEMATICAL MONOSPACE CAPITAL D + +# DZ DZ + (‎ DZ ‎) 0044 005A LATIN CAPITAL LETTER D, LATIN CAPITAL LETTER Z +← (‎ DZ ‎) 01F1 LATIN CAPITAL LETTER DZ + +# Dz Dz + (‎ Dz ‎) 0044 007A LATIN CAPITAL LETTER D, LATIN SMALL LETTER Z +← (‎ Dz ‎) 01F2 LATIN CAPITAL LETTER D WITH SMALL LETTER Z + +# DŽ DŽ + (‎ DŽ ‎) 0044 017D LATIN CAPITAL LETTER D, LATIN CAPITAL LETTER Z WITH CARON +← (‎ DŽ ‎) 01C4 LATIN CAPITAL LETTER DZ WITH CARON + +# Dž Dž + (‎ Dž ‎) 0044 017E LATIN CAPITAL LETTER D, LATIN SMALL LETTER Z WITH CARON +← (‎ Dž ‎) 01C5 LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON + +# D̵ Ð Đ Ɖ + (‎ D̵ ‎) 0044 0335 LATIN CAPITAL LETTER D, COMBINING SHORT STROKE OVERLAY +← (‎ Ð ‎) 00D0 LATIN CAPITAL LETTER ETH # →Đ→ +← (‎ Đ ‎) 0110 LATIN CAPITAL LETTER D WITH STROKE +← (‎ Ɖ ‎) 0189 LATIN CAPITAL LETTER AFRICAN D # →Đ→ + +# E Ε Е Ꭼ ⴹ ꓰ 𐊆 ⋿ 𑢦 𑢮 E ℰ 𝐄 𝐸 𝑬 𝓔 𝔈 𝔼 𝕰 𝖤 𝗘 𝘌 𝙀 𝙴 𝚬 𝛦 𝜠 𝝚 𝞔 + (‎ E ‎) 0045 LATIN CAPITAL LETTER E +← (‎ Ε ‎) 0395 GREEK CAPITAL LETTER EPSILON +← (‎ Е ‎) 0415 CYRILLIC CAPITAL LETTER IE +← (‎ Ꭼ ‎) 13AC CHEROKEE LETTER GV +← (‎ ⴹ ‎) 2D39 TIFINAGH LETTER YADD +← (‎ ꓰ ‎) A4F0 LISU LETTER E +← (‎ 𐊆 ‎) 10286 LYCIAN LETTER I +← (‎ ⋿ ‎) 22FF Z NOTATION BAG MEMBERSHIP +← (‎ 𑢦 ‎) 118A6 WARANG CITI CAPITAL LETTER II +← (‎ 𑢮 ‎) 118AE WARANG CITI CAPITAL LETTER YUJ +← (‎ E ‎) FF25 FULLWIDTH LATIN CAPITAL LETTER E # →Ε→ +← (‎ ℰ ‎) 2130 SCRIPT CAPITAL E +← (‎ 𝐄 ‎) 1D404 MATHEMATICAL BOLD CAPITAL E +← (‎ 𝐸 ‎) 1D438 MATHEMATICAL ITALIC CAPITAL E +← (‎ 𝑬 ‎) 1D46C MATHEMATICAL BOLD ITALIC CAPITAL E +← (‎ 𝓔 ‎) 1D4D4 MATHEMATICAL BOLD SCRIPT CAPITAL E +← (‎ 𝔈 ‎) 1D508 MATHEMATICAL FRAKTUR CAPITAL E +← (‎ 𝔼 ‎) 1D53C MATHEMATICAL DOUBLE-STRUCK CAPITAL E +← (‎ 𝕰 ‎) 1D570 MATHEMATICAL BOLD FRAKTUR CAPITAL E +← (‎ 𝖤 ‎) 1D5A4 MATHEMATICAL SANS-SERIF CAPITAL E +← (‎ 𝗘 ‎) 1D5D8 MATHEMATICAL SANS-SERIF BOLD CAPITAL E +← (‎ 𝘌 ‎) 1D60C MATHEMATICAL SANS-SERIF ITALIC CAPITAL E +← (‎ 𝙀 ‎) 1D640 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL E +← (‎ 𝙴 ‎) 1D674 MATHEMATICAL MONOSPACE CAPITAL E +← (‎ 𝚬 ‎) 1D6AC MATHEMATICAL BOLD CAPITAL EPSILON # →𝐄→ +← (‎ 𝛦 ‎) 1D6E6 MATHEMATICAL ITALIC CAPITAL EPSILON # →Ε→ +← (‎ 𝜠 ‎) 1D720 MATHEMATICAL BOLD ITALIC CAPITAL EPSILON # →Ε→ +← (‎ 𝝚 ‎) 1D75A MATHEMATICAL SANS-SERIF BOLD CAPITAL EPSILON # →Ε→ +← (‎ 𝞔 ‎) 1D794 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL EPSILON # →Ε→ + +# E̸ Ɇ + (‎ E̸ ‎) 0045 0338 LATIN CAPITAL LETTER E, COMBINING LONG SOLIDUS OVERLAY +← (‎ Ɇ ‎) 0246 LATIN CAPITAL LETTER E WITH STROKE + +# F Ϝ ᖴ ꓝ 𐊇 𐊥 𝈓 Ꞙ 𐔥 𑢢 𑣂 ℱ 𝐅 𝐹 𝑭 𝓕 𝔉 𝔽 𝕱 𝖥 𝗙 𝘍 𝙁 𝙵 𝟊 + (‎ F ‎) 0046 LATIN CAPITAL LETTER F +← (‎ Ϝ ‎) 03DC GREEK LETTER DIGAMMA +← (‎ ᖴ ‎) 15B4 CANADIAN SYLLABICS BLACKFOOT WE +← (‎ ꓝ ‎) A4DD LISU LETTER TSA +← (‎ 𐊇 ‎) 10287 LYCIAN LETTER W +← (‎ 𐊥 ‎) 102A5 CARIAN LETTER R +← (‎ 𝈓 ‎) 1D213 GREEK VOCAL NOTATION SYMBOL-20 # →Ϝ→ +← (‎ Ꞙ ‎) A798 LATIN CAPITAL LETTER F WITH STROKE +← (‎ 𐔥 ‎) 10525 ELBASAN LETTER GHE +← (‎ 𑢢 ‎) 118A2 WARANG CITI CAPITAL LETTER WI +← (‎ 𑣂 ‎) 118C2 WARANG CITI SMALL LETTER WI +← (‎ ℱ ‎) 2131 SCRIPT CAPITAL F +← (‎ 𝐅 ‎) 1D405 MATHEMATICAL BOLD CAPITAL F +← (‎ 𝐹 ‎) 1D439 MATHEMATICAL ITALIC CAPITAL F +← (‎ 𝑭 ‎) 1D46D MATHEMATICAL BOLD ITALIC CAPITAL F +← (‎ 𝓕 ‎) 1D4D5 MATHEMATICAL BOLD SCRIPT CAPITAL F +← (‎ 𝔉 ‎) 1D509 MATHEMATICAL FRAKTUR CAPITAL F +← (‎ 𝔽 ‎) 1D53D MATHEMATICAL DOUBLE-STRUCK CAPITAL F +← (‎ 𝕱 ‎) 1D571 MATHEMATICAL BOLD FRAKTUR CAPITAL F +← (‎ 𝖥 ‎) 1D5A5 MATHEMATICAL SANS-SERIF CAPITAL F +← (‎ 𝗙 ‎) 1D5D9 MATHEMATICAL SANS-SERIF BOLD CAPITAL F +← (‎ 𝘍 ‎) 1D60D MATHEMATICAL SANS-SERIF ITALIC CAPITAL F +← (‎ 𝙁 ‎) 1D641 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL F +← (‎ 𝙵 ‎) 1D675 MATHEMATICAL MONOSPACE CAPITAL F +← (‎ 𝟊 ‎) 1D7CA MATHEMATICAL BOLD CAPITAL DIGAMMA # →Ϝ→ + +# FAX ℻ + (‎ FAX ‎) 0046 0041 0058 LATIN CAPITAL LETTER F, LATIN CAPITAL LETTER A, LATIN CAPITAL LETTER X +← (‎ ℻ ‎) 213B FACSIMILE SIGN + +# F̦ F̡ Ƒ + (‎ F̡ ‎) 0046 0321 LATIN CAPITAL LETTER F, COMBINING PALATALIZED HOOK BELOW +← (‎ F̦ ‎) 0046 0326 LATIN CAPITAL LETTER F, COMBINING COMMA BELOW +← (‎ Ƒ ‎) 0191 LATIN CAPITAL LETTER F WITH HOOK + +# G Ԍ Ꮐ Ᏻ ꓖ 𝐆 𝐺 𝑮 𝒢 𝓖 𝔊 𝔾 𝕲 𝖦 𝗚 𝘎 𝙂 𝙶 + (‎ G ‎) 0047 LATIN CAPITAL LETTER G +← (‎ Ԍ ‎) 050C CYRILLIC CAPITAL LETTER KOMI SJE +← (‎ Ꮐ ‎) 13C0 CHEROKEE LETTER NAH +← (‎ Ᏻ ‎) 13F3 CHEROKEE LETTER YU +← (‎ ꓖ ‎) A4D6 LISU LETTER GA +← (‎ 𝐆 ‎) 1D406 MATHEMATICAL BOLD CAPITAL G +← (‎ 𝐺 ‎) 1D43A MATHEMATICAL ITALIC CAPITAL G +← (‎ 𝑮 ‎) 1D46E MATHEMATICAL BOLD ITALIC CAPITAL G +← (‎ 𝒢 ‎) 1D4A2 MATHEMATICAL SCRIPT CAPITAL G +← (‎ 𝓖 ‎) 1D4D6 MATHEMATICAL BOLD SCRIPT CAPITAL G +← (‎ 𝔊 ‎) 1D50A MATHEMATICAL FRAKTUR CAPITAL G +← (‎ 𝔾 ‎) 1D53E MATHEMATICAL DOUBLE-STRUCK CAPITAL G +← (‎ 𝕲 ‎) 1D572 MATHEMATICAL BOLD FRAKTUR CAPITAL G +← (‎ 𝖦 ‎) 1D5A6 MATHEMATICAL SANS-SERIF CAPITAL G +← (‎ 𝗚 ‎) 1D5DA MATHEMATICAL SANS-SERIF BOLD CAPITAL G +← (‎ 𝘎 ‎) 1D60E MATHEMATICAL SANS-SERIF ITALIC CAPITAL G +← (‎ 𝙂 ‎) 1D642 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL G +← (‎ 𝙶 ‎) 1D676 MATHEMATICAL MONOSPACE CAPITAL G + +# G' Gʽ Ɠ + (‎ G' ‎) 0047 0027 LATIN CAPITAL LETTER G, APOSTROPHE +← (‎ Gʽ ‎) 0047 02BD LATIN CAPITAL LETTER G, MODIFIER LETTER REVERSED COMMA +← (‎ Ɠ ‎) 0193 LATIN CAPITAL LETTER G WITH HOOK # →Gʽ→ + +# G̵ Ǥ + (‎ G̵ ‎) 0047 0335 LATIN CAPITAL LETTER G, COMBINING SHORT STROKE OVERLAY +← (‎ Ǥ ‎) 01E4 LATIN CAPITAL LETTER G WITH STROKE + +# H Η Н Ꮋ ᕼ Ⲏ ꓧ 𐋏 H ℋ ℌ ℍ 𝐇 𝐻 𝑯 𝓗 𝕳 𝖧 𝗛 𝘏 𝙃 𝙷 𝚮 𝛨 𝜢 𝝜 𝞖 + (‎ H ‎) 0048 LATIN CAPITAL LETTER H +← (‎ Η ‎) 0397 GREEK CAPITAL LETTER ETA +← (‎ Н ‎) 041D CYRILLIC CAPITAL LETTER EN +← (‎ Ꮋ ‎) 13BB CHEROKEE LETTER MI +← (‎ ᕼ ‎) 157C CANADIAN SYLLABICS NUNAVUT H +← (‎ Ⲏ ‎) 2C8E COPTIC CAPITAL LETTER HATE # →Η→ +← (‎ ꓧ ‎) A4E7 LISU LETTER XA +← (‎ 𐋏 ‎) 102CF CARIAN LETTER E2 +← (‎ H ‎) FF28 FULLWIDTH LATIN CAPITAL LETTER H # →Η→ +← (‎ ℋ ‎) 210B SCRIPT CAPITAL H +← (‎ ℌ ‎) 210C BLACK-LETTER CAPITAL H +← (‎ ℍ ‎) 210D DOUBLE-STRUCK CAPITAL H +← (‎ 𝐇 ‎) 1D407 MATHEMATICAL BOLD CAPITAL H +← (‎ 𝐻 ‎) 1D43B MATHEMATICAL ITALIC CAPITAL H +← (‎ 𝑯 ‎) 1D46F MATHEMATICAL BOLD ITALIC CAPITAL H +← (‎ 𝓗 ‎) 1D4D7 MATHEMATICAL BOLD SCRIPT CAPITAL H +← (‎ 𝕳 ‎) 1D573 MATHEMATICAL BOLD FRAKTUR CAPITAL H +← (‎ 𝖧 ‎) 1D5A7 MATHEMATICAL SANS-SERIF CAPITAL H +← (‎ 𝗛 ‎) 1D5DB MATHEMATICAL SANS-SERIF BOLD CAPITAL H +← (‎ 𝘏 ‎) 1D60F MATHEMATICAL SANS-SERIF ITALIC CAPITAL H +← (‎ 𝙃 ‎) 1D643 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL H +← (‎ 𝙷 ‎) 1D677 MATHEMATICAL MONOSPACE CAPITAL H +← (‎ 𝚮 ‎) 1D6AE MATHEMATICAL BOLD CAPITAL ETA # →Η→ +← (‎ 𝛨 ‎) 1D6E8 MATHEMATICAL ITALIC CAPITAL ETA # →Η→ +← (‎ 𝜢 ‎) 1D722 MATHEMATICAL BOLD ITALIC CAPITAL ETA # →𝑯→ +← (‎ 𝝜 ‎) 1D75C MATHEMATICAL SANS-SERIF BOLD CAPITAL ETA # →Η→ +← (‎ 𝞖 ‎) 1D796 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ETA # →Η→ + +# H̦ Н̦ Н̡ Ӈ Ӊ + (‎ H̦ ‎) 0048 0326 LATIN CAPITAL LETTER H, COMBINING COMMA BELOW +← (‎ Н̦ ‎) 041D 0326 CYRILLIC CAPITAL LETTER EN, COMBINING COMMA BELOW # →Н̡→ +← (‎ Н̡ ‎) 041D 0321 CYRILLIC CAPITAL LETTER EN, COMBINING PALATALIZED HOOK BELOW +← (‎ Ӈ ‎) 04C7 CYRILLIC CAPITAL LETTER EN WITH HOOK # →Н̡→ +← (‎ Ӊ ‎) 04C9 CYRILLIC CAPITAL LETTER EN WITH TAIL # →Н̡→ + +# H̩ Н̩ Ⱨ Ң + (‎ H̩ ‎) 0048 0329 LATIN CAPITAL LETTER H, COMBINING VERTICAL LINE BELOW +← (‎ Н̩ ‎) 041D 0329 CYRILLIC CAPITAL LETTER EN, COMBINING VERTICAL LINE BELOW +← (‎ Ⱨ ‎) 2C67 LATIN CAPITAL LETTER H WITH DESCENDER # →Ң→→Н̩→ +← (‎ Ң ‎) 04A2 CYRILLIC CAPITAL LETTER EN WITH DESCENDER # →Н̩→ + +# H̵ Ħ + (‎ H̵ ‎) 0048 0335 LATIN CAPITAL LETTER H, COMBINING SHORT STROKE OVERLAY +← (‎ Ħ ‎) 0126 LATIN CAPITAL LETTER H WITH STROKE + +# lll III Ⅲ + (‎ III ‎) 0049 0049 0049 LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I +← (‎ lll ‎) 006C 006C 006C LATIN SMALL LETTER L, LATIN SMALL LETTER L, LATIN SMALL LETTER L +← (‎ Ⅲ ‎) 2162 ROMAN NUMERAL THREE + +# lJ IJ IJ + (‎ IJ ‎) 0049 004A LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER J +← (‎ lJ ‎) 006C 004A LATIN SMALL LETTER L, LATIN CAPITAL LETTER J +← (‎ IJ ‎) 0132 LATIN CAPITAL LIGATURE IJ + +# lO IO Ю + (‎ IO ‎) 0049 004F LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER O +← (‎ lO ‎) 006C 004F LATIN SMALL LETTER L, LATIN CAPITAL LETTER O +← (‎ Ю ‎) 042E CYRILLIC CAPITAL LETTER YU + +# lV IV Ⅳ + (‎ IV ‎) 0049 0056 LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER V +← (‎ lV ‎) 006C 0056 LATIN SMALL LETTER L, LATIN CAPITAL LETTER V +← (‎ Ⅳ ‎) 2163 ROMAN NUMERAL FOUR + +# lX IX Ⅸ + (‎ IX ‎) 0049 0058 LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER X +← (‎ lX ‎) 006C 0058 LATIN SMALL LETTER L, LATIN CAPITAL LETTER X +← (‎ Ⅸ ‎) 2168 ROMAN NUMERAL NINE + +# l̵ I̵ Ɨ ƚ + (‎ I̵ ‎) 0049 0335 LATIN CAPITAL LETTER I, COMBINING SHORT STROKE OVERLAY +← (‎ l̵ ‎) 006C 0335 LATIN SMALL LETTER L, COMBINING SHORT STROKE OVERLAY +← (‎ Ɨ ‎) 0197 LATIN CAPITAL LETTER I WITH STROKE +← (‎ ƚ ‎) 019A LATIN SMALL LETTER L WITH BAR # →Ɨ→ + +# l̵l̵ I̵I̵ I̶I̶ 𐆙 + (‎ I̵I̵ ‎) 0049 0335 0049 0335 LATIN CAPITAL LETTER I, COMBINING SHORT STROKE OVERLAY, LATIN CAPITAL LETTER I, COMBINING SHORT STROKE OVERLAY +← (‎ l̵l̵ ‎) 006C 0335 006C 0335 LATIN SMALL LETTER L, COMBINING SHORT STROKE OVERLAY, LATIN SMALL LETTER L, COMBINING SHORT STROKE OVERLAY # →I̶I̶→ +← (‎ I̶I̶ ‎) 0049 0336 0049 0336 LATIN CAPITAL LETTER I, COMBINING LONG STROKE OVERLAY, LATIN CAPITAL LETTER I, COMBINING LONG STROKE OVERLAY +← (‎ 𐆙 ‎) 10199 ROMAN DUPONDIUS SIGN # →I̶I̶→ + +# l̵l̵S̵ I̵I̵S̵ I̶I̶S̶ 𐆘 + (‎ I̵I̵S̵ ‎) 0049 0335 0049 0335 0053 0335 LATIN CAPITAL LETTER I, COMBINING SHORT STROKE OVERLAY, LATIN CAPITAL LETTER I, COMBINING SHORT STROKE OVERLAY, LATIN CAPITAL LETTER S, COMBINING SHORT STROKE OVERLAY +← (‎ l̵l̵S̵ ‎) 006C 0335 006C 0335 0053 0335 LATIN SMALL LETTER L, COMBINING SHORT STROKE OVERLAY, LATIN SMALL LETTER L, COMBINING SHORT STROKE OVERLAY, LATIN CAPITAL LETTER S, COMBINING SHORT STROKE OVERLAY # →I̶I̶S̶→ +← (‎ I̶I̶S̶ ‎) 0049 0336 0049 0336 0053 0336 LATIN CAPITAL LETTER I, COMBINING LONG STROKE OVERLAY, LATIN CAPITAL LETTER I, COMBINING LONG STROKE OVERLAY, LATIN CAPITAL LETTER S, COMBINING LONG STROKE OVERLAY +← (‎ 𐆘 ‎) 10198 ROMAN SESTERTIUS SIGN # →I̶I̶S̶→ + +# J Ј Ꭻ ᒍ ꓙ Ϳ Ʝ J 𝐉 𝐽 𝑱 𝒥 𝓙 𝔍 𝕁 𝕵 𝖩 𝗝 𝘑 𝙅 𝙹 + (‎ J ‎) 004A LATIN CAPITAL LETTER J +← (‎ Ј ‎) 0408 CYRILLIC CAPITAL LETTER JE +← (‎ Ꭻ ‎) 13AB CHEROKEE LETTER GU +← (‎ ᒍ ‎) 148D CANADIAN SYLLABICS CO +← (‎ ꓙ ‎) A4D9 LISU LETTER JA +← (‎ Ϳ ‎) 037F GREEK CAPITAL LETTER YOT +← (‎ Ʝ ‎) A7B2 LATIN CAPITAL LETTER J WITH CROSSED-TAIL +← (‎ J ‎) FF2A FULLWIDTH LATIN CAPITAL LETTER J # →Ј→ +← (‎ 𝐉 ‎) 1D409 MATHEMATICAL BOLD CAPITAL J +← (‎ 𝐽 ‎) 1D43D MATHEMATICAL ITALIC CAPITAL J +← (‎ 𝑱 ‎) 1D471 MATHEMATICAL BOLD ITALIC CAPITAL J +← (‎ 𝒥 ‎) 1D4A5 MATHEMATICAL SCRIPT CAPITAL J +← (‎ 𝓙 ‎) 1D4D9 MATHEMATICAL BOLD SCRIPT CAPITAL J +← (‎ 𝔍 ‎) 1D50D MATHEMATICAL FRAKTUR CAPITAL J +← (‎ 𝕁 ‎) 1D541 MATHEMATICAL DOUBLE-STRUCK CAPITAL J +← (‎ 𝕵 ‎) 1D575 MATHEMATICAL BOLD FRAKTUR CAPITAL J +← (‎ 𝖩 ‎) 1D5A9 MATHEMATICAL SANS-SERIF CAPITAL J +← (‎ 𝗝 ‎) 1D5DD MATHEMATICAL SANS-SERIF BOLD CAPITAL J +← (‎ 𝘑 ‎) 1D611 MATHEMATICAL SANS-SERIF ITALIC CAPITAL J +← (‎ 𝙅 ‎) 1D645 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL J +← (‎ 𝙹 ‎) 1D679 MATHEMATICAL MONOSPACE CAPITAL J + +# J· Ꭻ· ᒍ· ᒍᐧ ᒙ + (‎ J· ‎) 004A 00B7 LATIN CAPITAL LETTER J, MIDDLE DOT +← (‎ Ꭻ· ‎) 13AB 00B7 CHEROKEE LETTER GU, MIDDLE DOT # →ᒍᐧ→ +← (‎ ᒍ· ‎) 148D 00B7 CANADIAN SYLLABICS CO, MIDDLE DOT # →ᒍᐧ→ +← (‎ ᒍᐧ ‎) 148D 1427 CANADIAN SYLLABICS CO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒙ ‎) 1499 CANADIAN SYLLABICS WEST-CREE CWO # →ᒍᐧ→ + +# J̵ Ɉ + (‎ J̵ ‎) 004A 0335 LATIN CAPITAL LETTER J, COMBINING SHORT STROKE OVERLAY +← (‎ Ɉ ‎) 0248 LATIN CAPITAL LETTER J WITH STROKE + +# K Κ К Ꮶ ᛕ Ⲕ ꓗ 𐔘 K K 𝐊 𝐾 𝑲 𝒦 𝓚 𝔎 𝕂 𝕶 𝖪 𝗞 𝘒 𝙆 𝙺 𝚱 𝛫 𝜥 𝝟 𝞙 + (‎ K ‎) 004B LATIN CAPITAL LETTER K +← (‎ Κ ‎) 039A GREEK CAPITAL LETTER KAPPA +← (‎ К ‎) 041A CYRILLIC CAPITAL LETTER KA +← (‎ Ꮶ ‎) 13E6 CHEROKEE LETTER TSO +← (‎ ᛕ ‎) 16D5 RUNIC LETTER OPEN-P +← (‎ Ⲕ ‎) 2C94 COPTIC CAPITAL LETTER KAPA # →Κ→ +← (‎ ꓗ ‎) A4D7 LISU LETTER KA +← (‎ 𐔘 ‎) 10518 ELBASAN LETTER QE +← (‎ K ‎) 212A KELVIN SIGN +← (‎ K ‎) FF2B FULLWIDTH LATIN CAPITAL LETTER K # →Κ→ +← (‎ 𝐊 ‎) 1D40A MATHEMATICAL BOLD CAPITAL K +← (‎ 𝐾 ‎) 1D43E MATHEMATICAL ITALIC CAPITAL K +← (‎ 𝑲 ‎) 1D472 MATHEMATICAL BOLD ITALIC CAPITAL K +← (‎ 𝒦 ‎) 1D4A6 MATHEMATICAL SCRIPT CAPITAL K +← (‎ 𝓚 ‎) 1D4DA MATHEMATICAL BOLD SCRIPT CAPITAL K +← (‎ 𝔎 ‎) 1D50E MATHEMATICAL FRAKTUR CAPITAL K +← (‎ 𝕂 ‎) 1D542 MATHEMATICAL DOUBLE-STRUCK CAPITAL K +← (‎ 𝕶 ‎) 1D576 MATHEMATICAL BOLD FRAKTUR CAPITAL K +← (‎ 𝖪 ‎) 1D5AA MATHEMATICAL SANS-SERIF CAPITAL K +← (‎ 𝗞 ‎) 1D5DE MATHEMATICAL SANS-SERIF BOLD CAPITAL K +← (‎ 𝘒 ‎) 1D612 MATHEMATICAL SANS-SERIF ITALIC CAPITAL K +← (‎ 𝙆 ‎) 1D646 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL K +← (‎ 𝙺 ‎) 1D67A MATHEMATICAL MONOSPACE CAPITAL K +← (‎ 𝚱 ‎) 1D6B1 MATHEMATICAL BOLD CAPITAL KAPPA # →Κ→ +← (‎ 𝛫 ‎) 1D6EB MATHEMATICAL ITALIC CAPITAL KAPPA # →𝐾→ +← (‎ 𝜥 ‎) 1D725 MATHEMATICAL BOLD ITALIC CAPITAL KAPPA # →𝑲→ +← (‎ 𝝟 ‎) 1D75F MATHEMATICAL SANS-SERIF BOLD CAPITAL KAPPA # →Κ→ +← (‎ 𝞙 ‎) 1D799 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL KAPPA # →Κ→ + +# K' Kʽ Ƙ + (‎ K' ‎) 004B 0027 LATIN CAPITAL LETTER K, APOSTROPHE +← (‎ Kʽ ‎) 004B 02BD LATIN CAPITAL LETTER K, MODIFIER LETTER REVERSED COMMA +← (‎ Ƙ ‎) 0198 LATIN CAPITAL LETTER K WITH HOOK # →Kʽ→ + +# K̩ К̩ Ⱪ Қ + (‎ K̩ ‎) 004B 0329 LATIN CAPITAL LETTER K, COMBINING VERTICAL LINE BELOW +← (‎ К̩ ‎) 041A 0329 CYRILLIC CAPITAL LETTER KA, COMBINING VERTICAL LINE BELOW +← (‎ Ⱪ ‎) 2C69 LATIN CAPITAL LETTER K WITH DESCENDER # →Қ→→К̩→ +← (‎ Қ ‎) 049A CYRILLIC CAPITAL LETTER KA WITH DESCENDER # →К̩→ + +# K̵ K̶ К̵ Ꝁ Ҟ ₭ + (‎ K̵ ‎) 004B 0335 LATIN CAPITAL LETTER K, COMBINING SHORT STROKE OVERLAY +← (‎ K̶ ‎) 004B 0336 LATIN CAPITAL LETTER K, COMBINING LONG STROKE OVERLAY +← (‎ К̵ ‎) 041A 0335 CYRILLIC CAPITAL LETTER KA, COMBINING SHORT STROKE OVERLAY +← (‎ Ꝁ ‎) A740 LATIN CAPITAL LETTER K WITH STROKE # →Ҟ→→К̵→ +← (‎ Ҟ ‎) 049E CYRILLIC CAPITAL LETTER KA WITH STROKE # →К̵→ +← (‎ ₭ ‎) 20AD KIP SIGN # →K̶→ + +# L Ꮮ ᒪ Ⳑ ꓡ 𐐛 𖼖 𝈪 𐔦 𑢣 𑢲 Ⅼ ℒ 𝐋 𝐿 𝑳 𝓛 𝔏 𝕃 𝕷 𝖫 𝗟 𝘓 𝙇 𝙻 + (‎ L ‎) 004C LATIN CAPITAL LETTER L +← (‎ Ꮮ ‎) 13DE CHEROKEE LETTER TLE +← (‎ ᒪ ‎) 14AA CANADIAN SYLLABICS MA +← (‎ Ⳑ ‎) 2CD0 COPTIC CAPITAL LETTER L-SHAPED HA +← (‎ ꓡ ‎) A4E1 LISU LETTER LA +← (‎ 𐐛 ‎) 1041B DESERET CAPITAL LETTER ETH +← (‎ 𖼖 ‎) 16F16 MIAO LETTER LA +← (‎ 𝈪 ‎) 1D22A GREEK INSTRUMENTAL NOTATION SYMBOL-23 +← (‎ 𐔦 ‎) 10526 ELBASAN LETTER GHAMMA +← (‎ 𑢣 ‎) 118A3 WARANG CITI CAPITAL LETTER YU +← (‎ 𑢲 ‎) 118B2 WARANG CITI CAPITAL LETTER TTE +← (‎ Ⅼ ‎) 216C ROMAN NUMERAL FIFTY +← (‎ ℒ ‎) 2112 SCRIPT CAPITAL L +← (‎ 𝐋 ‎) 1D40B MATHEMATICAL BOLD CAPITAL L +← (‎ 𝐿 ‎) 1D43F MATHEMATICAL ITALIC CAPITAL L +← (‎ 𝑳 ‎) 1D473 MATHEMATICAL BOLD ITALIC CAPITAL L +← (‎ 𝓛 ‎) 1D4DB MATHEMATICAL BOLD SCRIPT CAPITAL L +← (‎ 𝔏 ‎) 1D50F MATHEMATICAL FRAKTUR CAPITAL L +← (‎ 𝕃 ‎) 1D543 MATHEMATICAL DOUBLE-STRUCK CAPITAL L +← (‎ 𝕷 ‎) 1D577 MATHEMATICAL BOLD FRAKTUR CAPITAL L +← (‎ 𝖫 ‎) 1D5AB MATHEMATICAL SANS-SERIF CAPITAL L +← (‎ 𝗟 ‎) 1D5DF MATHEMATICAL SANS-SERIF BOLD CAPITAL L +← (‎ 𝘓 ‎) 1D613 MATHEMATICAL SANS-SERIF ITALIC CAPITAL L +← (‎ 𝙇 ‎) 1D647 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL L +← (‎ 𝙻 ‎) 1D67B MATHEMATICAL MONOSPACE CAPITAL L + +# LJ LJ + (‎ LJ ‎) 004C 004A LATIN CAPITAL LETTER L, LATIN CAPITAL LETTER J +← (‎ LJ ‎) 01C7 LATIN CAPITAL LETTER LJ + +# Lj Lj + (‎ Lj ‎) 004C 006A LATIN CAPITAL LETTER L, LATIN SMALL LETTER J +← (‎ Lj ‎) 01C8 LATIN CAPITAL LETTER L WITH SMALL LETTER J + +# L̸ L̷ Ł + (‎ L̷ ‎) 004C 0337 LATIN CAPITAL LETTER L, COMBINING SHORT SOLIDUS OVERLAY +← (‎ L̸ ‎) 004C 0338 LATIN CAPITAL LETTER L, COMBINING LONG SOLIDUS OVERLAY +← (‎ Ł ‎) 0141 LATIN CAPITAL LETTER L WITH STROKE + +# M Μ М Ϻ Ꮇ ᗰ ᛖ Ⲙ ꓟ 𐊰 𐌑 Ⅿ M ℳ 𝐌 𝑀 𝑴 𝓜 𝔐 𝕄 𝕸 𝖬 𝗠 𝘔 𝙈 𝙼 𝚳 𝛭 𝜧 𝝡 𝞛 + (‎ M ‎) 004D LATIN CAPITAL LETTER M +← (‎ Μ ‎) 039C GREEK CAPITAL LETTER MU +← (‎ М ‎) 041C CYRILLIC CAPITAL LETTER EM +← (‎ Ϻ ‎) 03FA GREEK CAPITAL LETTER SAN +← (‎ Ꮇ ‎) 13B7 CHEROKEE LETTER LU +← (‎ ᗰ ‎) 15F0 CANADIAN SYLLABICS CARRIER GO +← (‎ ᛖ ‎) 16D6 RUNIC LETTER EHWAZ EH E +← (‎ Ⲙ ‎) 2C98 COPTIC CAPITAL LETTER MI +← (‎ ꓟ ‎) A4DF LISU LETTER MA +← (‎ 𐊰 ‎) 102B0 CARIAN LETTER S +← (‎ 𐌑 ‎) 10311 OLD ITALIC LETTER SHE +← (‎ Ⅿ ‎) 216F ROMAN NUMERAL ONE THOUSAND +← (‎ M ‎) FF2D FULLWIDTH LATIN CAPITAL LETTER M # →Μ→ +← (‎ ℳ ‎) 2133 SCRIPT CAPITAL M +← (‎ 𝐌 ‎) 1D40C MATHEMATICAL BOLD CAPITAL M +← (‎ 𝑀 ‎) 1D440 MATHEMATICAL ITALIC CAPITAL M +← (‎ 𝑴 ‎) 1D474 MATHEMATICAL BOLD ITALIC CAPITAL M +← (‎ 𝓜 ‎) 1D4DC MATHEMATICAL BOLD SCRIPT CAPITAL M +← (‎ 𝔐 ‎) 1D510 MATHEMATICAL FRAKTUR CAPITAL M +← (‎ 𝕄 ‎) 1D544 MATHEMATICAL DOUBLE-STRUCK CAPITAL M +← (‎ 𝕸 ‎) 1D578 MATHEMATICAL BOLD FRAKTUR CAPITAL M +← (‎ 𝖬 ‎) 1D5AC MATHEMATICAL SANS-SERIF CAPITAL M +← (‎ 𝗠 ‎) 1D5E0 MATHEMATICAL SANS-SERIF BOLD CAPITAL M +← (‎ 𝘔 ‎) 1D614 MATHEMATICAL SANS-SERIF ITALIC CAPITAL M +← (‎ 𝙈 ‎) 1D648 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL M +← (‎ 𝙼 ‎) 1D67C MATHEMATICAL MONOSPACE CAPITAL M +← (‎ 𝚳 ‎) 1D6B3 MATHEMATICAL BOLD CAPITAL MU # →𝐌→ +← (‎ 𝛭 ‎) 1D6ED MATHEMATICAL ITALIC CAPITAL MU # →𝑀→ +← (‎ 𝜧 ‎) 1D727 MATHEMATICAL BOLD ITALIC CAPITAL MU # →𝑴→ +← (‎ 𝝡 ‎) 1D761 MATHEMATICAL SANS-SERIF BOLD CAPITAL MU # →Μ→ +← (‎ 𝞛 ‎) 1D79B MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL MU # →Μ→ + +# MB 🝫 + (‎ MB ‎) 004D 0042 LATIN CAPITAL LETTER M, LATIN CAPITAL LETTER B +← (‎ 🝫 ‎) 1F76B ALCHEMICAL SYMBOL FOR BATH OF MARY + +# M̦ М̦ М̡ Ӎ + (‎ M̦ ‎) 004D 0326 LATIN CAPITAL LETTER M, COMBINING COMMA BELOW +← (‎ М̦ ‎) 041C 0326 CYRILLIC CAPITAL LETTER EM, COMBINING COMMA BELOW # →М̡→ +← (‎ М̡ ‎) 041C 0321 CYRILLIC CAPITAL LETTER EM, COMBINING PALATALIZED HOOK BELOW +← (‎ Ӎ ‎) 04CD CYRILLIC CAPITAL LETTER EM WITH TAIL # →М̡→ + +# N Ν Ⲛ ꓠ 𐔓 N ℕ 𝐍 𝑁 𝑵 𝒩 𝓝 𝔑 𝕹 𝖭 𝗡 𝘕 𝙉 𝙽 𝚴 𝛮 𝜨 𝝢 𝞜 + (‎ N ‎) 004E LATIN CAPITAL LETTER N +← (‎ Ν ‎) 039D GREEK CAPITAL LETTER NU +← (‎ Ⲛ ‎) 2C9A COPTIC CAPITAL LETTER NI +← (‎ ꓠ ‎) A4E0 LISU LETTER NA +← (‎ 𐔓 ‎) 10513 ELBASAN LETTER NE +← (‎ N ‎) FF2E FULLWIDTH LATIN CAPITAL LETTER N # →Ν→ +← (‎ ℕ ‎) 2115 DOUBLE-STRUCK CAPITAL N +← (‎ 𝐍 ‎) 1D40D MATHEMATICAL BOLD CAPITAL N +← (‎ 𝑁 ‎) 1D441 MATHEMATICAL ITALIC CAPITAL N +← (‎ 𝑵 ‎) 1D475 MATHEMATICAL BOLD ITALIC CAPITAL N +← (‎ 𝒩 ‎) 1D4A9 MATHEMATICAL SCRIPT CAPITAL N +← (‎ 𝓝 ‎) 1D4DD MATHEMATICAL BOLD SCRIPT CAPITAL N +← (‎ 𝔑 ‎) 1D511 MATHEMATICAL FRAKTUR CAPITAL N +← (‎ 𝕹 ‎) 1D579 MATHEMATICAL BOLD FRAKTUR CAPITAL N +← (‎ 𝖭 ‎) 1D5AD MATHEMATICAL SANS-SERIF CAPITAL N +← (‎ 𝗡 ‎) 1D5E1 MATHEMATICAL SANS-SERIF BOLD CAPITAL N +← (‎ 𝘕 ‎) 1D615 MATHEMATICAL SANS-SERIF ITALIC CAPITAL N +← (‎ 𝙉 ‎) 1D649 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL N +← (‎ 𝙽 ‎) 1D67D MATHEMATICAL MONOSPACE CAPITAL N +← (‎ 𝚴 ‎) 1D6B4 MATHEMATICAL BOLD CAPITAL NU # →𝐍→ +← (‎ 𝛮 ‎) 1D6EE MATHEMATICAL ITALIC CAPITAL NU # →𝑁→ +← (‎ 𝜨 ‎) 1D728 MATHEMATICAL BOLD ITALIC CAPITAL NU # →𝑵→ +← (‎ 𝝢 ‎) 1D762 MATHEMATICAL SANS-SERIF BOLD CAPITAL NU # →Ν→ +← (‎ 𝞜 ‎) 1D79C MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL NU # →Ν→ + +# NJ NJ + (‎ NJ ‎) 004E 004A LATIN CAPITAL LETTER N, LATIN CAPITAL LETTER J +← (‎ NJ ‎) 01CA LATIN CAPITAL LETTER NJ + +# Nj Nj + (‎ Nj ‎) 004E 006A LATIN CAPITAL LETTER N, LATIN SMALL LETTER J +← (‎ Nj ‎) 01CB LATIN CAPITAL LETTER N WITH SMALL LETTER J + +# No № + (‎ No ‎) 004E 006F LATIN CAPITAL LETTER N, LATIN SMALL LETTER O +← (‎ № ‎) 2116 NUMERO SIGN + +# N̊ Ν̊ Νͦ 𐆎 + (‎ N̊ ‎) 004E 030A LATIN CAPITAL LETTER N, COMBINING RING ABOVE +← (‎ Ν̊ ‎) 039D 030A GREEK CAPITAL LETTER NU, COMBINING RING ABOVE # →Νͦ→ +← (‎ Νͦ ‎) 039D 0366 GREEK CAPITAL LETTER NU, COMBINING LATIN SMALL LETTER O +← (‎ 𐆎 ‎) 1018E NOMISMA SIGN # →Νͦ→ + +# N̦ N̡ Ɲ + (‎ N̡ ‎) 004E 0321 LATIN CAPITAL LETTER N, COMBINING PALATALIZED HOOK BELOW +← (‎ N̦ ‎) 004E 0326 LATIN CAPITAL LETTER N, COMBINING COMMA BELOW +← (‎ Ɲ ‎) 019D LATIN CAPITAL LETTER N WITH LEFT HOOK + +# O' Oʼ Ꭴ Ơ + (‎ O' ‎) 004F 0027 LATIN CAPITAL LETTER O, APOSTROPHE +← (‎ Oʼ ‎) 004F 02BC LATIN CAPITAL LETTER O, MODIFIER LETTER APOSTROPHE +← (‎ Ꭴ ‎) 13A4 CHEROKEE LETTER U # →Ơ→→Oʼ→ +← (‎ Ơ ‎) 01A0 LATIN CAPITAL LETTER O WITH HORN # →Oʼ→ + +# OE Œ + (‎ OE ‎) 004F 0045 LATIN CAPITAL LETTER O, LATIN CAPITAL LETTER E +← (‎ Œ ‎) 0152 LATIN CAPITAL LIGATURE OE + +# OO Ꝏ Ꚙ + (‎ OO ‎) 004F 004F LATIN CAPITAL LETTER O, LATIN CAPITAL LETTER O +← (‎ Ꝏ ‎) A74E LATIN CAPITAL LETTER OO +← (‎ Ꚙ ‎) A698 CYRILLIC CAPITAL LETTER DOUBLE O + +# O̸ Ø ⵁ + (‎ O̸ ‎) 004F 0338 LATIN CAPITAL LETTER O, COMBINING LONG SOLIDUS OVERLAY +← (‎ Ø ‎) 00D8 LATIN CAPITAL LETTER O WITH STROKE +← (‎ ⵁ ‎) 2D41 TIFINAGH LETTER BERBER ACADEMY YAH # →Ø→ + +# Ó̸ Ǿ + (‎ Ó̸ ‎) 004F 0338 0301 LATIN CAPITAL LETTER O, COMBINING LONG SOLIDUS OVERLAY, COMBINING ACUTE ACCENT +← (‎ Ǿ ‎) 01FE LATIN CAPITAL LETTER O WITH STROKE AND ACUTE + +# P Ρ Р Ꮲ ᑭ Ⲣ ꓑ 𐊕 P ℙ 𝐏 𝑃 𝑷 𝒫 𝓟 𝔓 𝕻 𝖯 𝗣 𝘗 𝙋 𝙿 𝚸 𝛲 𝜬 𝝦 𝞠 + (‎ P ‎) 0050 LATIN CAPITAL LETTER P +← (‎ Ρ ‎) 03A1 GREEK CAPITAL LETTER RHO +← (‎ Р ‎) 0420 CYRILLIC CAPITAL LETTER ER +← (‎ Ꮲ ‎) 13E2 CHEROKEE LETTER TLV +← (‎ ᑭ ‎) 146D CANADIAN SYLLABICS KI +← (‎ Ⲣ ‎) 2CA2 COPTIC CAPITAL LETTER RO +← (‎ ꓑ ‎) A4D1 LISU LETTER PA +← (‎ 𐊕 ‎) 10295 LYCIAN LETTER R +← (‎ P ‎) FF30 FULLWIDTH LATIN CAPITAL LETTER P # →Р→ +← (‎ ℙ ‎) 2119 DOUBLE-STRUCK CAPITAL P +← (‎ 𝐏 ‎) 1D40F MATHEMATICAL BOLD CAPITAL P +← (‎ 𝑃 ‎) 1D443 MATHEMATICAL ITALIC CAPITAL P +← (‎ 𝑷 ‎) 1D477 MATHEMATICAL BOLD ITALIC CAPITAL P +← (‎ 𝒫 ‎) 1D4AB MATHEMATICAL SCRIPT CAPITAL P +← (‎ 𝓟 ‎) 1D4DF MATHEMATICAL BOLD SCRIPT CAPITAL P +← (‎ 𝔓 ‎) 1D513 MATHEMATICAL FRAKTUR CAPITAL P +← (‎ 𝕻 ‎) 1D57B MATHEMATICAL BOLD FRAKTUR CAPITAL P +← (‎ 𝖯 ‎) 1D5AF MATHEMATICAL SANS-SERIF CAPITAL P +← (‎ 𝗣 ‎) 1D5E3 MATHEMATICAL SANS-SERIF BOLD CAPITAL P +← (‎ 𝘗 ‎) 1D617 MATHEMATICAL SANS-SERIF ITALIC CAPITAL P +← (‎ 𝙋 ‎) 1D64B MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL P +← (‎ 𝙿 ‎) 1D67F MATHEMATICAL MONOSPACE CAPITAL P +← (‎ 𝚸 ‎) 1D6B8 MATHEMATICAL BOLD CAPITAL RHO # →𝐏→ +← (‎ 𝛲 ‎) 1D6F2 MATHEMATICAL ITALIC CAPITAL RHO # →Ρ→ +← (‎ 𝜬 ‎) 1D72C MATHEMATICAL BOLD ITALIC CAPITAL RHO # →Ρ→ +← (‎ 𝝦 ‎) 1D766 MATHEMATICAL SANS-SERIF BOLD CAPITAL RHO # →Ρ→ +← (‎ 𝞠 ‎) 1D7A0 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL RHO # →Ρ→ + +# P' ᑭᑊ Ꮲ' ᑭ' ᒆ + (‎ P' ‎) 0050 0027 LATIN CAPITAL LETTER P, APOSTROPHE +← (‎ ᑭᑊ ‎) 146D 144A CANADIAN SYLLABICS KI, CANADIAN SYLLABICS WEST-CREE P +← (‎ Ꮲ' ‎) 13E2 0027 CHEROKEE LETTER TLV, APOSTROPHE # →ᑭᑊ→ +← (‎ ᑭ' ‎) 146D 0027 CANADIAN SYLLABICS KI, APOSTROPHE # →ᑭᑊ→ +← (‎ ᒆ ‎) 1486 CANADIAN SYLLABICS SOUTH-SLAVEY KIH # →ᑭᑊ→ + +# p· P· pᐧ Ꮲ· ᑭ· ᑭᐧ ᑷ + (‎ P· ‎) 0050 00B7 LATIN CAPITAL LETTER P, MIDDLE DOT +← (‎ p· ‎) 0070 00B7 LATIN SMALL LETTER P, MIDDLE DOT # →pᐧ→→ᑷ→→ᑭᐧ→ +← (‎ pᐧ ‎) 0070 1427 LATIN SMALL LETTER P, CANADIAN SYLLABICS FINAL MIDDLE DOT # →ᑷ→→ᑭᐧ→ +← (‎ Ꮲ· ‎) 13E2 00B7 CHEROKEE LETTER TLV, MIDDLE DOT # →ᑭᐧ→ +← (‎ ᑭ· ‎) 146D 00B7 CANADIAN SYLLABICS KI, MIDDLE DOT # →ᑭᐧ→ +← (‎ ᑭᐧ ‎) 146D 1427 CANADIAN SYLLABICS KI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑷ ‎) 1477 CANADIAN SYLLABICS WEST-CREE KWI # →ᑭᐧ→ + +# Q ⵕ ℚ 𝐐 𝑄 𝑸 𝒬 𝓠 𝔔 𝕼 𝖰 𝗤 𝘘 𝙌 𝚀 + (‎ Q ‎) 0051 LATIN CAPITAL LETTER Q +← (‎ ⵕ ‎) 2D55 TIFINAGH LETTER YARR +← (‎ ℚ ‎) 211A DOUBLE-STRUCK CAPITAL Q +← (‎ 𝐐 ‎) 1D410 MATHEMATICAL BOLD CAPITAL Q +← (‎ 𝑄 ‎) 1D444 MATHEMATICAL ITALIC CAPITAL Q +← (‎ 𝑸 ‎) 1D478 MATHEMATICAL BOLD ITALIC CAPITAL Q +← (‎ 𝒬 ‎) 1D4AC MATHEMATICAL SCRIPT CAPITAL Q +← (‎ 𝓠 ‎) 1D4E0 MATHEMATICAL BOLD SCRIPT CAPITAL Q +← (‎ 𝔔 ‎) 1D514 MATHEMATICAL FRAKTUR CAPITAL Q +← (‎ 𝕼 ‎) 1D57C MATHEMATICAL BOLD FRAKTUR CAPITAL Q +← (‎ 𝖰 ‎) 1D5B0 MATHEMATICAL SANS-SERIF CAPITAL Q +← (‎ 𝗤 ‎) 1D5E4 MATHEMATICAL SANS-SERIF BOLD CAPITAL Q +← (‎ 𝘘 ‎) 1D618 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Q +← (‎ 𝙌 ‎) 1D64C MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Q +← (‎ 𝚀 ‎) 1D680 MATHEMATICAL MONOSPACE CAPITAL Q + +# QE 🜀 + (‎ QE ‎) 0051 0045 LATIN CAPITAL LETTER Q, LATIN CAPITAL LETTER E +← (‎ 🜀 ‎) 1F700 ALCHEMICAL SYMBOL FOR QUINTESSENCE + +# R Ʀ Ꭱ Ꮢ ᖇ ꓣ 𖼵 𝈖 𐒴 ℛ ℜ ℝ 𝐑 𝑅 𝑹 𝓡 𝕽 𝖱 𝗥 𝘙 𝙍 𝚁 + (‎ R ‎) 0052 LATIN CAPITAL LETTER R +← (‎ Ʀ ‎) 01A6 LATIN LETTER YR +← (‎ Ꭱ ‎) 13A1 CHEROKEE LETTER E +← (‎ Ꮢ ‎) 13D2 CHEROKEE LETTER SV +← (‎ ᖇ ‎) 1587 CANADIAN SYLLABICS TLHI +← (‎ ꓣ ‎) A4E3 LISU LETTER ZHA +← (‎ 𖼵 ‎) 16F35 MIAO LETTER ZHA +← (‎ 𝈖 ‎) 1D216 GREEK VOCAL NOTATION SYMBOL-23 +← (‎ 𐒴 ‎) 104B4 OSAGE CAPITAL LETTER BRA # →Ʀ→ +← (‎ ℛ ‎) 211B SCRIPT CAPITAL R +← (‎ ℜ ‎) 211C BLACK-LETTER CAPITAL R +← (‎ ℝ ‎) 211D DOUBLE-STRUCK CAPITAL R +← (‎ 𝐑 ‎) 1D411 MATHEMATICAL BOLD CAPITAL R +← (‎ 𝑅 ‎) 1D445 MATHEMATICAL ITALIC CAPITAL R +← (‎ 𝑹 ‎) 1D479 MATHEMATICAL BOLD ITALIC CAPITAL R +← (‎ 𝓡 ‎) 1D4E1 MATHEMATICAL BOLD SCRIPT CAPITAL R +← (‎ 𝕽 ‎) 1D57D MATHEMATICAL BOLD FRAKTUR CAPITAL R +← (‎ 𝖱 ‎) 1D5B1 MATHEMATICAL SANS-SERIF CAPITAL R +← (‎ 𝗥 ‎) 1D5E5 MATHEMATICAL SANS-SERIF BOLD CAPITAL R +← (‎ 𝘙 ‎) 1D619 MATHEMATICAL SANS-SERIF ITALIC CAPITAL R +← (‎ 𝙍 ‎) 1D64D MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL R +← (‎ 𝚁 ‎) 1D681 MATHEMATICAL MONOSPACE CAPITAL R + +# Rs ₨ + (‎ Rs ‎) 0052 0073 LATIN CAPITAL LETTER R, LATIN SMALL LETTER S +← (‎ ₨ ‎) 20A8 RUPEE SIGN + +# S Ѕ Տ Ꮥ Ꮪ ꓢ 𐊖 𐐠 𖼺 S 𝐒 𝑆 𝑺 𝒮 𝓢 𝔖 𝕊 𝕾 𝖲 𝗦 𝘚 𝙎 𝚂 + (‎ S ‎) 0053 LATIN CAPITAL LETTER S +← (‎ Ѕ ‎) 0405 CYRILLIC CAPITAL LETTER DZE +← (‎ Տ ‎) 054F ARMENIAN CAPITAL LETTER TIWN +← (‎ Ꮥ ‎) 13D5 CHEROKEE LETTER DE +← (‎ Ꮪ ‎) 13DA CHEROKEE LETTER DU +← (‎ ꓢ ‎) A4E2 LISU LETTER SA +← (‎ 𐊖 ‎) 10296 LYCIAN LETTER S +← (‎ 𐐠 ‎) 10420 DESERET CAPITAL LETTER ZHEE +← (‎ 𖼺 ‎) 16F3A MIAO LETTER SA +← (‎ S ‎) FF33 FULLWIDTH LATIN CAPITAL LETTER S # →Ѕ→ +← (‎ 𝐒 ‎) 1D412 MATHEMATICAL BOLD CAPITAL S +← (‎ 𝑆 ‎) 1D446 MATHEMATICAL ITALIC CAPITAL S +← (‎ 𝑺 ‎) 1D47A MATHEMATICAL BOLD ITALIC CAPITAL S +← (‎ 𝒮 ‎) 1D4AE MATHEMATICAL SCRIPT CAPITAL S +← (‎ 𝓢 ‎) 1D4E2 MATHEMATICAL BOLD SCRIPT CAPITAL S +← (‎ 𝔖 ‎) 1D516 MATHEMATICAL FRAKTUR CAPITAL S +← (‎ 𝕊 ‎) 1D54A MATHEMATICAL DOUBLE-STRUCK CAPITAL S +← (‎ 𝕾 ‎) 1D57E MATHEMATICAL BOLD FRAKTUR CAPITAL S +← (‎ 𝖲 ‎) 1D5B2 MATHEMATICAL SANS-SERIF CAPITAL S +← (‎ 𝗦 ‎) 1D5E6 MATHEMATICAL SANS-SERIF BOLD CAPITAL S +← (‎ 𝘚 ‎) 1D61A MATHEMATICAL SANS-SERIF ITALIC CAPITAL S +← (‎ 𝙎 ‎) 1D64E MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL S +← (‎ 𝚂 ‎) 1D682 MATHEMATICAL MONOSPACE CAPITAL S + +# T Τ Т Ꭲ Ⲧ ꓔ 𐊗 𐊱 𐌕 𖼊 ⊤ ⟙ 🝨 𑢼 T 𝐓 𝑇 𝑻 𝒯 𝓣 𝔗 𝕋 𝕿 𝖳 𝗧 𝘛 𝙏 𝚃 𝚻 𝛵 𝜯 𝝩 𝞣 + (‎ T ‎) 0054 LATIN CAPITAL LETTER T +← (‎ Τ ‎) 03A4 GREEK CAPITAL LETTER TAU +← (‎ Т ‎) 0422 CYRILLIC CAPITAL LETTER TE +← (‎ Ꭲ ‎) 13A2 CHEROKEE LETTER I +← (‎ Ⲧ ‎) 2CA6 COPTIC CAPITAL LETTER TAU +← (‎ ꓔ ‎) A4D4 LISU LETTER TA +← (‎ 𐊗 ‎) 10297 LYCIAN LETTER T +← (‎ 𐊱 ‎) 102B1 CARIAN LETTER C-18 +← (‎ 𐌕 ‎) 10315 OLD ITALIC LETTER TE +← (‎ 𖼊 ‎) 16F0A MIAO LETTER TA +← (‎ ⊤ ‎) 22A4 DOWN TACK +← (‎ ⟙ ‎) 27D9 LARGE DOWN TACK +← (‎ 🝨 ‎) 1F768 ALCHEMICAL SYMBOL FOR CRUCIBLE-4 +← (‎ 𑢼 ‎) 118BC WARANG CITI CAPITAL LETTER HAR +← (‎ T ‎) FF34 FULLWIDTH LATIN CAPITAL LETTER T # →Т→ +← (‎ 𝐓 ‎) 1D413 MATHEMATICAL BOLD CAPITAL T +← (‎ 𝑇 ‎) 1D447 MATHEMATICAL ITALIC CAPITAL T +← (‎ 𝑻 ‎) 1D47B MATHEMATICAL BOLD ITALIC CAPITAL T +← (‎ 𝒯 ‎) 1D4AF MATHEMATICAL SCRIPT CAPITAL T +← (‎ 𝓣 ‎) 1D4E3 MATHEMATICAL BOLD SCRIPT CAPITAL T +← (‎ 𝔗 ‎) 1D517 MATHEMATICAL FRAKTUR CAPITAL T +← (‎ 𝕋 ‎) 1D54B MATHEMATICAL DOUBLE-STRUCK CAPITAL T +← (‎ 𝕿 ‎) 1D57F MATHEMATICAL BOLD FRAKTUR CAPITAL T +← (‎ 𝖳 ‎) 1D5B3 MATHEMATICAL SANS-SERIF CAPITAL T +← (‎ 𝗧 ‎) 1D5E7 MATHEMATICAL SANS-SERIF BOLD CAPITAL T +← (‎ 𝘛 ‎) 1D61B MATHEMATICAL SANS-SERIF ITALIC CAPITAL T +← (‎ 𝙏 ‎) 1D64F MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL T +← (‎ 𝚃 ‎) 1D683 MATHEMATICAL MONOSPACE CAPITAL T +← (‎ 𝚻 ‎) 1D6BB MATHEMATICAL BOLD CAPITAL TAU # →Τ→ +← (‎ 𝛵 ‎) 1D6F5 MATHEMATICAL ITALIC CAPITAL TAU # →Τ→ +← (‎ 𝜯 ‎) 1D72F MATHEMATICAL BOLD ITALIC CAPITAL TAU # →Τ→ +← (‎ 𝝩 ‎) 1D769 MATHEMATICAL SANS-SERIF BOLD CAPITAL TAU # →Τ→ +← (‎ 𝞣 ‎) 1D7A3 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL TAU # →Τ→ + +# T3 TƷ Ꜩ + (‎ T3 ‎) 0054 0033 LATIN CAPITAL LETTER T, DIGIT THREE +← (‎ TƷ ‎) 0054 01B7 LATIN CAPITAL LETTER T, LATIN CAPITAL LETTER EZH +← (‎ Ꜩ ‎) A728 LATIN CAPITAL LETTER TZ # →TƷ→ + +# TEL ℡ + (‎ TEL ‎) 0054 0045 004C LATIN CAPITAL LETTER T, LATIN CAPITAL LETTER E, LATIN CAPITAL LETTER L +← (‎ ℡ ‎) 2121 TELEPHONE SIGN + +# T̈ Ꭲ̈ ꓔ̈ ⊤̈ ⍡ + (‎ T̈ ‎) 0054 0308 LATIN CAPITAL LETTER T, COMBINING DIAERESIS +← (‎ Ꭲ̈ ‎) 13A2 0308 CHEROKEE LETTER I, COMBINING DIAERESIS # →⊤̈→ +← (‎ ꓔ̈ ‎) A4D4 0308 LISU LETTER TA, COMBINING DIAERESIS # →⊤̈→ +← (‎ ⊤̈ ‎) 22A4 0308 DOWN TACK, COMBINING DIAERESIS +← (‎ ⍡ ‎) 2361 APL FUNCTIONAL SYMBOL UP TACK DIAERESIS # →⊤̈→ + +# T̨ Ʈ + (‎ T̨ ‎) 0054 0328 LATIN CAPITAL LETTER T, COMBINING OGONEK +← (‎ Ʈ ‎) 01AE LATIN CAPITAL LETTER T WITH RETROFLEX HOOK + +# T̩ Т̩ Ҭ + (‎ T̩ ‎) 0054 0329 LATIN CAPITAL LETTER T, COMBINING VERTICAL LINE BELOW +← (‎ Т̩ ‎) 0422 0329 CYRILLIC CAPITAL LETTER TE, COMBINING VERTICAL LINE BELOW +← (‎ Ҭ ‎) 04AC CYRILLIC CAPITAL LETTER TE WITH DESCENDER # →Т̩→ + +# T̵ Ŧ + (‎ T̵ ‎) 0054 0335 LATIN CAPITAL LETTER T, COMBINING SHORT STROKE OVERLAY +← (‎ Ŧ ‎) 0166 LATIN CAPITAL LETTER T WITH STROKE + +# T̸ Ⱦ + (‎ T̸ ‎) 0054 0338 LATIN CAPITAL LETTER T, COMBINING LONG SOLIDUS OVERLAY +← (‎ Ⱦ ‎) 023E LATIN CAPITAL LETTER T WITH DIAGONAL STROKE + +# T⃫ Т⃫ ₮ + (‎ T⃫ ‎) 0054 20EB LATIN CAPITAL LETTER T, COMBINING LONG DOUBLE SOLIDUS OVERLAY +← (‎ Т⃫ ‎) 0422 20EB CYRILLIC CAPITAL LETTER TE, COMBINING LONG DOUBLE SOLIDUS OVERLAY +← (‎ ₮ ‎) 20AE TUGRIK SIGN # →Т⃫→ + +# U ሀ Ս ᑌ ꓴ 𖽂 ∪ ⋃ 𑢸 𐓎 𝐔 𝑈 𝑼 𝒰 𝓤 𝔘 𝕌 𝖀 𝖴 𝗨 𝘜 𝙐 𝚄 + (‎ U ‎) 0055 LATIN CAPITAL LETTER U +← (‎ ሀ ‎) 1200 ETHIOPIC SYLLABLE HA # →Ս→ +← (‎ Ս ‎) 054D ARMENIAN CAPITAL LETTER SEH +← (‎ ᑌ ‎) 144C CANADIAN SYLLABICS TE +← (‎ ꓴ ‎) A4F4 LISU LETTER U +← (‎ 𖽂 ‎) 16F42 MIAO LETTER WA +← (‎ ∪ ‎) 222A UNION # →ᑌ→ +← (‎ ⋃ ‎) 22C3 N-ARY UNION # →∪→→ᑌ→ +← (‎ 𑢸 ‎) 118B8 WARANG CITI CAPITAL LETTER PU +← (‎ 𐓎 ‎) 104CE OSAGE CAPITAL LETTER U +← (‎ 𝐔 ‎) 1D414 MATHEMATICAL BOLD CAPITAL U +← (‎ 𝑈 ‎) 1D448 MATHEMATICAL ITALIC CAPITAL U +← (‎ 𝑼 ‎) 1D47C MATHEMATICAL BOLD ITALIC CAPITAL U +← (‎ 𝒰 ‎) 1D4B0 MATHEMATICAL SCRIPT CAPITAL U +← (‎ 𝓤 ‎) 1D4E4 MATHEMATICAL BOLD SCRIPT CAPITAL U +← (‎ 𝔘 ‎) 1D518 MATHEMATICAL FRAKTUR CAPITAL U +← (‎ 𝕌 ‎) 1D54C MATHEMATICAL DOUBLE-STRUCK CAPITAL U +← (‎ 𝖀 ‎) 1D580 MATHEMATICAL BOLD FRAKTUR CAPITAL U +← (‎ 𝖴 ‎) 1D5B4 MATHEMATICAL SANS-SERIF CAPITAL U +← (‎ 𝗨 ‎) 1D5E8 MATHEMATICAL SANS-SERIF BOLD CAPITAL U +← (‎ 𝘜 ‎) 1D61C MATHEMATICAL SANS-SERIF ITALIC CAPITAL U +← (‎ 𝙐 ‎) 1D650 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL U +← (‎ 𝚄 ‎) 1D684 MATHEMATICAL MONOSPACE CAPITAL U + +# U' ᑌᑊ ሀ' ᑌ' ᑧ + (‎ U' ‎) 0055 0027 LATIN CAPITAL LETTER U, APOSTROPHE +← (‎ ᑌᑊ ‎) 144C 144A CANADIAN SYLLABICS TE, CANADIAN SYLLABICS WEST-CREE P # →ᑌ'→ +← (‎ ሀ' ‎) 1200 0027 ETHIOPIC SYLLABLE HA, APOSTROPHE # →ᑌ'→ +← (‎ ᑌ' ‎) 144C 0027 CANADIAN SYLLABICS TE, APOSTROPHE +← (‎ ᑧ ‎) 1467 CANADIAN SYLLABICS TTE # →ᑌᑊ→→ᑌ'→ + +# U+= U+〓 + (‎ U+= ‎) 0055 002B 003D LATIN CAPITAL LETTER U, PLUS SIGN, EQUALS SIGN +← (‎ U+〓 ‎) 0055 002B 3013 LATIN CAPITAL LETTER U, PLUS SIGN, GETA MARK + +# U· ሀ· ᑌ· ᑌᐧ ᑘ + (‎ U· ‎) 0055 00B7 LATIN CAPITAL LETTER U, MIDDLE DOT +← (‎ ሀ· ‎) 1200 00B7 ETHIOPIC SYLLABLE HA, MIDDLE DOT # →ᑌ·→ +← (‎ ᑌ· ‎) 144C 00B7 CANADIAN SYLLABICS TE, MIDDLE DOT +← (‎ ᑌᐧ ‎) 144C 1427 CANADIAN SYLLABICS TE, CANADIAN SYLLABICS FINAL MIDDLE DOT # →ᑌ·→ +← (‎ ᑘ ‎) 1458 CANADIAN SYLLABICS WEST-CREE TWE # →ᑌᐧ→→ᑌ·→ + +# U̵ U̶ Ʉ Ꮜ + (‎ U̵ ‎) 0055 0335 LATIN CAPITAL LETTER U, COMBINING SHORT STROKE OVERLAY +← (‎ U̶ ‎) 0055 0336 LATIN CAPITAL LETTER U, COMBINING LONG STROKE OVERLAY +← (‎ Ʉ ‎) 0244 LATIN CAPITAL LETTER U BAR # →U̶→ +← (‎ Ꮜ ‎) 13CC CHEROKEE LETTER SA # →Ʉ→→U̶→ + +# V ٧ ۷ Ѵ Ꮩ ᐯ ⴸ ꓦ ꛟ 𖼈 𝈍 𐔝 𑢠 Ⅴ 𝐕 𝑉 𝑽 𝒱 𝓥 𝔙 𝕍 𝖁 𝖵 𝗩 𝘝 𝙑 𝚅 + (‎ V ‎) 0056 LATIN CAPITAL LETTER V +← (‎ ٧ ‎) 0667 ARABIC-INDIC DIGIT SEVEN +← (‎ ۷ ‎) 06F7 EXTENDED ARABIC-INDIC DIGIT SEVEN # →‎٧‎→ +← (‎ Ѵ ‎) 0474 CYRILLIC CAPITAL LETTER IZHITSA +← (‎ Ꮩ ‎) 13D9 CHEROKEE LETTER DO +← (‎ ᐯ ‎) 142F CANADIAN SYLLABICS PE +← (‎ ⴸ ‎) 2D38 TIFINAGH LETTER YADH +← (‎ ꓦ ‎) A4E6 LISU LETTER HA +← (‎ ꛟ ‎) A6DF BAMUM LETTER KO +← (‎ 𖼈 ‎) 16F08 MIAO LETTER VA +← (‎ 𝈍 ‎) 1D20D GREEK VOCAL NOTATION SYMBOL-14 +← (‎ 𐔝 ‎) 1051D ELBASAN LETTER TE +← (‎ 𑢠 ‎) 118A0 WARANG CITI CAPITAL LETTER NGAA +← (‎ Ⅴ ‎) 2164 ROMAN NUMERAL FIVE +← (‎ 𝐕 ‎) 1D415 MATHEMATICAL BOLD CAPITAL V +← (‎ 𝑉 ‎) 1D449 MATHEMATICAL ITALIC CAPITAL V +← (‎ 𝑽 ‎) 1D47D MATHEMATICAL BOLD ITALIC CAPITAL V +← (‎ 𝒱 ‎) 1D4B1 MATHEMATICAL SCRIPT CAPITAL V +← (‎ 𝓥 ‎) 1D4E5 MATHEMATICAL BOLD SCRIPT CAPITAL V +← (‎ 𝔙 ‎) 1D519 MATHEMATICAL FRAKTUR CAPITAL V +← (‎ 𝕍 ‎) 1D54D MATHEMATICAL DOUBLE-STRUCK CAPITAL V +← (‎ 𝖁 ‎) 1D581 MATHEMATICAL BOLD FRAKTUR CAPITAL V +← (‎ 𝖵 ‎) 1D5B5 MATHEMATICAL SANS-SERIF CAPITAL V +← (‎ 𝗩 ‎) 1D5E9 MATHEMATICAL SANS-SERIF BOLD CAPITAL V +← (‎ 𝘝 ‎) 1D61D MATHEMATICAL SANS-SERIF ITALIC CAPITAL V +← (‎ 𝙑 ‎) 1D651 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL V +← (‎ 𝚅 ‎) 1D685 MATHEMATICAL MONOSPACE CAPITAL V + +# VB 🝬 + (‎ VB ‎) 0056 0042 LATIN CAPITAL LETTER V, LATIN CAPITAL LETTER B +← (‎ 🝬 ‎) 1F76C ALCHEMICAL SYMBOL FOR BATH OF VAPOURS + +# Vl VI Ⅵ + (‎ VI ‎) 0056 0049 LATIN CAPITAL LETTER V, LATIN CAPITAL LETTER I +← (‎ Vl ‎) 0056 006C LATIN CAPITAL LETTER V, LATIN SMALL LETTER L +← (‎ Ⅵ ‎) 2165 ROMAN NUMERAL SIX + +# Vll VII Ⅶ + (‎ VII ‎) 0056 0049 0049 LATIN CAPITAL LETTER V, LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I +← (‎ Vll ‎) 0056 006C 006C LATIN CAPITAL LETTER V, LATIN SMALL LETTER L, LATIN SMALL LETTER L +← (‎ Ⅶ ‎) 2166 ROMAN NUMERAL SEVEN + +# Vlll VIII Ⅷ + (‎ VIII ‎) 0056 0049 0049 0049 LATIN CAPITAL LETTER V, LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I +← (‎ Vlll ‎) 0056 006C 006C 006C LATIN CAPITAL LETTER V, LATIN SMALL LETTER L, LATIN SMALL LETTER L, LATIN SMALL LETTER L +← (‎ Ⅷ ‎) 2167 ROMAN NUMERAL EIGHT + +# V· ٧· ᐯ· ᐯᐧ ᐻ + (‎ V· ‎) 0056 00B7 LATIN CAPITAL LETTER V, MIDDLE DOT +← (‎ ٧· ‎) 0667 00B7 ARABIC-INDIC DIGIT SEVEN, MIDDLE DOT # →ᐯᐧ→ +← (‎ ᐯ· ‎) 142F 00B7 CANADIAN SYLLABICS PE, MIDDLE DOT # →ᐯᐧ→ +← (‎ ᐯᐧ ‎) 142F 1427 CANADIAN SYLLABICS PE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐻ ‎) 143B CANADIAN SYLLABICS WEST-CREE PWE # →ᐯᐧ→ + +# V̵ V̶ 𐆗 + (‎ V̵ ‎) 0056 0335 LATIN CAPITAL LETTER V, COMBINING SHORT STROKE OVERLAY +← (‎ V̶ ‎) 0056 0336 LATIN CAPITAL LETTER V, COMBINING LONG STROKE OVERLAY +← (‎ 𐆗 ‎) 10197 ROMAN QUINARIUS SIGN # →V̶→ + +# Vᷤ 🜈 + (‎ Vᷤ ‎) 0056 1DE4 LATIN CAPITAL LETTER V, COMBINING LATIN SMALL LETTER S +← (‎ 🜈 ‎) 1F708 ALCHEMICAL SYMBOL FOR AQUA VITAE + +# W Ԝ Ꮃ Ꮤ ꓪ 𑣦 𑣯 𝐖 𝑊 𝑾 𝒲 𝓦 𝔚 𝕎 𝖂 𝖶 𝗪 𝘞 𝙒 𝚆 + (‎ W ‎) 0057 LATIN CAPITAL LETTER W +← (‎ Ԝ ‎) 051C CYRILLIC CAPITAL LETTER WE +← (‎ Ꮃ ‎) 13B3 CHEROKEE LETTER LA +← (‎ Ꮤ ‎) 13D4 CHEROKEE LETTER TA +← (‎ ꓪ ‎) A4EA LISU LETTER WA +← (‎ 𑣦 ‎) 118E6 WARANG CITI DIGIT SIX +← (‎ 𑣯 ‎) 118EF WARANG CITI NUMBER SIXTY +← (‎ 𝐖 ‎) 1D416 MATHEMATICAL BOLD CAPITAL W +← (‎ 𝑊 ‎) 1D44A MATHEMATICAL ITALIC CAPITAL W +← (‎ 𝑾 ‎) 1D47E MATHEMATICAL BOLD ITALIC CAPITAL W +← (‎ 𝒲 ‎) 1D4B2 MATHEMATICAL SCRIPT CAPITAL W +← (‎ 𝓦 ‎) 1D4E6 MATHEMATICAL BOLD SCRIPT CAPITAL W +← (‎ 𝔚 ‎) 1D51A MATHEMATICAL FRAKTUR CAPITAL W +← (‎ 𝕎 ‎) 1D54E MATHEMATICAL DOUBLE-STRUCK CAPITAL W +← (‎ 𝖂 ‎) 1D582 MATHEMATICAL BOLD FRAKTUR CAPITAL W +← (‎ 𝖶 ‎) 1D5B6 MATHEMATICAL SANS-SERIF CAPITAL W +← (‎ 𝗪 ‎) 1D5EA MATHEMATICAL SANS-SERIF BOLD CAPITAL W +← (‎ 𝘞 ‎) 1D61E MATHEMATICAL SANS-SERIF ITALIC CAPITAL W +← (‎ 𝙒 ‎) 1D652 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL W +← (‎ 𝚆 ‎) 1D686 MATHEMATICAL MONOSPACE CAPITAL W + +# W̵ W̶ ₩ + (‎ W̵ ‎) 0057 0335 LATIN CAPITAL LETTER W, COMBINING SHORT STROKE OVERLAY +← (‎ W̶ ‎) 0057 0336 LATIN CAPITAL LETTER W, COMBINING LONG STROKE OVERLAY # →₩→ +← (‎ ₩ ‎) 20A9 WON SIGN + +# X Χ Х ᚷ Ⲭ ⵝ ꓫ 𐊐 𐊴 𐌗 ᙭ ╳ 𐌢 𐔧 𑣬 Ꭓ Ⅹ X 𝐗 𝑋 𝑿 𝒳 𝓧 𝔛 𝕏 𝖃 𝖷 𝗫 𝘟 𝙓 𝚇 𝚾 𝛸 𝜲 𝝬 𝞦 + (‎ X ‎) 0058 LATIN CAPITAL LETTER X +← (‎ Χ ‎) 03A7 GREEK CAPITAL LETTER CHI +← (‎ Х ‎) 0425 CYRILLIC CAPITAL LETTER HA +← (‎ ᚷ ‎) 16B7 RUNIC LETTER GEBO GYFU G +← (‎ Ⲭ ‎) 2CAC COPTIC CAPITAL LETTER KHI # →Х→ +← (‎ ⵝ ‎) 2D5D TIFINAGH LETTER YATH +← (‎ ꓫ ‎) A4EB LISU LETTER SHA +← (‎ 𐊐 ‎) 10290 LYCIAN LETTER MM +← (‎ 𐊴 ‎) 102B4 CARIAN LETTER X +← (‎ 𐌗 ‎) 10317 OLD ITALIC LETTER EKS +← (‎ ᙭ ‎) 166D CANADIAN SYLLABICS CHI SIGN +← (‎ ╳ ‎) 2573 BOX DRAWINGS LIGHT DIAGONAL CROSS +← (‎ 𐌢 ‎) 10322 OLD ITALIC NUMERAL TEN # →𐌗→ +← (‎ 𐔧 ‎) 10527 ELBASAN LETTER KHE +← (‎ 𑣬 ‎) 118EC WARANG CITI NUMBER THIRTY +← (‎ Ꭓ ‎) A7B3 LATIN CAPITAL LETTER CHI +← (‎ Ⅹ ‎) 2169 ROMAN NUMERAL TEN +← (‎ X ‎) FF38 FULLWIDTH LATIN CAPITAL LETTER X # →Х→ +← (‎ 𝐗 ‎) 1D417 MATHEMATICAL BOLD CAPITAL X +← (‎ 𝑋 ‎) 1D44B MATHEMATICAL ITALIC CAPITAL X +← (‎ 𝑿 ‎) 1D47F MATHEMATICAL BOLD ITALIC CAPITAL X +← (‎ 𝒳 ‎) 1D4B3 MATHEMATICAL SCRIPT CAPITAL X +← (‎ 𝓧 ‎) 1D4E7 MATHEMATICAL BOLD SCRIPT CAPITAL X +← (‎ 𝔛 ‎) 1D51B MATHEMATICAL FRAKTUR CAPITAL X +← (‎ 𝕏 ‎) 1D54F MATHEMATICAL DOUBLE-STRUCK CAPITAL X +← (‎ 𝖃 ‎) 1D583 MATHEMATICAL BOLD FRAKTUR CAPITAL X +← (‎ 𝖷 ‎) 1D5B7 MATHEMATICAL SANS-SERIF CAPITAL X +← (‎ 𝗫 ‎) 1D5EB MATHEMATICAL SANS-SERIF BOLD CAPITAL X +← (‎ 𝘟 ‎) 1D61F MATHEMATICAL SANS-SERIF ITALIC CAPITAL X +← (‎ 𝙓 ‎) 1D653 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL X +← (‎ 𝚇 ‎) 1D687 MATHEMATICAL MONOSPACE CAPITAL X +← (‎ 𝚾 ‎) 1D6BE MATHEMATICAL BOLD CAPITAL CHI # →Χ→ +← (‎ 𝛸 ‎) 1D6F8 MATHEMATICAL ITALIC CAPITAL CHI # →Χ→ +← (‎ 𝜲 ‎) 1D732 MATHEMATICAL BOLD ITALIC CAPITAL CHI # →𝑿→ +← (‎ 𝝬 ‎) 1D76C MATHEMATICAL SANS-SERIF BOLD CAPITAL CHI # →Χ→ +← (‎ 𝞦 ‎) 1D7A6 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL CHI # →Χ→ + +# Xl XI Ⅺ + (‎ XI ‎) 0058 0049 LATIN CAPITAL LETTER X, LATIN CAPITAL LETTER I +← (‎ Xl ‎) 0058 006C LATIN CAPITAL LETTER X, LATIN SMALL LETTER L +← (‎ Ⅺ ‎) 216A ROMAN NUMERAL ELEVEN + +# Xll XII Ⅻ + (‎ XII ‎) 0058 0049 0049 LATIN CAPITAL LETTER X, LATIN CAPITAL LETTER I, LATIN CAPITAL LETTER I +← (‎ Xll ‎) 0058 006C 006C LATIN CAPITAL LETTER X, LATIN SMALL LETTER L, LATIN SMALL LETTER L +← (‎ Ⅻ ‎) 216B ROMAN NUMERAL TWELVE + +# X̩ Х̩ Ҳ + (‎ X̩ ‎) 0058 0329 LATIN CAPITAL LETTER X, COMBINING VERTICAL LINE BELOW +← (‎ Х̩ ‎) 0425 0329 CYRILLIC CAPITAL LETTER HA, COMBINING VERTICAL LINE BELOW +← (‎ Ҳ ‎) 04B2 CYRILLIC CAPITAL LETTER HA WITH DESCENDER # →Х̩→ + +# X̵ X̶ 𐆖 + (‎ X̵ ‎) 0058 0335 LATIN CAPITAL LETTER X, COMBINING SHORT STROKE OVERLAY +← (‎ X̶ ‎) 0058 0336 LATIN CAPITAL LETTER X, COMBINING LONG STROKE OVERLAY +← (‎ 𐆖 ‎) 10196 ROMAN DENARIUS SIGN # →X̶→ + +# Y Υ У Ү Ꭹ Ꮍ Ⲩ ꓬ 𐊲 𖽃 𑢤 Y ϒ 𝐘 𝑌 𝒀 𝒴 𝓨 𝔜 𝕐 𝖄 𝖸 𝗬 𝘠 𝙔 𝚈 𝚼 𝛶 𝜰 𝝪 𝞤 + (‎ Y ‎) 0059 LATIN CAPITAL LETTER Y +← (‎ Υ ‎) 03A5 GREEK CAPITAL LETTER UPSILON +← (‎ У ‎) 0423 CYRILLIC CAPITAL LETTER U +← (‎ Ү ‎) 04AE CYRILLIC CAPITAL LETTER STRAIGHT U +← (‎ Ꭹ ‎) 13A9 CHEROKEE LETTER GI +← (‎ Ꮍ ‎) 13BD CHEROKEE LETTER MU # →Ꭹ→ +← (‎ Ⲩ ‎) 2CA8 COPTIC CAPITAL LETTER UA +← (‎ ꓬ ‎) A4EC LISU LETTER YA +← (‎ 𐊲 ‎) 102B2 CARIAN LETTER U +← (‎ 𖽃 ‎) 16F43 MIAO LETTER AH +← (‎ 𑢤 ‎) 118A4 WARANG CITI CAPITAL LETTER YA +← (‎ Y ‎) FF39 FULLWIDTH LATIN CAPITAL LETTER Y # →Υ→ +← (‎ ϒ ‎) 03D2 GREEK UPSILON WITH HOOK SYMBOL +← (‎ 𝐘 ‎) 1D418 MATHEMATICAL BOLD CAPITAL Y +← (‎ 𝑌 ‎) 1D44C MATHEMATICAL ITALIC CAPITAL Y +← (‎ 𝒀 ‎) 1D480 MATHEMATICAL BOLD ITALIC CAPITAL Y +← (‎ 𝒴 ‎) 1D4B4 MATHEMATICAL SCRIPT CAPITAL Y +← (‎ 𝓨 ‎) 1D4E8 MATHEMATICAL BOLD SCRIPT CAPITAL Y +← (‎ 𝔜 ‎) 1D51C MATHEMATICAL FRAKTUR CAPITAL Y +← (‎ 𝕐 ‎) 1D550 MATHEMATICAL DOUBLE-STRUCK CAPITAL Y +← (‎ 𝖄 ‎) 1D584 MATHEMATICAL BOLD FRAKTUR CAPITAL Y +← (‎ 𝖸 ‎) 1D5B8 MATHEMATICAL SANS-SERIF CAPITAL Y +← (‎ 𝗬 ‎) 1D5EC MATHEMATICAL SANS-SERIF BOLD CAPITAL Y +← (‎ 𝘠 ‎) 1D620 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Y +← (‎ 𝙔 ‎) 1D654 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Y +← (‎ 𝚈 ‎) 1D688 MATHEMATICAL MONOSPACE CAPITAL Y +← (‎ 𝚼 ‎) 1D6BC MATHEMATICAL BOLD CAPITAL UPSILON # →Υ→ +← (‎ 𝛶 ‎) 1D6F6 MATHEMATICAL ITALIC CAPITAL UPSILON # →Υ→ +← (‎ 𝜰 ‎) 1D730 MATHEMATICAL BOLD ITALIC CAPITAL UPSILON # →Υ→ +← (‎ 𝝪 ‎) 1D76A MATHEMATICAL SANS-SERIF BOLD CAPITAL UPSILON # →Υ→ +← (‎ 𝞤 ‎) 1D7A4 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL UPSILON # →Υ→ + +# Y̵ У̵ Ү̵ Ɏ Ұ ¥ + (‎ Y̵ ‎) 0059 0335 LATIN CAPITAL LETTER Y, COMBINING SHORT STROKE OVERLAY +← (‎ У̵ ‎) 0423 0335 CYRILLIC CAPITAL LETTER U, COMBINING SHORT STROKE OVERLAY # →Ү̵→ +← (‎ Ү̵ ‎) 04AE 0335 CYRILLIC CAPITAL LETTER STRAIGHT U, COMBINING SHORT STROKE OVERLAY +← (‎ Ɏ ‎) 024E LATIN CAPITAL LETTER Y WITH STROKE +← (‎ Ұ ‎) 04B0 CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE # →Ү̵→ +← (‎ ¥ ‎) 00A5 YEN SIGN + +# Z Ζ Ꮓ ꓜ 𑢩 𑣥 𐋵 Z ℤ ℨ 𝐙 𝑍 𝒁 𝒵 𝓩 𝖅 𝖹 𝗭 𝘡 𝙕 𝚉 𝚭 𝛧 𝜡 𝝛 𝞕 + (‎ Z ‎) 005A LATIN CAPITAL LETTER Z +← (‎ Ζ ‎) 0396 GREEK CAPITAL LETTER ZETA +← (‎ Ꮓ ‎) 13C3 CHEROKEE LETTER NO +← (‎ ꓜ ‎) A4DC LISU LETTER DZA +← (‎ 𑢩 ‎) 118A9 WARANG CITI CAPITAL LETTER O +← (‎ 𑣥 ‎) 118E5 WARANG CITI DIGIT FIVE +← (‎ 𐋵 ‎) 102F5 COPTIC EPACT NUMBER THREE HUNDRED +← (‎ Z ‎) FF3A FULLWIDTH LATIN CAPITAL LETTER Z # →Ζ→ +← (‎ ℤ ‎) 2124 DOUBLE-STRUCK CAPITAL Z +← (‎ ℨ ‎) 2128 BLACK-LETTER CAPITAL Z +← (‎ 𝐙 ‎) 1D419 MATHEMATICAL BOLD CAPITAL Z +← (‎ 𝑍 ‎) 1D44D MATHEMATICAL ITALIC CAPITAL Z +← (‎ 𝒁 ‎) 1D481 MATHEMATICAL BOLD ITALIC CAPITAL Z +← (‎ 𝒵 ‎) 1D4B5 MATHEMATICAL SCRIPT CAPITAL Z +← (‎ 𝓩 ‎) 1D4E9 MATHEMATICAL BOLD SCRIPT CAPITAL Z +← (‎ 𝖅 ‎) 1D585 MATHEMATICAL BOLD FRAKTUR CAPITAL Z +← (‎ 𝖹 ‎) 1D5B9 MATHEMATICAL SANS-SERIF CAPITAL Z +← (‎ 𝗭 ‎) 1D5ED MATHEMATICAL SANS-SERIF BOLD CAPITAL Z +← (‎ 𝘡 ‎) 1D621 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Z +← (‎ 𝙕 ‎) 1D655 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Z +← (‎ 𝚉 ‎) 1D689 MATHEMATICAL MONOSPACE CAPITAL Z +← (‎ 𝚭 ‎) 1D6AD MATHEMATICAL BOLD CAPITAL ZETA # →Ζ→ +← (‎ 𝛧 ‎) 1D6E7 MATHEMATICAL ITALIC CAPITAL ZETA # →𝑍→ +← (‎ 𝜡 ‎) 1D721 MATHEMATICAL BOLD ITALIC CAPITAL ZETA # →Ζ→ +← (‎ 𝝛 ‎) 1D75B MATHEMATICAL SANS-SERIF BOLD CAPITAL ZETA # →Ζ→ +← (‎ 𝞕 ‎) 1D795 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ZETA # →Ζ→ + +# Z̦ Z̧ Ȥ + (‎ Z̦ ‎) 005A 0326 LATIN CAPITAL LETTER Z, COMBINING COMMA BELOW +← (‎ Z̧ ‎) 005A 0327 LATIN CAPITAL LETTER Z, COMBINING CEDILLA +← (‎ Ȥ ‎) 0224 LATIN CAPITAL LETTER Z WITH HOOK # →Z̧→ + +# Z̵ Ƶ + (‎ Z̵ ‎) 005A 0335 LATIN CAPITAL LETTER Z, COMBINING SHORT STROKE OVERLAY +← (‎ Ƶ ‎) 01B5 LATIN CAPITAL LETTER Z WITH STROKE + +# \ 丶 ∖ ⟍ ⧵ ⧹ ㇔ 𝈏 𝈻 ⼂ ﹨ \ + (‎ \ ‎) 005C REVERSE SOLIDUS +← (‎ 丶 ‎) 4E36 CJK UNIFIED IDEOGRAPH-4E36 # →⼂→ +← (‎ ∖ ‎) 2216 SET MINUS +← (‎ ⟍ ‎) 27CD MATHEMATICAL FALLING DIAGONAL +← (‎ ⧵ ‎) 29F5 REVERSE SOLIDUS OPERATOR +← (‎ ⧹ ‎) 29F9 BIG REVERSE SOLIDUS +← (‎ ㇔ ‎) 31D4 CJK STROKE D # →⼂→ +← (‎ 𝈏 ‎) 1D20F GREEK VOCAL NOTATION SYMBOL-16 +← (‎ 𝈻 ‎) 1D23B GREEK INSTRUMENTAL NOTATION SYMBOL-48 # →𝈏→ +← (‎ ⼂ ‎) 2F02 KANGXI RADICAL DOT +← (‎ ﹨ ‎) FE68 SMALL REVERSE SOLIDUS # →∖→ +← (‎ \ ‎) FF3C FULLWIDTH REVERSE SOLIDUS # →∖→ + +# \\ ⑊ ⳹ + (‎ \\ ‎) 005C 005C REVERSE SOLIDUS, REVERSE SOLIDUS +← (‎ ⑊ ‎) 244A OCR DOUBLE BACKSLASH +← (‎ ⳹ ‎) 2CF9 COPTIC OLD NUBIAN FULL STOP + +# \ᑕ \⊂ ⟈ + (‎ \ᑕ ‎) 005C 1455 REVERSE SOLIDUS, CANADIAN SYLLABICS TA +← (‎ \⊂ ‎) 005C 2282 REVERSE SOLIDUS, SUBSET OF +← (‎ ⟈ ‎) 27C8 REVERSE SOLIDUS PRECEDING SUBSET # →\⊂→ + +# ^ ˆ ˄ + (‎ ^ ‎) 005E CIRCUMFLEX ACCENT +← (‎ ˆ ‎) 02C6 MODIFIER LETTER CIRCUMFLEX ACCENT +← (‎ ˄ ‎) 02C4 MODIFIER LETTER UP ARROWHEAD + +# _ ߺ ﹍ ﹎ ﹏ + (‎ _ ‎) 005F LOW LINE +← (‎ ߺ ‎) 07FA NKO LAJANYALAN +← (‎ ﹍ ‎) FE4D DASHED LOW LINE +← (‎ ﹎ ‎) FE4E CENTRELINE LOW LINE +← (‎ ﹏ ‎) FE4F WAVY LOW LINE + +# a ɑ α а ⍺ a 𝐚 𝑎 𝒂 𝒶 𝓪 𝔞 𝕒 𝖆 𝖺 𝗮 𝘢 𝙖 𝚊 𝛂 𝛼 𝜶 𝝰 𝞪 + (‎ a ‎) 0061 LATIN SMALL LETTER A +← (‎ ɑ ‎) 0251 LATIN SMALL LETTER ALPHA +← (‎ α ‎) 03B1 GREEK SMALL LETTER ALPHA +← (‎ а ‎) 0430 CYRILLIC SMALL LETTER A +← (‎ ⍺ ‎) 237A APL FUNCTIONAL SYMBOL ALPHA # →α→ +← (‎ a ‎) FF41 FULLWIDTH LATIN SMALL LETTER A # →а→ +← (‎ 𝐚 ‎) 1D41A MATHEMATICAL BOLD SMALL A +← (‎ 𝑎 ‎) 1D44E MATHEMATICAL ITALIC SMALL A +← (‎ 𝒂 ‎) 1D482 MATHEMATICAL BOLD ITALIC SMALL A +← (‎ 𝒶 ‎) 1D4B6 MATHEMATICAL SCRIPT SMALL A +← (‎ 𝓪 ‎) 1D4EA MATHEMATICAL BOLD SCRIPT SMALL A +← (‎ 𝔞 ‎) 1D51E MATHEMATICAL FRAKTUR SMALL A +← (‎ 𝕒 ‎) 1D552 MATHEMATICAL DOUBLE-STRUCK SMALL A +← (‎ 𝖆 ‎) 1D586 MATHEMATICAL BOLD FRAKTUR SMALL A +← (‎ 𝖺 ‎) 1D5BA MATHEMATICAL SANS-SERIF SMALL A +← (‎ 𝗮 ‎) 1D5EE MATHEMATICAL SANS-SERIF BOLD SMALL A +← (‎ 𝘢 ‎) 1D622 MATHEMATICAL SANS-SERIF ITALIC SMALL A +← (‎ 𝙖 ‎) 1D656 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL A +← (‎ 𝚊 ‎) 1D68A MATHEMATICAL MONOSPACE SMALL A +← (‎ 𝛂 ‎) 1D6C2 MATHEMATICAL BOLD SMALL ALPHA # →α→ +← (‎ 𝛼 ‎) 1D6FC MATHEMATICAL ITALIC SMALL ALPHA # →α→ +← (‎ 𝜶 ‎) 1D736 MATHEMATICAL BOLD ITALIC SMALL ALPHA # →α→ +← (‎ 𝝰 ‎) 1D770 MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA # →α→ +← (‎ 𝞪 ‎) 1D7AA MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA # →α→ + +# a/c ᵃ/c ᵃ⁄c ℀ + (‎ a/c ‎) 0061 002F 0063 LATIN SMALL LETTER A, SOLIDUS, LATIN SMALL LETTER C +← (‎ ᵃ/c ‎) 1D43 002F 0063 MODIFIER LETTER SMALL A, SOLIDUS, LATIN SMALL LETTER C # →ᵃ⁄c→→℀→ +← (‎ ᵃ⁄c ‎) 1D43 2044 0063 MODIFIER LETTER SMALL A, FRACTION SLASH, LATIN SMALL LETTER C # →℀→ +← (‎ ℀ ‎) 2100 ACCOUNT OF + +# a/s ᵃ/ₛ ᵃ⁄ₛ ℁ + (‎ a/s ‎) 0061 002F 0073 LATIN SMALL LETTER A, SOLIDUS, LATIN SMALL LETTER S +← (‎ ᵃ/ₛ ‎) 1D43 002F 209B MODIFIER LETTER SMALL A, SOLIDUS, LATIN SUBSCRIPT SMALL LETTER S # →ᵃ⁄ₛ→→℁→ +← (‎ ᵃ⁄ₛ ‎) 1D43 2044 209B MODIFIER LETTER SMALL A, FRACTION SLASH, LATIN SUBSCRIPT SMALL LETTER S # →℁→ +← (‎ ℁ ‎) 2101 ADDRESSED TO THE SUBJECT + +# aa ꜳ + (‎ aa ‎) 0061 0061 LATIN SMALL LETTER A, LATIN SMALL LETTER A +← (‎ ꜳ ‎) A733 LATIN SMALL LETTER AA + +# ae ае æ ӕ + (‎ ae ‎) 0061 0065 LATIN SMALL LETTER A, LATIN SMALL LETTER E +← (‎ ае ‎) 0430 0435 CYRILLIC SMALL LETTER A, CYRILLIC SMALL LETTER IE +← (‎ æ ‎) 00E6 LATIN SMALL LETTER AE +← (‎ ӕ ‎) 04D5 CYRILLIC SMALL LIGATURE A IE # →ае→ + +# ao ꜵ + (‎ ao ‎) 0061 006F LATIN SMALL LETTER A, LATIN SMALL LETTER O +← (‎ ꜵ ‎) A735 LATIN SMALL LETTER AO + +# au ꜷ + (‎ au ‎) 0061 0075 LATIN SMALL LETTER A, LATIN SMALL LETTER U +← (‎ ꜷ ‎) A737 LATIN SMALL LETTER AU + +# av ꜹ ꜻ + (‎ av ‎) 0061 0076 LATIN SMALL LETTER A, LATIN SMALL LETTER V +← (‎ ꜹ ‎) A739 LATIN SMALL LETTER AV +← (‎ ꜻ ‎) A73B LATIN SMALL LETTER AV WITH HORIZONTAL BAR + +# ay ꜽ + (‎ ay ‎) 0061 0079 LATIN SMALL LETTER A, LATIN SMALL LETTER Y +← (‎ ꜽ ‎) A73D LATIN SMALL LETTER AY + +# a̲ ɑ̲ α̲ ⍶ + (‎ a̲ ‎) 0061 0332 LATIN SMALL LETTER A, COMBINING LOW LINE +← (‎ ɑ̲ ‎) 0251 0332 LATIN SMALL LETTER ALPHA, COMBINING LOW LINE +← (‎ α̲ ‎) 03B1 0332 GREEK SMALL LETTER ALPHA, COMBINING LOW LINE # →ɑ̲→ +← (‎ ⍶ ‎) 2376 APL FUNCTIONAL SYMBOL ALPHA UNDERBAR # →α̲→→ɑ̲→ + +# b Ƅ Ь Ꮟ ᑲ ᖯ 𝐛 𝑏 𝒃 𝒷 𝓫 𝔟 𝕓 𝖇 𝖻 𝗯 𝘣 𝙗 𝚋 + (‎ b ‎) 0062 LATIN SMALL LETTER B +← (‎ Ƅ ‎) 0184 LATIN CAPITAL LETTER TONE SIX +← (‎ Ь ‎) 042C CYRILLIC CAPITAL LETTER SOFT SIGN # →Ƅ→ +← (‎ Ꮟ ‎) 13CF CHEROKEE LETTER SI +← (‎ ᑲ ‎) 1472 CANADIAN SYLLABICS KA +← (‎ ᖯ ‎) 15AF CANADIAN SYLLABICS AIVILIK B +← (‎ 𝐛 ‎) 1D41B MATHEMATICAL BOLD SMALL B +← (‎ 𝑏 ‎) 1D44F MATHEMATICAL ITALIC SMALL B +← (‎ 𝒃 ‎) 1D483 MATHEMATICAL BOLD ITALIC SMALL B +← (‎ 𝒷 ‎) 1D4B7 MATHEMATICAL SCRIPT SMALL B +← (‎ 𝓫 ‎) 1D4EB MATHEMATICAL BOLD SCRIPT SMALL B +← (‎ 𝔟 ‎) 1D51F MATHEMATICAL FRAKTUR SMALL B +← (‎ 𝕓 ‎) 1D553 MATHEMATICAL DOUBLE-STRUCK SMALL B +← (‎ 𝖇 ‎) 1D587 MATHEMATICAL BOLD FRAKTUR SMALL B +← (‎ 𝖻 ‎) 1D5BB MATHEMATICAL SANS-SERIF SMALL B +← (‎ 𝗯 ‎) 1D5EF MATHEMATICAL SANS-SERIF BOLD SMALL B +← (‎ 𝘣 ‎) 1D623 MATHEMATICAL SANS-SERIF ITALIC SMALL B +← (‎ 𝙗 ‎) 1D657 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL B +← (‎ 𝚋 ‎) 1D68B MATHEMATICAL MONOSPACE SMALL B + +# b' ᑲᑊ ᑲ' ᒈ + (‎ b' ‎) 0062 0027 LATIN SMALL LETTER B, APOSTROPHE +← (‎ ᑲᑊ ‎) 1472 144A CANADIAN SYLLABICS KA, CANADIAN SYLLABICS WEST-CREE P +← (‎ ᑲ' ‎) 1472 0027 CANADIAN SYLLABICS KA, APOSTROPHE # →ᑲᑊ→ +← (‎ ᒈ ‎) 1488 CANADIAN SYLLABICS SOUTH-SLAVEY KAH # →ᑲᑊ→ + +# bl Ьl Ь1 ЬІ Ы + (‎ bl ‎) 0062 006C LATIN SMALL LETTER B, LATIN SMALL LETTER L +← (‎ Ьl ‎) 042C 006C CYRILLIC CAPITAL LETTER SOFT SIGN, LATIN SMALL LETTER L # →ЬІ→→Ь1→ +← (‎ Ь1 ‎) 042C 0031 CYRILLIC CAPITAL LETTER SOFT SIGN, DIGIT ONE +← (‎ ЬІ ‎) 042C 0406 CYRILLIC CAPITAL LETTER SOFT SIGN, CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I # →Ь1→ +← (‎ Ы ‎) 042B CYRILLIC CAPITAL LETTER YERU # →ЬІ→→Ь1→ + +# b· ᑲ· ᑲᐧ ᑿ + (‎ b· ‎) 0062 00B7 LATIN SMALL LETTER B, MIDDLE DOT +← (‎ ᑲ· ‎) 1472 00B7 CANADIAN SYLLABICS KA, MIDDLE DOT # →ᑲᐧ→ +← (‎ ᑲᐧ ‎) 1472 1427 CANADIAN SYLLABICS KA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑿ ‎) 147F CANADIAN SYLLABICS WEST-CREE KWA # →ᑲᐧ→ + +# b̄ Ƃ ƃ Б + (‎ b̄ ‎) 0062 0304 LATIN SMALL LETTER B, COMBINING MACRON +← (‎ Ƃ ‎) 0182 LATIN CAPITAL LETTER B WITH TOPBAR +← (‎ ƃ ‎) 0183 LATIN SMALL LETTER B WITH TOPBAR +← (‎ Б ‎) 0411 CYRILLIC CAPITAL LETTER BE # →Ƃ→ + +# ḃ ᑳ + (‎ ḃ ‎) 0062 0307 LATIN SMALL LETTER B, COMBINING DOT ABOVE +← (‎ ᑳ ‎) 1473 CANADIAN SYLLABICS KAA + +# ḃ· ᑳ· ᑳᐧ ᒁ + (‎ ḃ· ‎) 0062 0307 00B7 LATIN SMALL LETTER B, COMBINING DOT ABOVE, MIDDLE DOT +← (‎ ᑳ· ‎) 1473 00B7 CANADIAN SYLLABICS KAA, MIDDLE DOT # →ᑳᐧ→ +← (‎ ᑳᐧ ‎) 1473 1427 CANADIAN SYLLABICS KAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒁ ‎) 1481 CANADIAN SYLLABICS WEST-CREE KWAA # →ᑳᐧ→ + +# b̔ ɓ + (‎ b̔ ‎) 0062 0314 LATIN SMALL LETTER B, COMBINING REVERSED COMMA ABOVE +← (‎ ɓ ‎) 0253 LATIN SMALL LETTER B WITH HOOK + +# b̵ Ь̵ ƀ ҍ Ҍ Ѣ ѣ + (‎ b̵ ‎) 0062 0335 LATIN SMALL LETTER B, COMBINING SHORT STROKE OVERLAY +← (‎ Ь̵ ‎) 042C 0335 CYRILLIC CAPITAL LETTER SOFT SIGN, COMBINING SHORT STROKE OVERLAY +← (‎ ƀ ‎) 0180 LATIN SMALL LETTER B WITH STROKE +← (‎ ҍ ‎) 048D CYRILLIC SMALL LETTER SEMISOFT SIGN # →ѣ→→Ь̵→ +← (‎ Ҍ ‎) 048C CYRILLIC CAPITAL LETTER SEMISOFT SIGN # →Ѣ→→Ь̵→ +← (‎ Ѣ ‎) 0462 CYRILLIC CAPITAL LETTER YAT # →Ь̵→ +← (‎ ѣ ‎) 0463 CYRILLIC SMALL LETTER YAT # →Ь̵→ + +# c ᴄ с ⲥ 𐐽 ꮯ ⅽ c ϲ 𝐜 𝑐 𝒄 𝒸 𝓬 𝔠 𝕔 𝖈 𝖼 𝗰 𝘤 𝙘 𝚌 + (‎ c ‎) 0063 LATIN SMALL LETTER C +← (‎ ᴄ ‎) 1D04 LATIN LETTER SMALL CAPITAL C +← (‎ с ‎) 0441 CYRILLIC SMALL LETTER ES +← (‎ ⲥ ‎) 2CA5 COPTIC SMALL LETTER SIMA # →ϲ→ +← (‎ 𐐽 ‎) 1043D DESERET SMALL LETTER CHEE +← (‎ ꮯ ‎) ABAF CHEROKEE SMALL LETTER TLI # →ᴄ→ +← (‎ ⅽ ‎) 217D SMALL ROMAN NUMERAL ONE HUNDRED +← (‎ c ‎) FF43 FULLWIDTH LATIN SMALL LETTER C # →с→ +← (‎ ϲ ‎) 03F2 GREEK LUNATE SIGMA SYMBOL +← (‎ 𝐜 ‎) 1D41C MATHEMATICAL BOLD SMALL C +← (‎ 𝑐 ‎) 1D450 MATHEMATICAL ITALIC SMALL C +← (‎ 𝒄 ‎) 1D484 MATHEMATICAL BOLD ITALIC SMALL C +← (‎ 𝒸 ‎) 1D4B8 MATHEMATICAL SCRIPT SMALL C +← (‎ 𝓬 ‎) 1D4EC MATHEMATICAL BOLD SCRIPT SMALL C +← (‎ 𝔠 ‎) 1D520 MATHEMATICAL FRAKTUR SMALL C +← (‎ 𝕔 ‎) 1D554 MATHEMATICAL DOUBLE-STRUCK SMALL C +← (‎ 𝖈 ‎) 1D588 MATHEMATICAL BOLD FRAKTUR SMALL C +← (‎ 𝖼 ‎) 1D5BC MATHEMATICAL SANS-SERIF SMALL C +← (‎ 𝗰 ‎) 1D5F0 MATHEMATICAL SANS-SERIF BOLD SMALL C +← (‎ 𝘤 ‎) 1D624 MATHEMATICAL SANS-SERIF ITALIC SMALL C +← (‎ 𝙘 ‎) 1D658 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL C +← (‎ 𝚌 ‎) 1D68C MATHEMATICAL MONOSPACE SMALL C + +# c/o ᶜ/₀ ᶜ⁄₀ ℅ + (‎ c/o ‎) 0063 002F 006F LATIN SMALL LETTER C, SOLIDUS, LATIN SMALL LETTER O +← (‎ ᶜ/₀ ‎) 1D9C 002F 2080 MODIFIER LETTER SMALL C, SOLIDUS, SUBSCRIPT ZERO # →ᶜ⁄₀→→℅→ +← (‎ ᶜ⁄₀ ‎) 1D9C 2044 2080 MODIFIER LETTER SMALL C, FRACTION SLASH, SUBSCRIPT ZERO # →℅→ +← (‎ ℅ ‎) 2105 CARE OF + +# c/u ᶜ/ᵤ ᶜ⁄ᵤ ℆ + (‎ c/u ‎) 0063 002F 0075 LATIN SMALL LETTER C, SOLIDUS, LATIN SMALL LETTER U +← (‎ ᶜ/ᵤ ‎) 1D9C 002F 1D64 MODIFIER LETTER SMALL C, SOLIDUS, LATIN SUBSCRIPT SMALL LETTER U # →ᶜ⁄ᵤ→→℆→ +← (‎ ᶜ⁄ᵤ ‎) 1D9C 2044 1D64 MODIFIER LETTER SMALL C, FRACTION SLASH, LATIN SUBSCRIPT SMALL LETTER U # →℆→ +← (‎ ℆ ‎) 2106 CADA UNA + +# c̦ с̦ с̡ ҫ ç + (‎ c̦ ‎) 0063 0326 LATIN SMALL LETTER C, COMBINING COMMA BELOW +← (‎ с̦ ‎) 0441 0326 CYRILLIC SMALL LETTER ES, COMBINING COMMA BELOW # →с̡→ +← (‎ с̡ ‎) 0441 0321 CYRILLIC SMALL LETTER ES, COMBINING PALATALIZED HOOK BELOW +← (‎ ҫ ‎) 04AB CYRILLIC SMALL LETTER ES WITH DESCENDER # →с̡→ +← (‎ ç ‎) 00E7 LATIN SMALL LETTER C WITH CEDILLA # →ҫ→→с̡→ + +# c̸ ȼ ¢ + (‎ c̸ ‎) 0063 0338 LATIN SMALL LETTER C, COMBINING LONG SOLIDUS OVERLAY +← (‎ ȼ ‎) 023C LATIN SMALL LETTER C WITH STROKE # →¢→ +← (‎ ¢ ‎) 00A2 CENT SIGN + +# d ԁ Ꮷ ᑯ ꓒ ⅾ ⅆ 𝐝 𝑑 𝒅 𝒹 𝓭 𝔡 𝕕 𝖉 𝖽 𝗱 𝘥 𝙙 𝚍 + (‎ d ‎) 0064 LATIN SMALL LETTER D +← (‎ ԁ ‎) 0501 CYRILLIC SMALL LETTER KOMI DE +← (‎ Ꮷ ‎) 13E7 CHEROKEE LETTER TSU +← (‎ ᑯ ‎) 146F CANADIAN SYLLABICS KO +← (‎ ꓒ ‎) A4D2 LISU LETTER PHA +← (‎ ⅾ ‎) 217E SMALL ROMAN NUMERAL FIVE HUNDRED +← (‎ ⅆ ‎) 2146 DOUBLE-STRUCK ITALIC SMALL D +← (‎ 𝐝 ‎) 1D41D MATHEMATICAL BOLD SMALL D +← (‎ 𝑑 ‎) 1D451 MATHEMATICAL ITALIC SMALL D +← (‎ 𝒅 ‎) 1D485 MATHEMATICAL BOLD ITALIC SMALL D +← (‎ 𝒹 ‎) 1D4B9 MATHEMATICAL SCRIPT SMALL D +← (‎ 𝓭 ‎) 1D4ED MATHEMATICAL BOLD SCRIPT SMALL D +← (‎ 𝔡 ‎) 1D521 MATHEMATICAL FRAKTUR SMALL D +← (‎ 𝕕 ‎) 1D555 MATHEMATICAL DOUBLE-STRUCK SMALL D +← (‎ 𝖉 ‎) 1D589 MATHEMATICAL BOLD FRAKTUR SMALL D +← (‎ 𝖽 ‎) 1D5BD MATHEMATICAL SANS-SERIF SMALL D +← (‎ 𝗱 ‎) 1D5F1 MATHEMATICAL SANS-SERIF BOLD SMALL D +← (‎ 𝘥 ‎) 1D625 MATHEMATICAL SANS-SERIF ITALIC SMALL D +← (‎ 𝙙 ‎) 1D659 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL D +← (‎ 𝚍 ‎) 1D68D MATHEMATICAL MONOSPACE SMALL D + +# d' ᑯᑊ ᑯ' ᒇ + (‎ d' ‎) 0064 0027 LATIN SMALL LETTER D, APOSTROPHE +← (‎ ᑯᑊ ‎) 146F 144A CANADIAN SYLLABICS KO, CANADIAN SYLLABICS WEST-CREE P +← (‎ ᑯ' ‎) 146F 0027 CANADIAN SYLLABICS KO, APOSTROPHE # →ᑯᑊ→ +← (‎ ᒇ ‎) 1487 CANADIAN SYLLABICS SOUTH-SLAVEY KOH # →ᑯᑊ→ + +# dz ʣ dz + (‎ dz ‎) 0064 007A LATIN SMALL LETTER D, LATIN SMALL LETTER Z +← (‎ ʣ ‎) 02A3 LATIN SMALL LETTER DZ DIGRAPH +← (‎ dz ‎) 01F3 LATIN SMALL LETTER DZ + +# d· ᑯ· ᑯᐧ ᑻ + (‎ d· ‎) 0064 00B7 LATIN SMALL LETTER D, MIDDLE DOT +← (‎ ᑯ· ‎) 146F 00B7 CANADIAN SYLLABICS KO, MIDDLE DOT # →ᑯᐧ→ +← (‎ ᑯᐧ ‎) 146F 1427 CANADIAN SYLLABICS KO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑻ ‎) 147B CANADIAN SYLLABICS WEST-CREE KWO # →ᑯᐧ→ + +# dž dž + (‎ dž ‎) 0064 017E LATIN SMALL LETTER D, LATIN SMALL LETTER Z WITH CARON +← (‎ dž ‎) 01C6 LATIN SMALL LETTER DZ WITH CARON + +# dȝ dʒ ʤ + (‎ dȝ ‎) 0064 021D LATIN SMALL LETTER D, LATIN SMALL LETTER YOGH +← (‎ dʒ ‎) 0064 0292 LATIN SMALL LETTER D, LATIN SMALL LETTER EZH +← (‎ ʤ ‎) 02A4 LATIN SMALL LETTER DEZH DIGRAPH # →dʒ→ + +# dʑ ʥ + (‎ dʑ ‎) 0064 0291 LATIN SMALL LETTER D, LATIN SMALL LETTER Z WITH CURL +← (‎ ʥ ‎) 02A5 LATIN SMALL LETTER DZ DIGRAPH WITH CURL + +# d̄ ƌ + (‎ d̄ ‎) 0064 0304 LATIN SMALL LETTER D, COMBINING MACRON +← (‎ ƌ ‎) 018C LATIN SMALL LETTER D WITH TOPBAR + +# d̔ ɗ + (‎ d̔ ‎) 0064 0314 LATIN SMALL LETTER D, COMBINING REVERSED COMMA ABOVE +← (‎ ɗ ‎) 0257 LATIN SMALL LETTER D WITH HOOK + +# d̨ d̢ ɖ + (‎ d̢ ‎) 0064 0322 LATIN SMALL LETTER D, COMBINING RETROFLEX HOOK BELOW +← (‎ d̨ ‎) 0064 0328 LATIN SMALL LETTER D, COMBINING OGONEK +← (‎ ɖ ‎) 0256 LATIN SMALL LETTER D WITH TAIL + +# d̵ đ + (‎ d̵ ‎) 0064 0335 LATIN SMALL LETTER D, COMBINING SHORT STROKE OVERLAY +← (‎ đ ‎) 0111 LATIN SMALL LETTER D WITH STROKE + +# ḏ̵ đ̱ ₫ + (‎ ḏ̵ ‎) 0064 0335 0331 LATIN SMALL LETTER D, COMBINING SHORT STROKE OVERLAY, COMBINING MACRON BELOW +← (‎ đ̱ ‎) 0111 0331 LATIN SMALL LETTER D WITH STROKE, COMBINING MACRON BELOW +← (‎ ₫ ‎) 20AB DONG SIGN # →đ̱→ + +# e е ҽ ℮ ꬲ e ℯ ⅇ 𝐞 𝑒 𝒆 𝓮 𝔢 𝕖 𝖊 𝖾 𝗲 𝘦 𝙚 𝚎 + (‎ e ‎) 0065 LATIN SMALL LETTER E +← (‎ е ‎) 0435 CYRILLIC SMALL LETTER IE +← (‎ ҽ ‎) 04BD CYRILLIC SMALL LETTER ABKHASIAN CHE +← (‎ ℮ ‎) 212E ESTIMATED SYMBOL +← (‎ ꬲ ‎) AB32 LATIN SMALL LETTER BLACKLETTER E +← (‎ e ‎) FF45 FULLWIDTH LATIN SMALL LETTER E # →е→ +← (‎ ℯ ‎) 212F SCRIPT SMALL E +← (‎ ⅇ ‎) 2147 DOUBLE-STRUCK ITALIC SMALL E +← (‎ 𝐞 ‎) 1D41E MATHEMATICAL BOLD SMALL E +← (‎ 𝑒 ‎) 1D452 MATHEMATICAL ITALIC SMALL E +← (‎ 𝒆 ‎) 1D486 MATHEMATICAL BOLD ITALIC SMALL E +← (‎ 𝓮 ‎) 1D4EE MATHEMATICAL BOLD SCRIPT SMALL E +← (‎ 𝔢 ‎) 1D522 MATHEMATICAL FRAKTUR SMALL E +← (‎ 𝕖 ‎) 1D556 MATHEMATICAL DOUBLE-STRUCK SMALL E +← (‎ 𝖊 ‎) 1D58A MATHEMATICAL BOLD FRAKTUR SMALL E +← (‎ 𝖾 ‎) 1D5BE MATHEMATICAL SANS-SERIF SMALL E +← (‎ 𝗲 ‎) 1D5F2 MATHEMATICAL SANS-SERIF BOLD SMALL E +← (‎ 𝘦 ‎) 1D626 MATHEMATICAL SANS-SERIF ITALIC SMALL E +← (‎ 𝙚 ‎) 1D65A MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL E +← (‎ 𝚎 ‎) 1D68E MATHEMATICAL MONOSPACE SMALL E + +# ę е̨ ҽ̢ ҿ + (‎ ę ‎) 0065 0328 LATIN SMALL LETTER E, COMBINING OGONEK +← (‎ е̨ ‎) 0435 0328 CYRILLIC SMALL LETTER IE, COMBINING OGONEK # →ҽ̢→ +← (‎ ҽ̢ ‎) 04BD 0322 CYRILLIC SMALL LETTER ABKHASIAN CHE, COMBINING RETROFLEX HOOK BELOW +← (‎ ҿ ‎) 04BF CYRILLIC SMALL LETTER ABKHASIAN CHE WITH DESCENDER # →ҽ̢→ + +# e̸ e̷ ɇ + (‎ e̷ ‎) 0065 0337 LATIN SMALL LETTER E, COMBINING SHORT SOLIDUS OVERLAY +← (‎ e̸ ‎) 0065 0338 LATIN SMALL LETTER E, COMBINING LONG SOLIDUS OVERLAY +← (‎ ɇ ‎) 0247 LATIN SMALL LETTER E WITH STROKE + +# f ẝ ք ꞙ ꬵ ſ 𝐟 𝑓 𝒇 𝒻 𝓯 𝔣 𝕗 𝖋 𝖿 𝗳 𝘧 𝙛 𝚏 + (‎ f ‎) 0066 LATIN SMALL LETTER F +← (‎ ẝ ‎) 1E9D LATIN SMALL LETTER LONG S WITH HIGH STROKE +← (‎ ք ‎) 0584 ARMENIAN SMALL LETTER KEH +← (‎ ꞙ ‎) A799 LATIN SMALL LETTER F WITH STROKE +← (‎ ꬵ ‎) AB35 LATIN SMALL LETTER LENIS F +← (‎ ſ ‎) 017F LATIN SMALL LETTER LONG S +← (‎ 𝐟 ‎) 1D41F MATHEMATICAL BOLD SMALL F +← (‎ 𝑓 ‎) 1D453 MATHEMATICAL ITALIC SMALL F +← (‎ 𝒇 ‎) 1D487 MATHEMATICAL BOLD ITALIC SMALL F +← (‎ 𝒻 ‎) 1D4BB MATHEMATICAL SCRIPT SMALL F +← (‎ 𝓯 ‎) 1D4EF MATHEMATICAL BOLD SCRIPT SMALL F +← (‎ 𝔣 ‎) 1D523 MATHEMATICAL FRAKTUR SMALL F +← (‎ 𝕗 ‎) 1D557 MATHEMATICAL DOUBLE-STRUCK SMALL F +← (‎ 𝖋 ‎) 1D58B MATHEMATICAL BOLD FRAKTUR SMALL F +← (‎ 𝖿 ‎) 1D5BF MATHEMATICAL SANS-SERIF SMALL F +← (‎ 𝗳 ‎) 1D5F3 MATHEMATICAL SANS-SERIF BOLD SMALL F +← (‎ 𝘧 ‎) 1D627 MATHEMATICAL SANS-SERIF ITALIC SMALL F +← (‎ 𝙛 ‎) 1D65B MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL F +← (‎ 𝚏 ‎) 1D68F MATHEMATICAL MONOSPACE SMALL F + +# ff ff + (‎ ff ‎) 0066 0066 LATIN SMALL LETTER F, LATIN SMALL LETTER F +← (‎ ff ‎) FB00 LATIN SMALL LIGATURE FF + +# ffi ffi + (‎ ffi ‎) 0066 0066 0069 LATIN SMALL LETTER F, LATIN SMALL LETTER F, LATIN SMALL LETTER I +← (‎ ffi ‎) FB03 LATIN SMALL LIGATURE FFI + +# ffl ffl + (‎ ffl ‎) 0066 0066 006C LATIN SMALL LETTER F, LATIN SMALL LETTER F, LATIN SMALL LETTER L +← (‎ ffl ‎) FB04 LATIN SMALL LIGATURE FFL + +# fi fi + (‎ fi ‎) 0066 0069 LATIN SMALL LETTER F, LATIN SMALL LETTER I +← (‎ fi ‎) FB01 LATIN SMALL LIGATURE FI + +# fl fl + (‎ fl ‎) 0066 006C LATIN SMALL LETTER F, LATIN SMALL LETTER L +← (‎ fl ‎) FB02 LATIN SMALL LIGATURE FL + +# fŋ ʩ + (‎ fŋ ‎) 0066 014B LATIN SMALL LETTER F, LATIN SMALL LETTER ENG +← (‎ ʩ ‎) 02A9 LATIN SMALL LETTER FENG DIGRAPH + +# f̦ f̡ ƒ + (‎ f̡ ‎) 0066 0321 LATIN SMALL LETTER F, COMBINING PALATALIZED HOOK BELOW +← (‎ f̦ ‎) 0066 0326 LATIN SMALL LETTER F, COMBINING COMMA BELOW +← (‎ ƒ ‎) 0192 LATIN SMALL LETTER F WITH HOOK + +# f̴ ᵮ + (‎ f̴ ‎) 0066 0334 LATIN SMALL LETTER F, COMBINING TILDE OVERLAY +← (‎ ᵮ ‎) 1D6E LATIN SMALL LETTER F WITH MIDDLE TILDE + +# g ƍ ɡ ᶃ ց g ℊ 𝐠 𝑔 𝒈 𝓰 𝔤 𝕘 𝖌 𝗀 𝗴 𝘨 𝙜 𝚐 + (‎ g ‎) 0067 LATIN SMALL LETTER G +← (‎ ƍ ‎) 018D LATIN SMALL LETTER TURNED DELTA +← (‎ ɡ ‎) 0261 LATIN SMALL LETTER SCRIPT G +← (‎ ᶃ ‎) 1D83 LATIN SMALL LETTER G WITH PALATAL HOOK +← (‎ ց ‎) 0581 ARMENIAN SMALL LETTER CO +← (‎ g ‎) FF47 FULLWIDTH LATIN SMALL LETTER G # →ɡ→ +← (‎ ℊ ‎) 210A SCRIPT SMALL G +← (‎ 𝐠 ‎) 1D420 MATHEMATICAL BOLD SMALL G +← (‎ 𝑔 ‎) 1D454 MATHEMATICAL ITALIC SMALL G +← (‎ 𝒈 ‎) 1D488 MATHEMATICAL BOLD ITALIC SMALL G +← (‎ 𝓰 ‎) 1D4F0 MATHEMATICAL BOLD SCRIPT SMALL G +← (‎ 𝔤 ‎) 1D524 MATHEMATICAL FRAKTUR SMALL G +← (‎ 𝕘 ‎) 1D558 MATHEMATICAL DOUBLE-STRUCK SMALL G +← (‎ 𝖌 ‎) 1D58C MATHEMATICAL BOLD FRAKTUR SMALL G +← (‎ 𝗀 ‎) 1D5C0 MATHEMATICAL SANS-SERIF SMALL G +← (‎ 𝗴 ‎) 1D5F4 MATHEMATICAL SANS-SERIF BOLD SMALL G +← (‎ 𝘨 ‎) 1D628 MATHEMATICAL SANS-SERIF ITALIC SMALL G +← (‎ 𝙜 ‎) 1D65C MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL G +← (‎ 𝚐 ‎) 1D690 MATHEMATICAL MONOSPACE SMALL G + +# g̔ ɠ + (‎ g̔ ‎) 0067 0314 LATIN SMALL LETTER G, COMBINING REVERSED COMMA ABOVE +← (‎ ɠ ‎) 0260 LATIN SMALL LETTER G WITH HOOK + +# g̵ ǥ + (‎ g̵ ‎) 0067 0335 LATIN SMALL LETTER G, COMBINING SHORT STROKE OVERLAY +← (‎ ǥ ‎) 01E5 LATIN SMALL LETTER G WITH STROKE + +# h һ հ Ꮒ h ℎ 𝐡 𝒉 𝒽 𝓱 𝔥 𝕙 𝖍 𝗁 𝗵 𝘩 𝙝 𝚑 + (‎ h ‎) 0068 LATIN SMALL LETTER H +← (‎ һ ‎) 04BB CYRILLIC SMALL LETTER SHHA +← (‎ հ ‎) 0570 ARMENIAN SMALL LETTER HO +← (‎ Ꮒ ‎) 13C2 CHEROKEE LETTER NI +← (‎ h ‎) FF48 FULLWIDTH LATIN SMALL LETTER H # →һ→ +← (‎ ℎ ‎) 210E PLANCK CONSTANT +← (‎ 𝐡 ‎) 1D421 MATHEMATICAL BOLD SMALL H +← (‎ 𝒉 ‎) 1D489 MATHEMATICAL BOLD ITALIC SMALL H +← (‎ 𝒽 ‎) 1D4BD MATHEMATICAL SCRIPT SMALL H +← (‎ 𝓱 ‎) 1D4F1 MATHEMATICAL BOLD SCRIPT SMALL H +← (‎ 𝔥 ‎) 1D525 MATHEMATICAL FRAKTUR SMALL H +← (‎ 𝕙 ‎) 1D559 MATHEMATICAL DOUBLE-STRUCK SMALL H +← (‎ 𝖍 ‎) 1D58D MATHEMATICAL BOLD FRAKTUR SMALL H +← (‎ 𝗁 ‎) 1D5C1 MATHEMATICAL SANS-SERIF SMALL H +← (‎ 𝗵 ‎) 1D5F5 MATHEMATICAL SANS-SERIF BOLD SMALL H +← (‎ 𝘩 ‎) 1D629 MATHEMATICAL SANS-SERIF ITALIC SMALL H +← (‎ 𝙝 ‎) 1D65D MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL H +← (‎ 𝚑 ‎) 1D691 MATHEMATICAL MONOSPACE SMALL H + +# h̔ ɦ Ᏺ ꚕ + (‎ h̔ ‎) 0068 0314 LATIN SMALL LETTER H, COMBINING REVERSED COMMA ABOVE +← (‎ ɦ ‎) 0266 LATIN SMALL LETTER H WITH HOOK +← (‎ Ᏺ ‎) 13F2 CHEROKEE LETTER YO +← (‎ ꚕ ‎) A695 CYRILLIC SMALL LETTER HWE # →ɦ→ + +# h̵ һ̵ ħ ћ ℏ + (‎ h̵ ‎) 0068 0335 LATIN SMALL LETTER H, COMBINING SHORT STROKE OVERLAY +← (‎ һ̵ ‎) 04BB 0335 CYRILLIC SMALL LETTER SHHA, COMBINING SHORT STROKE OVERLAY # →ћ→→ħ→ +← (‎ ħ ‎) 0127 LATIN SMALL LETTER H WITH STROKE +← (‎ ћ ‎) 045B CYRILLIC SMALL LETTER TSHE # →ħ→ +← (‎ ℏ ‎) 210F PLANCK CONSTANT OVER TWO PI # →ħ→ + +# i ı ɩ ɪ ι і ӏ Ꭵ ꙇ ⍳ 𑣃 ꭵ ⅰ i ι ℹ ⅈ 𝐢 𝑖 𝒊 𝒾 𝓲 𝔦 𝕚 𝖎 𝗂 𝗶 𝘪 𝙞 𝚒 𝚤 𝛊 𝜄 𝜾 𝝸 𝞲 ˛ ͺ + (‎ i ‎) 0069 LATIN SMALL LETTER I +← (‎ ı ‎) 0131 LATIN SMALL LETTER DOTLESS I +← (‎ ɩ ‎) 0269 LATIN SMALL LETTER IOTA +← (‎ ɪ ‎) 026A LATIN LETTER SMALL CAPITAL I # →ı→ +← (‎ ι ‎) 03B9 GREEK SMALL LETTER IOTA +← (‎ і ‎) 0456 CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I +← (‎ ӏ ‎) 04CF CYRILLIC SMALL LETTER PALOCHKA # →ı→ +← (‎ Ꭵ ‎) 13A5 CHEROKEE LETTER V +← (‎ ꙇ ‎) A647 CYRILLIC SMALL LETTER IOTA # →ι→ +← (‎ ⍳ ‎) 2373 APL FUNCTIONAL SYMBOL IOTA # →ι→ +← (‎ 𑣃 ‎) 118C3 WARANG CITI SMALL LETTER YU # →ι→ +← (‎ ꭵ ‎) AB75 CHEROKEE SMALL LETTER V +← (‎ ⅰ ‎) 2170 SMALL ROMAN NUMERAL ONE +← (‎ i ‎) FF49 FULLWIDTH LATIN SMALL LETTER I # →і→ +← (‎ ι ‎) 1FBE GREEK PROSGEGRAMMENI # →ι→ +← (‎ ℹ ‎) 2139 INFORMATION SOURCE +← (‎ ⅈ ‎) 2148 DOUBLE-STRUCK ITALIC SMALL I +← (‎ 𝐢 ‎) 1D422 MATHEMATICAL BOLD SMALL I +← (‎ 𝑖 ‎) 1D456 MATHEMATICAL ITALIC SMALL I +← (‎ 𝒊 ‎) 1D48A MATHEMATICAL BOLD ITALIC SMALL I +← (‎ 𝒾 ‎) 1D4BE MATHEMATICAL SCRIPT SMALL I +← (‎ 𝓲 ‎) 1D4F2 MATHEMATICAL BOLD SCRIPT SMALL I +← (‎ 𝔦 ‎) 1D526 MATHEMATICAL FRAKTUR SMALL I +← (‎ 𝕚 ‎) 1D55A MATHEMATICAL DOUBLE-STRUCK SMALL I +← (‎ 𝖎 ‎) 1D58E MATHEMATICAL BOLD FRAKTUR SMALL I +← (‎ 𝗂 ‎) 1D5C2 MATHEMATICAL SANS-SERIF SMALL I +← (‎ 𝗶 ‎) 1D5F6 MATHEMATICAL SANS-SERIF BOLD SMALL I +← (‎ 𝘪 ‎) 1D62A MATHEMATICAL SANS-SERIF ITALIC SMALL I +← (‎ 𝙞 ‎) 1D65E MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I +← (‎ 𝚒 ‎) 1D692 MATHEMATICAL MONOSPACE SMALL I +← (‎ 𝚤 ‎) 1D6A4 MATHEMATICAL ITALIC SMALL DOTLESS I # →ı→ +← (‎ 𝛊 ‎) 1D6CA MATHEMATICAL BOLD SMALL IOTA # →ι→ +← (‎ 𝜄 ‎) 1D704 MATHEMATICAL ITALIC SMALL IOTA # →ι→ +← (‎ 𝜾 ‎) 1D73E MATHEMATICAL BOLD ITALIC SMALL IOTA # →ι→ +← (‎ 𝝸 ‎) 1D778 MATHEMATICAL SANS-SERIF BOLD SMALL IOTA # →ι→ +← (‎ 𝞲 ‎) 1D7B2 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL IOTA # →ι→ +← (‎ ˛ ‎) 02DB OGONEK # →ͺ→→ι→→ι→ +← (‎ ͺ ‎) 037A GREEK YPOGEGRAMMENI # →ι→→ι→ + +# ii ⅱ + (‎ ii ‎) 0069 0069 LATIN SMALL LETTER I, LATIN SMALL LETTER I +← (‎ ⅱ ‎) 2171 SMALL ROMAN NUMERAL TWO + +# iii ⅲ + (‎ iii ‎) 0069 0069 0069 LATIN SMALL LETTER I, LATIN SMALL LETTER I, LATIN SMALL LETTER I +← (‎ ⅲ ‎) 2172 SMALL ROMAN NUMERAL THREE + +# ij ij + (‎ ij ‎) 0069 006A LATIN SMALL LETTER I, LATIN SMALL LETTER J +← (‎ ij ‎) 0133 LATIN SMALL LIGATURE IJ + +# iv ⅳ + (‎ iv ‎) 0069 0076 LATIN SMALL LETTER I, LATIN SMALL LETTER V +← (‎ ⅳ ‎) 2173 SMALL ROMAN NUMERAL FOUR + +# ix ⅸ + (‎ ix ‎) 0069 0078 LATIN SMALL LETTER I, LATIN SMALL LETTER X +← (‎ ⅸ ‎) 2178 SMALL ROMAN NUMERAL NINE + +# i̲ ι̲ ⍸ + (‎ i̲ ‎) 0069 0332 LATIN SMALL LETTER I, COMBINING LOW LINE +← (‎ ι̲ ‎) 03B9 0332 GREEK SMALL LETTER IOTA, COMBINING LOW LINE +← (‎ ⍸ ‎) 2378 APL FUNCTIONAL SYMBOL IOTA UNDERBAR # →ι̲→ + +# i̵ ɩ̵ ɪ̵ ɨ ᵻ ᵼ + (‎ i̵ ‎) 0069 0335 LATIN SMALL LETTER I, COMBINING SHORT STROKE OVERLAY +← (‎ ɩ̵ ‎) 0269 0335 LATIN SMALL LETTER IOTA, COMBINING SHORT STROKE OVERLAY +← (‎ ɪ̵ ‎) 026A 0335 LATIN LETTER SMALL CAPITAL I, COMBINING SHORT STROKE OVERLAY +← (‎ ɨ ‎) 0268 LATIN SMALL LETTER I WITH STROKE +← (‎ ᵻ ‎) 1D7B LATIN SMALL CAPITAL LETTER I WITH STROKE # →ɪ̵→ +← (‎ ᵼ ‎) 1D7C LATIN SMALL LETTER IOTA WITH STROKE # →ɩ̵→ + +# j ј ϳ j ⅉ 𝐣 𝑗 𝒋 𝒿 𝓳 𝔧 𝕛 𝖏 𝗃 𝗷 𝘫 𝙟 𝚓 + (‎ j ‎) 006A LATIN SMALL LETTER J +← (‎ ј ‎) 0458 CYRILLIC SMALL LETTER JE +← (‎ ϳ ‎) 03F3 GREEK LETTER YOT +← (‎ j ‎) FF4A FULLWIDTH LATIN SMALL LETTER J # →ϳ→ +← (‎ ⅉ ‎) 2149 DOUBLE-STRUCK ITALIC SMALL J +← (‎ 𝐣 ‎) 1D423 MATHEMATICAL BOLD SMALL J +← (‎ 𝑗 ‎) 1D457 MATHEMATICAL ITALIC SMALL J +← (‎ 𝒋 ‎) 1D48B MATHEMATICAL BOLD ITALIC SMALL J +← (‎ 𝒿 ‎) 1D4BF MATHEMATICAL SCRIPT SMALL J +← (‎ 𝓳 ‎) 1D4F3 MATHEMATICAL BOLD SCRIPT SMALL J +← (‎ 𝔧 ‎) 1D527 MATHEMATICAL FRAKTUR SMALL J +← (‎ 𝕛 ‎) 1D55B MATHEMATICAL DOUBLE-STRUCK SMALL J +← (‎ 𝖏 ‎) 1D58F MATHEMATICAL BOLD FRAKTUR SMALL J +← (‎ 𝗃 ‎) 1D5C3 MATHEMATICAL SANS-SERIF SMALL J +← (‎ 𝗷 ‎) 1D5F7 MATHEMATICAL SANS-SERIF BOLD SMALL J +← (‎ 𝘫 ‎) 1D62B MATHEMATICAL SANS-SERIF ITALIC SMALL J +← (‎ 𝙟 ‎) 1D65F MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J +← (‎ 𝚓 ‎) 1D693 MATHEMATICAL MONOSPACE SMALL J + +# j̵ ɉ + (‎ j̵ ‎) 006A 0335 LATIN SMALL LETTER J, COMBINING SHORT STROKE OVERLAY +← (‎ ɉ ‎) 0249 LATIN SMALL LETTER J WITH STROKE + +# k 𝐤 𝑘 𝒌 𝓀 𝓴 𝔨 𝕜 𝖐 𝗄 𝗸 𝘬 𝙠 𝚔 + (‎ k ‎) 006B LATIN SMALL LETTER K +← (‎ 𝐤 ‎) 1D424 MATHEMATICAL BOLD SMALL K +← (‎ 𝑘 ‎) 1D458 MATHEMATICAL ITALIC SMALL K +← (‎ 𝒌 ‎) 1D48C MATHEMATICAL BOLD ITALIC SMALL K +← (‎ 𝓀 ‎) 1D4C0 MATHEMATICAL SCRIPT SMALL K +← (‎ 𝓴 ‎) 1D4F4 MATHEMATICAL BOLD SCRIPT SMALL K +← (‎ 𝔨 ‎) 1D528 MATHEMATICAL FRAKTUR SMALL K +← (‎ 𝕜 ‎) 1D55C MATHEMATICAL DOUBLE-STRUCK SMALL K +← (‎ 𝖐 ‎) 1D590 MATHEMATICAL BOLD FRAKTUR SMALL K +← (‎ 𝗄 ‎) 1D5C4 MATHEMATICAL SANS-SERIF SMALL K +← (‎ 𝗸 ‎) 1D5F8 MATHEMATICAL SANS-SERIF BOLD SMALL K +← (‎ 𝘬 ‎) 1D62C MATHEMATICAL SANS-SERIF ITALIC SMALL K +← (‎ 𝙠 ‎) 1D660 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL K +← (‎ 𝚔 ‎) 1D694 MATHEMATICAL MONOSPACE SMALL K + +# k̔ ƙ + (‎ k̔ ‎) 006B 0314 LATIN SMALL LETTER K, COMBINING REVERSED COMMA ABOVE +← (‎ ƙ ‎) 0199 LATIN SMALL LETTER K WITH HOOK + +# lj lj + (‎ lj ‎) 006C 006A LATIN SMALL LETTER L, LATIN SMALL LETTER J +← (‎ lj ‎) 01C9 LATIN SMALL LETTER LJ + +# ls ʪ + (‎ ls ‎) 006C 0073 LATIN SMALL LETTER L, LATIN SMALL LETTER S +← (‎ ʪ ‎) 02AA LATIN SMALL LETTER LS DIGRAPH + +# lt ₶ + (‎ lt ‎) 006C 0074 LATIN SMALL LETTER L, LATIN SMALL LETTER T +← (‎ ₶ ‎) 20B6 LIVRE TOURNOIS SIGN + +# lz ʫ + (‎ lz ‎) 006C 007A LATIN SMALL LETTER L, LATIN SMALL LETTER Z +← (‎ ʫ ‎) 02AB LATIN SMALL LETTER LZ DIGRAPH + +# lȝ lʒ ɮ + (‎ lȝ ‎) 006C 021D LATIN SMALL LETTER L, LATIN SMALL LETTER YOGH +← (‎ lʒ ‎) 006C 0292 LATIN SMALL LETTER L, LATIN SMALL LETTER EZH +← (‎ ɮ ‎) 026E LATIN SMALL LETTER LEZH # →lʒ→ + +# l̨ l̢ ɭ + (‎ l̢ ‎) 006C 0322 LATIN SMALL LETTER L, COMBINING RETROFLEX HOOK BELOW +← (‎ l̨ ‎) 006C 0328 LATIN SMALL LETTER L, COMBINING OGONEK +← (‎ ɭ ‎) 026D LATIN SMALL LETTER L WITH RETROFLEX HOOK + +# l̴ ɫ + (‎ l̴ ‎) 006C 0334 LATIN SMALL LETTER L, COMBINING TILDE OVERLAY +← (‎ ɫ ‎) 026B LATIN SMALL LETTER L WITH MIDDLE TILDE + +# l̸ l̷ ł + (‎ l̷ ‎) 006C 0337 LATIN SMALL LETTER L, COMBINING SHORT SOLIDUS OVERLAY +← (‎ l̸ ‎) 006C 0338 LATIN SMALL LETTER L, COMBINING LONG SOLIDUS OVERLAY +← (‎ ł ‎) 0142 LATIN SMALL LETTER L WITH STROKE + +# rn m 𑣣 𑜀 ⅿ 𝐦 𝑚 𝒎 𝓂 𝓶 𝔪 𝕞 𝖒 𝗆 𝗺 𝘮 𝙢 𝚖 + (‎ m ‎) 006D LATIN SMALL LETTER M +← (‎ rn ‎) 0072 006E LATIN SMALL LETTER R, LATIN SMALL LETTER N +← (‎ 𑣣 ‎) 118E3 WARANG CITI DIGIT THREE +← (‎ 𑜀 ‎) 11700 AHOM LETTER KA +← (‎ ⅿ ‎) 217F SMALL ROMAN NUMERAL ONE THOUSAND +← (‎ 𝐦 ‎) 1D426 MATHEMATICAL BOLD SMALL M +← (‎ 𝑚 ‎) 1D45A MATHEMATICAL ITALIC SMALL M +← (‎ 𝒎 ‎) 1D48E MATHEMATICAL BOLD ITALIC SMALL M +← (‎ 𝓂 ‎) 1D4C2 MATHEMATICAL SCRIPT SMALL M +← (‎ 𝓶 ‎) 1D4F6 MATHEMATICAL BOLD SCRIPT SMALL M +← (‎ 𝔪 ‎) 1D52A MATHEMATICAL FRAKTUR SMALL M +← (‎ 𝕞 ‎) 1D55E MATHEMATICAL DOUBLE-STRUCK SMALL M +← (‎ 𝖒 ‎) 1D592 MATHEMATICAL BOLD FRAKTUR SMALL M +← (‎ 𝗆 ‎) 1D5C6 MATHEMATICAL SANS-SERIF SMALL M +← (‎ 𝗺 ‎) 1D5FA MATHEMATICAL SANS-SERIF BOLD SMALL M +← (‎ 𝘮 ‎) 1D62E MATHEMATICAL SANS-SERIF ITALIC SMALL M +← (‎ 𝙢 ‎) 1D662 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL M +← (‎ 𝚖 ‎) 1D696 MATHEMATICAL MONOSPACE SMALL M + +# rn̦ m̡ ɱ + (‎ m̡ ‎) 006D 0321 LATIN SMALL LETTER M, COMBINING PALATALIZED HOOK BELOW +← (‎ rn̦ ‎) 0072 006E 0326 LATIN SMALL LETTER R, LATIN SMALL LETTER N, COMBINING COMMA BELOW +← (‎ ɱ ‎) 0271 LATIN SMALL LETTER M WITH HOOK + +# rn̴ m̴ ᵯ + (‎ m̴ ‎) 006D 0334 LATIN SMALL LETTER M, COMBINING TILDE OVERLAY +← (‎ rn̴ ‎) 0072 006E 0334 LATIN SMALL LETTER R, LATIN SMALL LETTER N, COMBINING TILDE OVERLAY +← (‎ ᵯ ‎) 1D6F LATIN SMALL LETTER M WITH MIDDLE TILDE + +# rn̸ m̷ ₥ + (‎ m̷ ‎) 006D 0337 LATIN SMALL LETTER M, COMBINING SHORT SOLIDUS OVERLAY +← (‎ rn̸ ‎) 0072 006E 0338 LATIN SMALL LETTER R, LATIN SMALL LETTER N, COMBINING LONG SOLIDUS OVERLAY +← (‎ ₥ ‎) 20A5 MILL SIGN + +# n ո ռ 𝐧 𝑛 𝒏 𝓃 𝓷 𝔫 𝕟 𝖓 𝗇 𝗻 𝘯 𝙣 𝚗 + (‎ n ‎) 006E LATIN SMALL LETTER N +← (‎ ո ‎) 0578 ARMENIAN SMALL LETTER VO +← (‎ ռ ‎) 057C ARMENIAN SMALL LETTER RA +← (‎ 𝐧 ‎) 1D427 MATHEMATICAL BOLD SMALL N +← (‎ 𝑛 ‎) 1D45B MATHEMATICAL ITALIC SMALL N +← (‎ 𝒏 ‎) 1D48F MATHEMATICAL BOLD ITALIC SMALL N +← (‎ 𝓃 ‎) 1D4C3 MATHEMATICAL SCRIPT SMALL N +← (‎ 𝓷 ‎) 1D4F7 MATHEMATICAL BOLD SCRIPT SMALL N +← (‎ 𝔫 ‎) 1D52B MATHEMATICAL FRAKTUR SMALL N +← (‎ 𝕟 ‎) 1D55F MATHEMATICAL DOUBLE-STRUCK SMALL N +← (‎ 𝖓 ‎) 1D593 MATHEMATICAL BOLD FRAKTUR SMALL N +← (‎ 𝗇 ‎) 1D5C7 MATHEMATICAL SANS-SERIF SMALL N +← (‎ 𝗻 ‎) 1D5FB MATHEMATICAL SANS-SERIF BOLD SMALL N +← (‎ 𝘯 ‎) 1D62F MATHEMATICAL SANS-SERIF ITALIC SMALL N +← (‎ 𝙣 ‎) 1D663 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL N +← (‎ 𝚗 ‎) 1D697 MATHEMATICAL MONOSPACE SMALL N + +# nj nj + (‎ nj ‎) 006E 006A LATIN SMALL LETTER N, LATIN SMALL LETTER J +← (‎ nj ‎) 01CC LATIN SMALL LETTER NJ + +# n̨ n̢ ɳ + (‎ n̢ ‎) 006E 0322 LATIN SMALL LETTER N, COMBINING RETROFLEX HOOK BELOW +← (‎ n̨ ‎) 006E 0328 LATIN SMALL LETTER N, COMBINING OGONEK +← (‎ ɳ ‎) 0273 LATIN SMALL LETTER N WITH RETROFLEX HOOK + +# n̩ ƞ η 𝛈 𝜂 𝜼 𝝶 𝞰 + (‎ n̩ ‎) 006E 0329 LATIN SMALL LETTER N, COMBINING VERTICAL LINE BELOW +← (‎ ƞ ‎) 019E LATIN SMALL LETTER N WITH LONG RIGHT LEG +← (‎ η ‎) 03B7 GREEK SMALL LETTER ETA # →ƞ→ +← (‎ 𝛈 ‎) 1D6C8 MATHEMATICAL BOLD SMALL ETA # →η→→ƞ→ +← (‎ 𝜂 ‎) 1D702 MATHEMATICAL ITALIC SMALL ETA # →η→→ƞ→ +← (‎ 𝜼 ‎) 1D73C MATHEMATICAL BOLD ITALIC SMALL ETA # →η→→ƞ→ +← (‎ 𝝶 ‎) 1D776 MATHEMATICAL SANS-SERIF BOLD SMALL ETA # →η→→ƞ→ +← (‎ 𝞰 ‎) 1D7B0 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ETA # →η→→ƞ→ + +# n̴ ᵰ + (‎ n̴ ‎) 006E 0334 LATIN SMALL LETTER N, COMBINING TILDE OVERLAY +← (‎ ᵰ ‎) 1D70 LATIN SMALL LETTER N WITH MIDDLE TILDE + +# o ᴏ ᴑ ο σ о օ ס ه ٥ ھ ہ ە ۵ ० ੦ ૦ ௦ ం ౦ ಂ ೦ ം ഠ ൦ ං ๐ ໐ ဝ ၀ ჿ ⲟ 𐐬 ꬽ 𑣈 𑣗 𐓪 o ℴ 𞸤 𞹤 𞺄 ﮦ ﮧ ﮨ ﮩ ﮪ ﮫ ﮬ ﮭ ﻩ ﻪ ﻫ ﻬ 𝐨 𝑜 𝒐 𝓸 𝔬 𝕠 𝖔 𝗈 𝗼 𝘰 𝙤 𝚘 𝛐 𝛔 𝜊 𝜎 𝝄 𝝈 𝝾 𝞂 𝞸 𝞼 + (‎ o ‎) 006F LATIN SMALL LETTER O +← (‎ ᴏ ‎) 1D0F LATIN LETTER SMALL CAPITAL O +← (‎ ᴑ ‎) 1D11 LATIN SMALL LETTER SIDEWAYS O +← (‎ ο ‎) 03BF GREEK SMALL LETTER OMICRON +← (‎ σ ‎) 03C3 GREEK SMALL LETTER SIGMA +← (‎ о ‎) 043E CYRILLIC SMALL LETTER O +← (‎ օ ‎) 0585 ARMENIAN SMALL LETTER OH +← (‎ ס ‎) 05E1 HEBREW LETTER SAMEKH +← (‎ ه ‎) 0647 ARABIC LETTER HEH +← (‎ ٥ ‎) 0665 ARABIC-INDIC DIGIT FIVE +← (‎ ھ ‎) 06BE ARABIC LETTER HEH DOACHASHMEE # →‎ه‎→ +← (‎ ہ ‎) 06C1 ARABIC LETTER HEH GOAL # →‎ه‎→ +← (‎ ە ‎) 06D5 ARABIC LETTER AE # →‎ه‎→ +← (‎ ۵ ‎) 06F5 EXTENDED ARABIC-INDIC DIGIT FIVE # →‎٥‎→ +← (‎ ० ‎) 0966 DEVANAGARI DIGIT ZERO +← (‎ ੦ ‎) 0A66 GURMUKHI DIGIT ZERO +← (‎ ૦ ‎) 0AE6 GUJARATI DIGIT ZERO +← (‎ ௦ ‎) 0BE6 TAMIL DIGIT ZERO +← (‎ ం ‎) 0C02 TELUGU SIGN ANUSVARA +← (‎ ౦ ‎) 0C66 TELUGU DIGIT ZERO +← (‎ ಂ ‎) 0C82 KANNADA SIGN ANUSVARA +← (‎ ೦ ‎) 0CE6 KANNADA DIGIT ZERO # →౦→ +← (‎ ം ‎) 0D02 MALAYALAM SIGN ANUSVARA +← (‎ ഠ ‎) 0D20 MALAYALAM LETTER TTHA +← (‎ ൦ ‎) 0D66 MALAYALAM DIGIT ZERO +← (‎ ං ‎) 0D82 SINHALA SIGN ANUSVARAYA +← (‎ ๐ ‎) 0E50 THAI DIGIT ZERO +← (‎ ໐ ‎) 0ED0 LAO DIGIT ZERO +← (‎ ဝ ‎) 101D MYANMAR LETTER WA +← (‎ ၀ ‎) 1040 MYANMAR DIGIT ZERO +← (‎ ჿ ‎) 10FF GEORGIAN LETTER LABIAL SIGN +← (‎ ⲟ ‎) 2C9F COPTIC SMALL LETTER O +← (‎ 𐐬 ‎) 1042C DESERET SMALL LETTER LONG O +← (‎ ꬽ ‎) AB3D LATIN SMALL LETTER BLACKLETTER O +← (‎ 𑣈 ‎) 118C8 WARANG CITI SMALL LETTER E +← (‎ 𑣗 ‎) 118D7 WARANG CITI SMALL LETTER BU +← (‎ 𐓪 ‎) 104EA OSAGE SMALL LETTER O +← (‎ o ‎) FF4F FULLWIDTH LATIN SMALL LETTER O # →о→ +← (‎ ℴ ‎) 2134 SCRIPT SMALL O +← (‎ 𞸤 ‎) 1EE24 ARABIC MATHEMATICAL INITIAL HEH # →‎ه‎→ +← (‎ 𞹤 ‎) 1EE64 ARABIC MATHEMATICAL STRETCHED HEH # →‎ه‎→ +← (‎ 𞺄 ‎) 1EE84 ARABIC MATHEMATICAL LOOPED HEH # →‎ه‎→ +← (‎ ﮦ ‎) FBA6 ARABIC LETTER HEH GOAL ISOLATED FORM # →‎ه‎→ +← (‎ ﮧ ‎) FBA7 ARABIC LETTER HEH GOAL FINAL FORM # →‎ہ‎→→‎ه‎→ +← (‎ ﮨ ‎) FBA8 ARABIC LETTER HEH GOAL INITIAL FORM # →‎ہ‎→→‎ه‎→ +← (‎ ﮩ ‎) FBA9 ARABIC LETTER HEH GOAL MEDIAL FORM # →‎ہ‎→→‎ه‎→ +← (‎ ﮪ ‎) FBAA ARABIC LETTER HEH DOACHASHMEE ISOLATED FORM # →‎ه‎→ +← (‎ ﮫ ‎) FBAB ARABIC LETTER HEH DOACHASHMEE FINAL FORM # →‎ﻪ‎→→‎ه‎→ +← (‎ ﮬ ‎) FBAC ARABIC LETTER HEH DOACHASHMEE INITIAL FORM # →‎ﻫ‎→→‎ه‎→ +← (‎ ﮭ ‎) FBAD ARABIC LETTER HEH DOACHASHMEE MEDIAL FORM # →‎ﻬ‎→→‎ه‎→ +← (‎ ﻩ ‎) FEE9 ARABIC LETTER HEH ISOLATED FORM # →‎ه‎→ +← (‎ ﻪ ‎) FEEA ARABIC LETTER HEH FINAL FORM # →‎ه‎→ +← (‎ ﻫ ‎) FEEB ARABIC LETTER HEH INITIAL FORM # →‎ه‎→ +← (‎ ﻬ ‎) FEEC ARABIC LETTER HEH MEDIAL FORM # →‎ه‎→ +← (‎ 𝐨 ‎) 1D428 MATHEMATICAL BOLD SMALL O +← (‎ 𝑜 ‎) 1D45C MATHEMATICAL ITALIC SMALL O +← (‎ 𝒐 ‎) 1D490 MATHEMATICAL BOLD ITALIC SMALL O +← (‎ 𝓸 ‎) 1D4F8 MATHEMATICAL BOLD SCRIPT SMALL O +← (‎ 𝔬 ‎) 1D52C MATHEMATICAL FRAKTUR SMALL O +← (‎ 𝕠 ‎) 1D560 MATHEMATICAL DOUBLE-STRUCK SMALL O +← (‎ 𝖔 ‎) 1D594 MATHEMATICAL BOLD FRAKTUR SMALL O +← (‎ 𝗈 ‎) 1D5C8 MATHEMATICAL SANS-SERIF SMALL O +← (‎ 𝗼 ‎) 1D5FC MATHEMATICAL SANS-SERIF BOLD SMALL O +← (‎ 𝘰 ‎) 1D630 MATHEMATICAL SANS-SERIF ITALIC SMALL O +← (‎ 𝙤 ‎) 1D664 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL O +← (‎ 𝚘 ‎) 1D698 MATHEMATICAL MONOSPACE SMALL O +← (‎ 𝛐 ‎) 1D6D0 MATHEMATICAL BOLD SMALL OMICRON # →𝐨→ +← (‎ 𝛔 ‎) 1D6D4 MATHEMATICAL BOLD SMALL SIGMA # →σ→ +← (‎ 𝜊 ‎) 1D70A MATHEMATICAL ITALIC SMALL OMICRON # →𝑜→ +← (‎ 𝜎 ‎) 1D70E MATHEMATICAL ITALIC SMALL SIGMA # →σ→ +← (‎ 𝝄 ‎) 1D744 MATHEMATICAL BOLD ITALIC SMALL OMICRON # →𝒐→ +← (‎ 𝝈 ‎) 1D748 MATHEMATICAL BOLD ITALIC SMALL SIGMA # →σ→ +← (‎ 𝝾 ‎) 1D77E MATHEMATICAL SANS-SERIF BOLD SMALL OMICRON # →ο→ +← (‎ 𝞂 ‎) 1D782 MATHEMATICAL SANS-SERIF BOLD SMALL SIGMA # →σ→ +← (‎ 𝞸 ‎) 1D7B8 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMICRON # →ο→ +← (‎ 𝞼 ‎) 1D7BC MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL SIGMA # →σ→ + +# o' oʼ ơ + (‎ o' ‎) 006F 0027 LATIN SMALL LETTER O, APOSTROPHE +← (‎ oʼ ‎) 006F 02BC LATIN SMALL LETTER O, MODIFIER LETTER APOSTROPHE +← (‎ ơ ‎) 01A1 LATIN SMALL LETTER O WITH HORN # →oʼ→ + +# oe œ + (‎ oe ‎) 006F 0065 LATIN SMALL LETTER O, LATIN SMALL LETTER E +← (‎ œ ‎) 0153 LATIN SMALL LIGATURE OE + +# oo ꝏ ∞ ꚙ + (‎ oo ‎) 006F 006F LATIN SMALL LETTER O, LATIN SMALL LETTER O +← (‎ ꝏ ‎) A74F LATIN SMALL LETTER OO +← (‎ ∞ ‎) 221E INFINITY # →ꝏ→ +← (‎ ꚙ ‎) A699 CYRILLIC SMALL LETTER DOUBLE O + +# ô ه̂ ھٛ ۿ + (‎ ô ‎) 006F 0302 LATIN SMALL LETTER O, COMBINING CIRCUMFLEX ACCENT +← (‎ ه̂ ‎) 0647 0302 ARABIC LETTER HEH, COMBINING CIRCUMFLEX ACCENT # →‎ھٛ‎→ +← (‎ ھٛ ‎) 06BE 065B ARABIC LETTER HEH DOACHASHMEE, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ۿ ‎) 06FF ARABIC LETTER HEH WITH INVERTED V # →‎ھٛ‎→ + +# ơ ꭴ + (‎ ơ ‎) 006F 031B LATIN SMALL LETTER O, COMBINING HORN +← (‎ ꭴ ‎) AB74 CHEROKEE SMALL LETTER U + +# o̵ o̶ о̵ ɵ ꝋ ө ѳ ꮎ ꮻ + (‎ o̵ ‎) 006F 0335 LATIN SMALL LETTER O, COMBINING SHORT STROKE OVERLAY +← (‎ o̶ ‎) 006F 0336 LATIN SMALL LETTER O, COMBINING LONG STROKE OVERLAY +← (‎ о̵ ‎) 043E 0335 CYRILLIC SMALL LETTER O, COMBINING SHORT STROKE OVERLAY # →ѳ→ +← (‎ ɵ ‎) 0275 LATIN SMALL LETTER BARRED O +← (‎ ꝋ ‎) A74B LATIN SMALL LETTER O WITH LONG STROKE OVERLAY # →o̶→ +← (‎ ө ‎) 04E9 CYRILLIC SMALL LETTER BARRED O # →ѳ→ +← (‎ ѳ ‎) 0473 CYRILLIC SMALL LETTER FITA +← (‎ ꮎ ‎) AB8E CHEROKEE SMALL LETTER NA # →ɵ→ +← (‎ ꮻ ‎) ABBB CHEROKEE SMALL LETTER WI # →ѳ→ + +# o̸ o̷ ø ꬾ + (‎ o̷ ‎) 006F 0337 LATIN SMALL LETTER O, COMBINING SHORT SOLIDUS OVERLAY +← (‎ o̸ ‎) 006F 0338 LATIN SMALL LETTER O, COMBINING LONG SOLIDUS OVERLAY +← (‎ ø ‎) 00F8 LATIN SMALL LETTER O WITH STROKE +← (‎ ꬾ ‎) AB3E LATIN SMALL LETTER BLACKLETTER O WITH STROKE # →ø→ + +# oج هج ﱑ ﳗ + (‎ oج ‎) 006F 062C LATIN SMALL LETTER O, ARABIC LETTER JEEM +← (‎ هج ‎) 0647 062C ARABIC LETTER HEH, ARABIC LETTER JEEM +← (‎ ﱑ ‎) FC51 ARABIC LIGATURE HEH WITH JEEM ISOLATED FORM # →‎هج‎→ +← (‎ ﳗ ‎) FCD7 ARABIC LIGATURE HEH WITH JEEM INITIAL FORM # →‎هج‎→ + +# oم هم ﱒ ﳘ + (‎ oم ‎) 006F 0645 LATIN SMALL LETTER O, ARABIC LETTER MEEM +← (‎ هم ‎) 0647 0645 ARABIC LETTER HEH, ARABIC LETTER MEEM +← (‎ ﱒ ‎) FC52 ARABIC LIGATURE HEH WITH MEEM ISOLATED FORM # →‎هم‎→ +← (‎ ﳘ ‎) FCD8 ARABIC LIGATURE HEH WITH MEEM INITIAL FORM # →‎هم‎→ + +# oمج همج ﶓ + (‎ oمج ‎) 006F 0645 062C LATIN SMALL LETTER O, ARABIC LETTER MEEM, ARABIC LETTER JEEM +← (‎ همج ‎) 0647 0645 062C ARABIC LETTER HEH, ARABIC LETTER MEEM, ARABIC LETTER JEEM +← (‎ ﶓ ‎) FD93 ARABIC LIGATURE HEH WITH MEEM WITH JEEM INITIAL FORM # →‎همج‎→ + +# oمم همم ﶔ + (‎ oمم ‎) 006F 0645 0645 LATIN SMALL LETTER O, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ همم ‎) 0647 0645 0645 ARABIC LETTER HEH, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﶔ ‎) FD94 ARABIC LIGATURE HEH WITH MEEM WITH MEEM INITIAL FORM # →‎همم‎→ + +# oى هى هي ﱓ ﱔ + (‎ oى ‎) 006F 0649 LATIN SMALL LETTER O, ARABIC LETTER ALEF MAKSURA +← (‎ هى ‎) 0647 0649 ARABIC LETTER HEH, ARABIC LETTER ALEF MAKSURA +← (‎ هي ‎) 0647 064A ARABIC LETTER HEH, ARABIC LETTER YEH +← (‎ ﱓ ‎) FC53 ARABIC LIGATURE HEH WITH ALEF MAKSURA ISOLATED FORM # →‎هى‎→ +← (‎ ﱔ ‎) FC54 ARABIC LIGATURE HEH WITH YEH ISOLATED FORM # →‎هي‎→ + +# oٰ هٰ ﳙ + (‎ oٰ ‎) 006F 0670 LATIN SMALL LETTER O, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ هٰ ‎) 0647 0670 ARABIC LETTER HEH, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ﳙ ‎) FCD9 ARABIC LIGATURE HEH WITH SUPERSCRIPT ALEF INITIAL FORM # →‎هٰ‎→ + +# oരo ംരം ൦ര൦ ൟ + (‎ oരo ‎) 006F 0D30 006F LATIN SMALL LETTER O, MALAYALAM LETTER RA, LATIN SMALL LETTER O +← (‎ ംരം ‎) 0D02 0D30 0D02 MALAYALAM SIGN ANUSVARA, MALAYALAM LETTER RA, MALAYALAM SIGN ANUSVARA +← (‎ ൦ര൦ ‎) 0D66 0D30 0D66 MALAYALAM DIGIT ZERO, MALAYALAM LETTER RA, MALAYALAM DIGIT ZERO # →ൟ→→ംരം→ +← (‎ ൟ ‎) 0D5F MALAYALAM LETTER ARCHAIC II # →ംരം→ + +# oာ ဝာ တ + (‎ oာ ‎) 006F 102C LATIN SMALL LETTER O, MYANMAR VOWEL SIGN AA +← (‎ ဝာ ‎) 101D 102C MYANMAR LETTER WA, MYANMAR VOWEL SIGN AA +← (‎ တ ‎) 1010 MYANMAR LETTER TA # →ဝာ→ + +# oᴇ ɶ + (‎ oᴇ ‎) 006F 1D07 LATIN SMALL LETTER O, LATIN LETTER SMALL CAPITAL E +← (‎ ɶ ‎) 0276 LATIN LETTER SMALL CAPITAL OE + +# p ρ р ⲣ ⍴ p ϱ 𝐩 𝑝 𝒑 𝓅 𝓹 𝔭 𝕡 𝖕 𝗉 𝗽 𝘱 𝙥 𝚙 𝛒 𝛠 𝜌 𝜚 𝝆 𝝔 𝞀 𝞎 𝞺 𝟈 + (‎ p ‎) 0070 LATIN SMALL LETTER P +← (‎ ρ ‎) 03C1 GREEK SMALL LETTER RHO +← (‎ р ‎) 0440 CYRILLIC SMALL LETTER ER +← (‎ ⲣ ‎) 2CA3 COPTIC SMALL LETTER RO # →ρ→ +← (‎ ⍴ ‎) 2374 APL FUNCTIONAL SYMBOL RHO # →ρ→ +← (‎ p ‎) FF50 FULLWIDTH LATIN SMALL LETTER P # →р→ +← (‎ ϱ ‎) 03F1 GREEK RHO SYMBOL # →ρ→ +← (‎ 𝐩 ‎) 1D429 MATHEMATICAL BOLD SMALL P +← (‎ 𝑝 ‎) 1D45D MATHEMATICAL ITALIC SMALL P +← (‎ 𝒑 ‎) 1D491 MATHEMATICAL BOLD ITALIC SMALL P +← (‎ 𝓅 ‎) 1D4C5 MATHEMATICAL SCRIPT SMALL P +← (‎ 𝓹 ‎) 1D4F9 MATHEMATICAL BOLD SCRIPT SMALL P +← (‎ 𝔭 ‎) 1D52D MATHEMATICAL FRAKTUR SMALL P +← (‎ 𝕡 ‎) 1D561 MATHEMATICAL DOUBLE-STRUCK SMALL P +← (‎ 𝖕 ‎) 1D595 MATHEMATICAL BOLD FRAKTUR SMALL P +← (‎ 𝗉 ‎) 1D5C9 MATHEMATICAL SANS-SERIF SMALL P +← (‎ 𝗽 ‎) 1D5FD MATHEMATICAL SANS-SERIF BOLD SMALL P +← (‎ 𝘱 ‎) 1D631 MATHEMATICAL SANS-SERIF ITALIC SMALL P +← (‎ 𝙥 ‎) 1D665 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL P +← (‎ 𝚙 ‎) 1D699 MATHEMATICAL MONOSPACE SMALL P +← (‎ 𝛒 ‎) 1D6D2 MATHEMATICAL BOLD SMALL RHO # →ρ→ +← (‎ 𝛠 ‎) 1D6E0 MATHEMATICAL BOLD RHO SYMBOL # →ρ→ +← (‎ 𝜌 ‎) 1D70C MATHEMATICAL ITALIC SMALL RHO # →ρ→ +← (‎ 𝜚 ‎) 1D71A MATHEMATICAL ITALIC RHO SYMBOL # →ρ→ +← (‎ 𝝆 ‎) 1D746 MATHEMATICAL BOLD ITALIC SMALL RHO # →ρ→ +← (‎ 𝝔 ‎) 1D754 MATHEMATICAL BOLD ITALIC RHO SYMBOL # →ρ→ +← (‎ 𝞀 ‎) 1D780 MATHEMATICAL SANS-SERIF BOLD SMALL RHO # →ρ→ +← (‎ 𝞎 ‎) 1D78E MATHEMATICAL SANS-SERIF BOLD RHO SYMBOL # →ρ→ +← (‎ 𝞺 ‎) 1D7BA MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL RHO # →ρ→ +← (‎ 𝟈 ‎) 1D7C8 MATHEMATICAL SANS-SERIF BOLD ITALIC RHO SYMBOL # →ρ→ + +# p̔ ƥ + (‎ p̔ ‎) 0070 0314 LATIN SMALL LETTER P, COMBINING REVERSED COMMA ABOVE +← (‎ ƥ ‎) 01A5 LATIN SMALL LETTER P WITH HOOK + +# p̵ ᵽ + (‎ p̵ ‎) 0070 0335 LATIN SMALL LETTER P, COMBINING SHORT STROKE OVERLAY +← (‎ ᵽ ‎) 1D7D LATIN SMALL LETTER P WITH STROKE + +# q ԛ գ զ 𝐪 𝑞 𝒒 𝓆 𝓺 𝔮 𝕢 𝖖 𝗊 𝗾 𝘲 𝙦 𝚚 + (‎ q ‎) 0071 LATIN SMALL LETTER Q +← (‎ ԛ ‎) 051B CYRILLIC SMALL LETTER QA +← (‎ գ ‎) 0563 ARMENIAN SMALL LETTER GIM +← (‎ զ ‎) 0566 ARMENIAN SMALL LETTER ZA +← (‎ 𝐪 ‎) 1D42A MATHEMATICAL BOLD SMALL Q +← (‎ 𝑞 ‎) 1D45E MATHEMATICAL ITALIC SMALL Q +← (‎ 𝒒 ‎) 1D492 MATHEMATICAL BOLD ITALIC SMALL Q +← (‎ 𝓆 ‎) 1D4C6 MATHEMATICAL SCRIPT SMALL Q +← (‎ 𝓺 ‎) 1D4FA MATHEMATICAL BOLD SCRIPT SMALL Q +← (‎ 𝔮 ‎) 1D52E MATHEMATICAL FRAKTUR SMALL Q +← (‎ 𝕢 ‎) 1D562 MATHEMATICAL DOUBLE-STRUCK SMALL Q +← (‎ 𝖖 ‎) 1D596 MATHEMATICAL BOLD FRAKTUR SMALL Q +← (‎ 𝗊 ‎) 1D5CA MATHEMATICAL SANS-SERIF SMALL Q +← (‎ 𝗾 ‎) 1D5FE MATHEMATICAL SANS-SERIF BOLD SMALL Q +← (‎ 𝘲 ‎) 1D632 MATHEMATICAL SANS-SERIF ITALIC SMALL Q +← (‎ 𝙦 ‎) 1D666 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Q +← (‎ 𝚚 ‎) 1D69A MATHEMATICAL MONOSPACE SMALL Q + +# q̔ ʠ + (‎ q̔ ‎) 0071 0314 LATIN SMALL LETTER Q, COMBINING REVERSED COMMA ABOVE +← (‎ ʠ ‎) 02A0 LATIN SMALL LETTER Q WITH HOOK + +# r г ᴦ ⲅ ꭇ ꭈ ꮁ 𝐫 𝑟 𝒓 𝓇 𝓻 𝔯 𝕣 𝖗 𝗋 𝗿 𝘳 𝙧 𝚛 + (‎ r ‎) 0072 LATIN SMALL LETTER R +← (‎ г ‎) 0433 CYRILLIC SMALL LETTER GHE +← (‎ ᴦ ‎) 1D26 GREEK LETTER SMALL CAPITAL GAMMA # →г→ +← (‎ ⲅ ‎) 2C85 COPTIC SMALL LETTER GAMMA # →г→ +← (‎ ꭇ ‎) AB47 LATIN SMALL LETTER R WITHOUT HANDLE +← (‎ ꭈ ‎) AB48 LATIN SMALL LETTER DOUBLE R +← (‎ ꮁ ‎) AB81 CHEROKEE SMALL LETTER HU # →ᴦ→→г→ +← (‎ 𝐫 ‎) 1D42B MATHEMATICAL BOLD SMALL R +← (‎ 𝑟 ‎) 1D45F MATHEMATICAL ITALIC SMALL R +← (‎ 𝒓 ‎) 1D493 MATHEMATICAL BOLD ITALIC SMALL R +← (‎ 𝓇 ‎) 1D4C7 MATHEMATICAL SCRIPT SMALL R +← (‎ 𝓻 ‎) 1D4FB MATHEMATICAL BOLD SCRIPT SMALL R +← (‎ 𝔯 ‎) 1D52F MATHEMATICAL FRAKTUR SMALL R +← (‎ 𝕣 ‎) 1D563 MATHEMATICAL DOUBLE-STRUCK SMALL R +← (‎ 𝖗 ‎) 1D597 MATHEMATICAL BOLD FRAKTUR SMALL R +← (‎ 𝗋 ‎) 1D5CB MATHEMATICAL SANS-SERIF SMALL R +← (‎ 𝗿 ‎) 1D5FF MATHEMATICAL SANS-SERIF BOLD SMALL R +← (‎ 𝘳 ‎) 1D633 MATHEMATICAL SANS-SERIF ITALIC SMALL R +← (‎ 𝙧 ‎) 1D667 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL R +← (‎ 𝚛 ‎) 1D69B MATHEMATICAL MONOSPACE SMALL R + +# r' гˈ г' ґ + (‎ r' ‎) 0072 0027 LATIN SMALL LETTER R, APOSTROPHE +← (‎ гˈ ‎) 0433 02C8 CYRILLIC SMALL LETTER GHE, MODIFIER LETTER VERTICAL LINE +← (‎ г' ‎) 0433 0027 CYRILLIC SMALL LETTER GHE, APOSTROPHE # →гˈ→ +← (‎ ґ ‎) 0491 CYRILLIC SMALL LETTER GHE WITH UPTURN # →гˈ→ + +# r̨ ɽ + (‎ r̨ ‎) 0072 0328 LATIN SMALL LETTER R, COMBINING OGONEK +← (‎ ɽ ‎) 027D LATIN SMALL LETTER R WITH TAIL + +# r̩ ɼ + (‎ r̩ ‎) 0072 0329 LATIN SMALL LETTER R, COMBINING VERTICAL LINE BELOW +← (‎ ɼ ‎) 027C LATIN SMALL LETTER R WITH LONG LEG + +# r̴ ᵲ + (‎ r̴ ‎) 0072 0334 LATIN SMALL LETTER R, COMBINING TILDE OVERLAY +← (‎ ᵲ ‎) 1D72 LATIN SMALL LETTER R WITH MIDDLE TILDE + +# r̵ г̵ ɍ ғ + (‎ r̵ ‎) 0072 0335 LATIN SMALL LETTER R, COMBINING SHORT STROKE OVERLAY +← (‎ г̵ ‎) 0433 0335 CYRILLIC SMALL LETTER GHE, COMBINING SHORT STROKE OVERLAY +← (‎ ɍ ‎) 024D LATIN SMALL LETTER R WITH STROKE +← (‎ ғ ‎) 0493 CYRILLIC SMALL LETTER GHE WITH STROKE # →г̵→ + +# s ƽ ꜱ ѕ 𐑈 𑣁 ꮪ s 𝐬 𝑠 𝒔 𝓈 𝓼 𝔰 𝕤 𝖘 𝗌 𝘀 𝘴 𝙨 𝚜 + (‎ s ‎) 0073 LATIN SMALL LETTER S +← (‎ ƽ ‎) 01BD LATIN SMALL LETTER TONE FIVE +← (‎ ꜱ ‎) A731 LATIN LETTER SMALL CAPITAL S +← (‎ ѕ ‎) 0455 CYRILLIC SMALL LETTER DZE +← (‎ 𐑈 ‎) 10448 DESERET SMALL LETTER ZHEE +← (‎ 𑣁 ‎) 118C1 WARANG CITI SMALL LETTER A +← (‎ ꮪ ‎) ABAA CHEROKEE SMALL LETTER DU # →ꜱ→ +← (‎ s ‎) FF53 FULLWIDTH LATIN SMALL LETTER S # →ѕ→ +← (‎ 𝐬 ‎) 1D42C MATHEMATICAL BOLD SMALL S +← (‎ 𝑠 ‎) 1D460 MATHEMATICAL ITALIC SMALL S +← (‎ 𝒔 ‎) 1D494 MATHEMATICAL BOLD ITALIC SMALL S +← (‎ 𝓈 ‎) 1D4C8 MATHEMATICAL SCRIPT SMALL S +← (‎ 𝓼 ‎) 1D4FC MATHEMATICAL BOLD SCRIPT SMALL S +← (‎ 𝔰 ‎) 1D530 MATHEMATICAL FRAKTUR SMALL S +← (‎ 𝕤 ‎) 1D564 MATHEMATICAL DOUBLE-STRUCK SMALL S +← (‎ 𝖘 ‎) 1D598 MATHEMATICAL BOLD FRAKTUR SMALL S +← (‎ 𝗌 ‎) 1D5CC MATHEMATICAL SANS-SERIF SMALL S +← (‎ 𝘀 ‎) 1D600 MATHEMATICAL SANS-SERIF BOLD SMALL S +← (‎ 𝘴 ‎) 1D634 MATHEMATICAL SANS-SERIF ITALIC SMALL S +← (‎ 𝙨 ‎) 1D668 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL S +← (‎ 𝚜 ‎) 1D69C MATHEMATICAL MONOSPACE SMALL S + +# sss 🝜 + (‎ sss ‎) 0073 0073 0073 LATIN SMALL LETTER S, LATIN SMALL LETTER S, LATIN SMALL LETTER S +← (‎ 🝜 ‎) 1F75C ALCHEMICAL SYMBOL FOR STRATUM SUPER STRATUM + +# st st + (‎ st ‎) 0073 0074 LATIN SMALL LETTER S, LATIN SMALL LETTER T +← (‎ st ‎) FB06 LATIN SMALL LIGATURE ST + +# s̨ ʂ + (‎ s̨ ‎) 0073 0328 LATIN SMALL LETTER S, COMBINING OGONEK +← (‎ ʂ ‎) 0282 LATIN SMALL LETTER S WITH HOOK + +# s̴ ᵴ + (‎ s̴ ‎) 0073 0334 LATIN SMALL LETTER S, COMBINING TILDE OVERLAY +← (‎ ᵴ ‎) 1D74 LATIN SMALL LETTER S WITH MIDDLE TILDE + +# t 𝐭 𝑡 𝒕 𝓉 𝓽 𝔱 𝕥 𝖙 𝗍 𝘁 𝘵 𝙩 𝚝 + (‎ t ‎) 0074 LATIN SMALL LETTER T +← (‎ 𝐭 ‎) 1D42D MATHEMATICAL BOLD SMALL T +← (‎ 𝑡 ‎) 1D461 MATHEMATICAL ITALIC SMALL T +← (‎ 𝒕 ‎) 1D495 MATHEMATICAL BOLD ITALIC SMALL T +← (‎ 𝓉 ‎) 1D4C9 MATHEMATICAL SCRIPT SMALL T +← (‎ 𝓽 ‎) 1D4FD MATHEMATICAL BOLD SCRIPT SMALL T +← (‎ 𝔱 ‎) 1D531 MATHEMATICAL FRAKTUR SMALL T +← (‎ 𝕥 ‎) 1D565 MATHEMATICAL DOUBLE-STRUCK SMALL T +← (‎ 𝖙 ‎) 1D599 MATHEMATICAL BOLD FRAKTUR SMALL T +← (‎ 𝗍 ‎) 1D5CD MATHEMATICAL SANS-SERIF SMALL T +← (‎ 𝘁 ‎) 1D601 MATHEMATICAL SANS-SERIF BOLD SMALL T +← (‎ 𝘵 ‎) 1D635 MATHEMATICAL SANS-SERIF ITALIC SMALL T +← (‎ 𝙩 ‎) 1D669 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL T +← (‎ 𝚝 ‎) 1D69D MATHEMATICAL MONOSPACE SMALL T + +# tf ꝷ + (‎ tf ‎) 0074 0066 LATIN SMALL LETTER T, LATIN SMALL LETTER F +← (‎ ꝷ ‎) A777 LATIN SMALL LETTER TUM + +# ts ʦ + (‎ ts ‎) 0074 0073 LATIN SMALL LETTER T, LATIN SMALL LETTER S +← (‎ ʦ ‎) 02A6 LATIN SMALL LETTER TS DIGRAPH + +# tȝ ꜩ + (‎ tȝ ‎) 0074 021D LATIN SMALL LETTER T, LATIN SMALL LETTER YOGH +← (‎ ꜩ ‎) A729 LATIN SMALL LETTER TZ + +# tɕ ʨ + (‎ tɕ ‎) 0074 0255 LATIN SMALL LETTER T, LATIN SMALL LETTER C WITH CURL +← (‎ ʨ ‎) 02A8 LATIN SMALL LETTER TC DIGRAPH WITH CURL + +# tʃ ʧ + (‎ tʃ ‎) 0074 0283 LATIN SMALL LETTER T, LATIN SMALL LETTER ESH +← (‎ ʧ ‎) 02A7 LATIN SMALL LETTER TESH DIGRAPH + +# t̔ ƭ + (‎ t̔ ‎) 0074 0314 LATIN SMALL LETTER T, COMBINING REVERSED COMMA ABOVE +← (‎ ƭ ‎) 01AD LATIN SMALL LETTER T WITH HOOK + +# t̴ ᵵ + (‎ t̴ ‎) 0074 0334 LATIN SMALL LETTER T, COMBINING TILDE OVERLAY +← (‎ ᵵ ‎) 1D75 LATIN SMALL LETTER T WITH MIDDLE TILDE + +# t̵ ŧ + (‎ t̵ ‎) 0074 0335 LATIN SMALL LETTER T, COMBINING SHORT STROKE OVERLAY +← (‎ ŧ ‎) 0167 LATIN SMALL LETTER T WITH STROKE + +# u ʋ ᴜ υ ս ꞟ ꭎ ꭒ 𑣘 𐓶 𝐮 𝑢 𝒖 𝓊 𝓾 𝔲 𝕦 𝖚 𝗎 𝘂 𝘶 𝙪 𝚞 𝛖 𝜐 𝝊 𝞄 𝞾 + (‎ u ‎) 0075 LATIN SMALL LETTER U +← (‎ ʋ ‎) 028B LATIN SMALL LETTER V WITH HOOK +← (‎ ᴜ ‎) 1D1C LATIN LETTER SMALL CAPITAL U +← (‎ υ ‎) 03C5 GREEK SMALL LETTER UPSILON # →ʋ→ +← (‎ ս ‎) 057D ARMENIAN SMALL LETTER SEH +← (‎ ꞟ ‎) A79F LATIN SMALL LETTER VOLAPUK UE +← (‎ ꭎ ‎) AB4E LATIN SMALL LETTER U WITH SHORT RIGHT LEG +← (‎ ꭒ ‎) AB52 LATIN SMALL LETTER U WITH LEFT HOOK +← (‎ 𑣘 ‎) 118D8 WARANG CITI SMALL LETTER PU # →υ→→ʋ→ +← (‎ 𐓶 ‎) 104F6 OSAGE SMALL LETTER U # →ᴜ→ +← (‎ 𝐮 ‎) 1D42E MATHEMATICAL BOLD SMALL U +← (‎ 𝑢 ‎) 1D462 MATHEMATICAL ITALIC SMALL U +← (‎ 𝒖 ‎) 1D496 MATHEMATICAL BOLD ITALIC SMALL U +← (‎ 𝓊 ‎) 1D4CA MATHEMATICAL SCRIPT SMALL U +← (‎ 𝓾 ‎) 1D4FE MATHEMATICAL BOLD SCRIPT SMALL U +← (‎ 𝔲 ‎) 1D532 MATHEMATICAL FRAKTUR SMALL U +← (‎ 𝕦 ‎) 1D566 MATHEMATICAL DOUBLE-STRUCK SMALL U +← (‎ 𝖚 ‎) 1D59A MATHEMATICAL BOLD FRAKTUR SMALL U +← (‎ 𝗎 ‎) 1D5CE MATHEMATICAL SANS-SERIF SMALL U +← (‎ 𝘂 ‎) 1D602 MATHEMATICAL SANS-SERIF BOLD SMALL U +← (‎ 𝘶 ‎) 1D636 MATHEMATICAL SANS-SERIF ITALIC SMALL U +← (‎ 𝙪 ‎) 1D66A MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL U +← (‎ 𝚞 ‎) 1D69E MATHEMATICAL MONOSPACE SMALL U +← (‎ 𝛖 ‎) 1D6D6 MATHEMATICAL BOLD SMALL UPSILON # →υ→→ʋ→ +← (‎ 𝜐 ‎) 1D710 MATHEMATICAL ITALIC SMALL UPSILON # →υ→→ʋ→ +← (‎ 𝝊 ‎) 1D74A MATHEMATICAL BOLD ITALIC SMALL UPSILON # →υ→→ʋ→ +← (‎ 𝞄 ‎) 1D784 MATHEMATICAL SANS-SERIF BOLD SMALL UPSILON # →υ→→ʋ→ +← (‎ 𝞾 ‎) 1D7BE MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL UPSILON # →υ→→ʋ→ + +# ue ᵫ + (‎ ue ‎) 0075 0065 LATIN SMALL LETTER U, LATIN SMALL LETTER E +← (‎ ᵫ ‎) 1D6B LATIN SMALL LETTER UE + +# uo ꭣ + (‎ uo ‎) 0075 006F LATIN SMALL LETTER U, LATIN SMALL LETTER O +← (‎ ꭣ ‎) AB63 LATIN SMALL LETTER UO + +# u̵ ᴜ̵ ᵾ ꮜ + (‎ u̵ ‎) 0075 0335 LATIN SMALL LETTER U, COMBINING SHORT STROKE OVERLAY +← (‎ ᴜ̵ ‎) 1D1C 0335 LATIN LETTER SMALL CAPITAL U, COMBINING SHORT STROKE OVERLAY +← (‎ ᵾ ‎) 1D7E LATIN SMALL CAPITAL LETTER U WITH STROKE # →ᴜ̵→ +← (‎ ꮜ ‎) AB9C CHEROKEE SMALL LETTER SA # →ᴜ̵→ + +# v ᴠ ν ט ѵ ∨ ⋁ 𑣀 ꮩ 𑜆 ⅴ v 𝐯 𝑣 𝒗 𝓋 𝓿 𝔳 𝕧 𝖛 𝗏 𝘃 𝘷 𝙫 𝚟 𝛎 𝜈 𝝂 𝝼 𝞶 + (‎ v ‎) 0076 LATIN SMALL LETTER V +← (‎ ᴠ ‎) 1D20 LATIN LETTER SMALL CAPITAL V +← (‎ ν ‎) 03BD GREEK SMALL LETTER NU +← (‎ ט ‎) 05D8 HEBREW LETTER TET +← (‎ ѵ ‎) 0475 CYRILLIC SMALL LETTER IZHITSA +← (‎ ∨ ‎) 2228 LOGICAL OR +← (‎ ⋁ ‎) 22C1 N-ARY LOGICAL OR # →∨→ +← (‎ 𑣀 ‎) 118C0 WARANG CITI SMALL LETTER NGAA +← (‎ ꮩ ‎) ABA9 CHEROKEE SMALL LETTER DO # →ᴠ→ +← (‎ 𑜆 ‎) 11706 AHOM LETTER PA +← (‎ ⅴ ‎) 2174 SMALL ROMAN NUMERAL FIVE +← (‎ v ‎) FF56 FULLWIDTH LATIN SMALL LETTER V # →ν→ +← (‎ 𝐯 ‎) 1D42F MATHEMATICAL BOLD SMALL V +← (‎ 𝑣 ‎) 1D463 MATHEMATICAL ITALIC SMALL V +← (‎ 𝒗 ‎) 1D497 MATHEMATICAL BOLD ITALIC SMALL V +← (‎ 𝓋 ‎) 1D4CB MATHEMATICAL SCRIPT SMALL V +← (‎ 𝓿 ‎) 1D4FF MATHEMATICAL BOLD SCRIPT SMALL V +← (‎ 𝔳 ‎) 1D533 MATHEMATICAL FRAKTUR SMALL V +← (‎ 𝕧 ‎) 1D567 MATHEMATICAL DOUBLE-STRUCK SMALL V +← (‎ 𝖛 ‎) 1D59B MATHEMATICAL BOLD FRAKTUR SMALL V +← (‎ 𝗏 ‎) 1D5CF MATHEMATICAL SANS-SERIF SMALL V +← (‎ 𝘃 ‎) 1D603 MATHEMATICAL SANS-SERIF BOLD SMALL V +← (‎ 𝘷 ‎) 1D637 MATHEMATICAL SANS-SERIF ITALIC SMALL V +← (‎ 𝙫 ‎) 1D66B MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL V +← (‎ 𝚟 ‎) 1D69F MATHEMATICAL MONOSPACE SMALL V +← (‎ 𝛎 ‎) 1D6CE MATHEMATICAL BOLD SMALL NU # →ν→ +← (‎ 𝜈 ‎) 1D708 MATHEMATICAL ITALIC SMALL NU # →ν→ +← (‎ 𝝂 ‎) 1D742 MATHEMATICAL BOLD ITALIC SMALL NU # →ν→ +← (‎ 𝝼 ‎) 1D77C MATHEMATICAL SANS-SERIF BOLD SMALL NU # →ν→ +← (‎ 𝞶 ‎) 1D7B6 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL NU # →ν→ + +# vi ⅵ + (‎ vi ‎) 0076 0069 LATIN SMALL LETTER V, LATIN SMALL LETTER I +← (‎ ⅵ ‎) 2175 SMALL ROMAN NUMERAL SIX + +# vii ⅶ + (‎ vii ‎) 0076 0069 0069 LATIN SMALL LETTER V, LATIN SMALL LETTER I, LATIN SMALL LETTER I +← (‎ ⅶ ‎) 2176 SMALL ROMAN NUMERAL SEVEN + +# viii ⅷ + (‎ viii ‎) 0076 0069 0069 0069 LATIN SMALL LETTER V, LATIN SMALL LETTER I, LATIN SMALL LETTER I, LATIN SMALL LETTER I +← (‎ ⅷ ‎) 2177 SMALL ROMAN NUMERAL EIGHT + +# w ɯ ᴡ ԝ ա ѡ ꮃ 𑜊 𑜎 𑜏 𝐰 𝑤 𝒘 𝓌 𝔀 𝔴 𝕨 𝖜 𝗐 𝘄 𝘸 𝙬 𝚠 + (‎ w ‎) 0077 LATIN SMALL LETTER W +← (‎ ɯ ‎) 026F LATIN SMALL LETTER TURNED M +← (‎ ᴡ ‎) 1D21 LATIN LETTER SMALL CAPITAL W +← (‎ ԝ ‎) 051D CYRILLIC SMALL LETTER WE +← (‎ ա ‎) 0561 ARMENIAN SMALL LETTER AYB # →ɯ→ +← (‎ ѡ ‎) 0461 CYRILLIC SMALL LETTER OMEGA +← (‎ ꮃ ‎) AB83 CHEROKEE SMALL LETTER LA # →ᴡ→ +← (‎ 𑜊 ‎) 1170A AHOM LETTER JA +← (‎ 𑜎 ‎) 1170E AHOM LETTER LA +← (‎ 𑜏 ‎) 1170F AHOM LETTER SA +← (‎ 𝐰 ‎) 1D430 MATHEMATICAL BOLD SMALL W +← (‎ 𝑤 ‎) 1D464 MATHEMATICAL ITALIC SMALL W +← (‎ 𝒘 ‎) 1D498 MATHEMATICAL BOLD ITALIC SMALL W +← (‎ 𝓌 ‎) 1D4CC MATHEMATICAL SCRIPT SMALL W +← (‎ 𝔀 ‎) 1D500 MATHEMATICAL BOLD SCRIPT SMALL W +← (‎ 𝔴 ‎) 1D534 MATHEMATICAL FRAKTUR SMALL W +← (‎ 𝕨 ‎) 1D568 MATHEMATICAL DOUBLE-STRUCK SMALL W +← (‎ 𝖜 ‎) 1D59C MATHEMATICAL BOLD FRAKTUR SMALL W +← (‎ 𝗐 ‎) 1D5D0 MATHEMATICAL SANS-SERIF SMALL W +← (‎ 𝘄 ‎) 1D604 MATHEMATICAL SANS-SERIF BOLD SMALL W +← (‎ 𝘸 ‎) 1D638 MATHEMATICAL SANS-SERIF ITALIC SMALL W +← (‎ 𝙬 ‎) 1D66C MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL W +← (‎ 𝚠 ‎) 1D6A0 MATHEMATICAL MONOSPACE SMALL W + +# ẇ 𑓅 + (‎ ẇ ‎) 0077 0307 LATIN SMALL LETTER W, COMBINING DOT ABOVE +← (‎ 𑓅 ‎) 114C5 TIRHUTA GVANG + +# w̦ w̡ ꝡ + (‎ w̡ ‎) 0077 0321 LATIN SMALL LETTER W, COMBINING PALATALIZED HOOK BELOW +← (‎ w̦ ‎) 0077 0326 LATIN SMALL LETTER W, COMBINING COMMA BELOW +← (‎ ꝡ ‎) A761 LATIN SMALL LETTER VY + +# w҆҇ ѡ҆҇ ԝ҆҇ w҃ ѡ҃ ԝ҃ ѽ + (‎ w҃ ‎) 0077 0483 LATIN SMALL LETTER W, COMBINING CYRILLIC TITLO +← (‎ w҆҇ ‎) 0077 0486 0487 LATIN SMALL LETTER W, COMBINING CYRILLIC PSILI PNEUMATA, COMBINING CYRILLIC POKRYTIE # →ѡ҆҇→→ѽ→→ѡ҃→ +← (‎ ѡ҆҇ ‎) 0461 0486 0487 CYRILLIC SMALL LETTER OMEGA, COMBINING CYRILLIC PSILI PNEUMATA, COMBINING CYRILLIC POKRYTIE # →ѽ→→ѡ҃→ +← (‎ ԝ҆҇ ‎) 051D 0486 0487 CYRILLIC SMALL LETTER WE, COMBINING CYRILLIC PSILI PNEUMATA, COMBINING CYRILLIC POKRYTIE # →ѡ҆҇→→ѽ→→ѡ҃→ +← (‎ ѡ҃ ‎) 0461 0483 CYRILLIC SMALL LETTER OMEGA, COMBINING CYRILLIC TITLO +← (‎ ԝ҃ ‎) 051D 0483 CYRILLIC SMALL LETTER WE, COMBINING CYRILLIC TITLO # →ѡ҃→ +← (‎ ѽ ‎) 047D CYRILLIC SMALL LETTER OMEGA WITH TITLO # →ѡ҃→ + +# x х ᕁ ᕽ × ᙮ ⤫ ⤬ ⨯ ⅹ x 𝐱 𝑥 𝒙 𝓍 𝔁 𝔵 𝕩 𝖝 𝗑 𝘅 𝘹 𝙭 𝚡 + (‎ x ‎) 0078 LATIN SMALL LETTER X +← (‎ х ‎) 0445 CYRILLIC SMALL LETTER HA +← (‎ ᕁ ‎) 1541 CANADIAN SYLLABICS SAYISI YI # →᙮→ +← (‎ ᕽ ‎) 157D CANADIAN SYLLABICS HK # →ᕁ→→᙮→ +← (‎ × ‎) 00D7 MULTIPLICATION SIGN +← (‎ ᙮ ‎) 166E CANADIAN SYLLABICS FULL STOP +← (‎ ⤫ ‎) 292B RISING DIAGONAL CROSSING FALLING DIAGONAL +← (‎ ⤬ ‎) 292C FALLING DIAGONAL CROSSING RISING DIAGONAL +← (‎ ⨯ ‎) 2A2F VECTOR OR CROSS PRODUCT # →×→ +← (‎ ⅹ ‎) 2179 SMALL ROMAN NUMERAL TEN +← (‎ x ‎) FF58 FULLWIDTH LATIN SMALL LETTER X # →х→ +← (‎ 𝐱 ‎) 1D431 MATHEMATICAL BOLD SMALL X +← (‎ 𝑥 ‎) 1D465 MATHEMATICAL ITALIC SMALL X +← (‎ 𝒙 ‎) 1D499 MATHEMATICAL BOLD ITALIC SMALL X +← (‎ 𝓍 ‎) 1D4CD MATHEMATICAL SCRIPT SMALL X +← (‎ 𝔁 ‎) 1D501 MATHEMATICAL BOLD SCRIPT SMALL X +← (‎ 𝔵 ‎) 1D535 MATHEMATICAL FRAKTUR SMALL X +← (‎ 𝕩 ‎) 1D569 MATHEMATICAL DOUBLE-STRUCK SMALL X +← (‎ 𝖝 ‎) 1D59D MATHEMATICAL BOLD FRAKTUR SMALL X +← (‎ 𝗑 ‎) 1D5D1 MATHEMATICAL SANS-SERIF SMALL X +← (‎ 𝘅 ‎) 1D605 MATHEMATICAL SANS-SERIF BOLD SMALL X +← (‎ 𝘹 ‎) 1D639 MATHEMATICAL SANS-SERIF ITALIC SMALL X +← (‎ 𝙭 ‎) 1D66D MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL X +← (‎ 𝚡 ‎) 1D6A1 MATHEMATICAL MONOSPACE SMALL X + +# xi ⅺ + (‎ xi ‎) 0078 0069 LATIN SMALL LETTER X, LATIN SMALL LETTER I +← (‎ ⅺ ‎) 217A SMALL ROMAN NUMERAL ELEVEN + +# xii ⅻ + (‎ xii ‎) 0078 0069 0069 LATIN SMALL LETTER X, LATIN SMALL LETTER I, LATIN SMALL LETTER I +← (‎ ⅻ ‎) 217B SMALL ROMAN NUMERAL TWELVE + +# ẋ ×̇ ⨰ + (‎ ẋ ‎) 0078 0307 LATIN SMALL LETTER X, COMBINING DOT ABOVE +← (‎ ×̇ ‎) 00D7 0307 MULTIPLICATION SIGN, COMBINING DOT ABOVE +← (‎ ⨰ ‎) 2A30 MULTIPLICATION SIGN WITH DOT ABOVE # →×̇→ + +# y ɣ ʏ ᶌ ỿ γ у ү ყ ꭚ 𑣜 y ℽ 𝐲 𝑦 𝒚 𝓎 𝔂 𝔶 𝕪 𝖞 𝗒 𝘆 𝘺 𝙮 𝚢 𝛄 𝛾 𝜸 𝝲 𝞬 + (‎ y ‎) 0079 LATIN SMALL LETTER Y +← (‎ ɣ ‎) 0263 LATIN SMALL LETTER GAMMA # →γ→ +← (‎ ʏ ‎) 028F LATIN LETTER SMALL CAPITAL Y # →ү→→γ→ +← (‎ ᶌ ‎) 1D8C LATIN SMALL LETTER V WITH PALATAL HOOK +← (‎ ỿ ‎) 1EFF LATIN SMALL LETTER Y WITH LOOP +← (‎ γ ‎) 03B3 GREEK SMALL LETTER GAMMA +← (‎ у ‎) 0443 CYRILLIC SMALL LETTER U +← (‎ ү ‎) 04AF CYRILLIC SMALL LETTER STRAIGHT U # →γ→ +← (‎ ყ ‎) 10E7 GEORGIAN LETTER QAR +← (‎ ꭚ ‎) AB5A LATIN SMALL LETTER Y WITH SHORT RIGHT LEG +← (‎ 𑣜 ‎) 118DC WARANG CITI SMALL LETTER HAR # →ɣ→→γ→ +← (‎ y ‎) FF59 FULLWIDTH LATIN SMALL LETTER Y # →у→ +← (‎ ℽ ‎) 213D DOUBLE-STRUCK SMALL GAMMA # →γ→ +← (‎ 𝐲 ‎) 1D432 MATHEMATICAL BOLD SMALL Y +← (‎ 𝑦 ‎) 1D466 MATHEMATICAL ITALIC SMALL Y +← (‎ 𝒚 ‎) 1D49A MATHEMATICAL BOLD ITALIC SMALL Y +← (‎ 𝓎 ‎) 1D4CE MATHEMATICAL SCRIPT SMALL Y +← (‎ 𝔂 ‎) 1D502 MATHEMATICAL BOLD SCRIPT SMALL Y +← (‎ 𝔶 ‎) 1D536 MATHEMATICAL FRAKTUR SMALL Y +← (‎ 𝕪 ‎) 1D56A MATHEMATICAL DOUBLE-STRUCK SMALL Y +← (‎ 𝖞 ‎) 1D59E MATHEMATICAL BOLD FRAKTUR SMALL Y +← (‎ 𝗒 ‎) 1D5D2 MATHEMATICAL SANS-SERIF SMALL Y +← (‎ 𝘆 ‎) 1D606 MATHEMATICAL SANS-SERIF BOLD SMALL Y +← (‎ 𝘺 ‎) 1D63A MATHEMATICAL SANS-SERIF ITALIC SMALL Y +← (‎ 𝙮 ‎) 1D66E MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Y +← (‎ 𝚢 ‎) 1D6A2 MATHEMATICAL MONOSPACE SMALL Y +← (‎ 𝛄 ‎) 1D6C4 MATHEMATICAL BOLD SMALL GAMMA # →γ→ +← (‎ 𝛾 ‎) 1D6FE MATHEMATICAL ITALIC SMALL GAMMA # →γ→ +← (‎ 𝜸 ‎) 1D738 MATHEMATICAL BOLD ITALIC SMALL GAMMA # →γ→ +← (‎ 𝝲 ‎) 1D772 MATHEMATICAL SANS-SERIF BOLD SMALL GAMMA # →γ→ +← (‎ 𝞬 ‎) 1D7AC MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL GAMMA # →γ→ + +# y̔ ƴ + (‎ y̔ ‎) 0079 0314 LATIN SMALL LETTER Y, COMBINING REVERSED COMMA ABOVE +← (‎ ƴ ‎) 01B4 LATIN SMALL LETTER Y WITH HOOK + +# y̵ у̵ ү̵ ɏ ұ + (‎ y̵ ‎) 0079 0335 LATIN SMALL LETTER Y, COMBINING SHORT STROKE OVERLAY +← (‎ у̵ ‎) 0443 0335 CYRILLIC SMALL LETTER U, COMBINING SHORT STROKE OVERLAY # →ү̵→ +← (‎ ү̵ ‎) 04AF 0335 CYRILLIC SMALL LETTER STRAIGHT U, COMBINING SHORT STROKE OVERLAY +← (‎ ɏ ‎) 024F LATIN SMALL LETTER Y WITH STROKE +← (‎ ұ ‎) 04B1 CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE # →ү̵→ + +# z ᴢ 𑣄 ꮓ 𝐳 𝑧 𝒛 𝓏 𝔃 𝔷 𝕫 𝖟 𝗓 𝘇 𝘻 𝙯 𝚣 + (‎ z ‎) 007A LATIN SMALL LETTER Z +← (‎ ᴢ ‎) 1D22 LATIN LETTER SMALL CAPITAL Z +← (‎ 𑣄 ‎) 118C4 WARANG CITI SMALL LETTER YA +← (‎ ꮓ ‎) AB93 CHEROKEE SMALL LETTER NO # →ᴢ→ +← (‎ 𝐳 ‎) 1D433 MATHEMATICAL BOLD SMALL Z +← (‎ 𝑧 ‎) 1D467 MATHEMATICAL ITALIC SMALL Z +← (‎ 𝒛 ‎) 1D49B MATHEMATICAL BOLD ITALIC SMALL Z +← (‎ 𝓏 ‎) 1D4CF MATHEMATICAL SCRIPT SMALL Z +← (‎ 𝔃 ‎) 1D503 MATHEMATICAL BOLD SCRIPT SMALL Z +← (‎ 𝔷 ‎) 1D537 MATHEMATICAL FRAKTUR SMALL Z +← (‎ 𝕫 ‎) 1D56B MATHEMATICAL DOUBLE-STRUCK SMALL Z +← (‎ 𝖟 ‎) 1D59F MATHEMATICAL BOLD FRAKTUR SMALL Z +← (‎ 𝗓 ‎) 1D5D3 MATHEMATICAL SANS-SERIF SMALL Z +← (‎ 𝘇 ‎) 1D607 MATHEMATICAL SANS-SERIF BOLD SMALL Z +← (‎ 𝘻 ‎) 1D63B MATHEMATICAL SANS-SERIF ITALIC SMALL Z +← (‎ 𝙯 ‎) 1D66F MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Z +← (‎ 𝚣 ‎) 1D6A3 MATHEMATICAL MONOSPACE SMALL Z + +# z̦ z̡ ȥ + (‎ z̡ ‎) 007A 0321 LATIN SMALL LETTER Z, COMBINING PALATALIZED HOOK BELOW +← (‎ z̦ ‎) 007A 0326 LATIN SMALL LETTER Z, COMBINING COMMA BELOW +← (‎ ȥ ‎) 0225 LATIN SMALL LETTER Z WITH HOOK + +# z̨ z̢ ʐ + (‎ z̢ ‎) 007A 0322 LATIN SMALL LETTER Z, COMBINING RETROFLEX HOOK BELOW +← (‎ z̨ ‎) 007A 0328 LATIN SMALL LETTER Z, COMBINING OGONEK +← (‎ ʐ ‎) 0290 LATIN SMALL LETTER Z WITH RETROFLEX HOOK + +# z̴ ᵶ + (‎ z̴ ‎) 007A 0334 LATIN SMALL LETTER Z, COMBINING TILDE OVERLAY +← (‎ ᵶ ‎) 1D76 LATIN SMALL LETTER Z WITH MIDDLE TILDE + +# z̵ ƶ + (‎ z̵ ‎) 007A 0335 LATIN SMALL LETTER Z, COMBINING SHORT STROKE OVERLAY +← (‎ ƶ ‎) 01B6 LATIN SMALL LETTER Z WITH STROKE + +# { ❴ 𝄔 + (‎ { ‎) 007B LEFT CURLY BRACKET +← (‎ ❴ ‎) 2774 MEDIUM LEFT CURLY BRACKET ORNAMENT +← (‎ 𝄔 ‎) 1D114 MUSICAL SYMBOL BRACE + +# } ❵ + (‎ } ‎) 007D RIGHT CURLY BRACKET +← (‎ ❵ ‎) 2775 MEDIUM RIGHT CURLY BRACKET ORNAMENT + +# ~ ⁓ ∼ ˜ ῀ + (‎ ~ ‎) 007E TILDE +← (‎ ⁓ ‎) 2053 SWUNG DASH +← (‎ ∼ ‎) 223C TILDE OPERATOR +← (‎ ˜ ‎) 02DC SMALL TILDE +← (‎ ῀ ‎) 1FC0 GREEK PERISPOMENI # →˜→ + +# ~̇ ⁓̇ ∼̇ ⩪ ⸞ + (‎ ~̇ ‎) 007E 0307 TILDE, COMBINING DOT ABOVE +← (‎ ⁓̇ ‎) 2053 0307 SWUNG DASH, COMBINING DOT ABOVE +← (‎ ∼̇ ‎) 223C 0307 TILDE OPERATOR, COMBINING DOT ABOVE # →⁓̇→ +← (‎ ⩪ ‎) 2A6A TILDE OPERATOR WITH DOT ABOVE # →∼̇→→⁓̇→ +← (‎ ⸞ ‎) 2E1E TILDE WITH DOT ABOVE # →⩪→→∼̇→→⁓̇→ + +# ~̈ ⍨ + (‎ ~̈ ‎) 007E 0308 TILDE, COMBINING DIAERESIS +← (‎ ⍨ ‎) 2368 APL FUNCTIONAL SYMBOL TILDE DIAERESIS + +# ~̣ ⸟ + (‎ ~̣ ‎) 007E 0323 TILDE, COMBINING DOT BELOW +← (‎ ⸟ ‎) 2E1F TILDE WITH DOT BELOW + +# £ ₤ + (‎ £ ‎) 00A3 POUND SIGN +← (‎ ₤ ‎) 20A4 LIRA SIGN + +# © Ⓒ + (‎ © ‎) 00A9 COPYRIGHT SIGN +← (‎ Ⓒ ‎) 24B8 CIRCLED LATIN CAPITAL LETTER C + +# ® Ⓡ + (‎ ® ‎) 00AE REGISTERED SIGN +← (‎ Ⓡ ‎) 24C7 CIRCLED LATIN CAPITAL LETTER R + +# ˉ ▔ ¯ ‾ ﹉ ﹊ ﹋ ﹌  ̄ + (‎ ¯ ‎) 00AF MACRON +← (‎ ˉ ‎) 02C9 MODIFIER LETTER MACRON +← (‎ ▔ ‎) 2594 UPPER ONE EIGHTH BLOCK +← (‎ ‾ ‎) 203E OVERLINE +← (‎ ﹉ ‎) FE49 DASHED OVERLINE # →‾→ +← (‎ ﹊ ‎) FE4A CENTRELINE OVERLINE # →‾→ +← (‎ ﹋ ‎) FE4B WAVY OVERLINE # →‾→ +← (‎ ﹌ ‎) FE4C DOUBLE WAVY OVERLINE # →‾→ +← (‎  ̄ ‎) FFE3 FULLWIDTH MACRON # →‾→ + +# ˉb ¯b ъ + (‎ ¯b ‎) 00AF 0062 MACRON, LATIN SMALL LETTER B +← (‎ ˉb ‎) 02C9 0062 MODIFIER LETTER MACRON, LATIN SMALL LETTER B +← (‎ ъ ‎) 044A CYRILLIC SMALL LETTER HARD SIGN + +# ° ∘ ○ ◦ ⸰ ˚ + (‎ ° ‎) 00B0 DEGREE SIGN +← (‎ ∘ ‎) 2218 RING OPERATOR +← (‎ ○ ‎) 25CB WHITE CIRCLE # →◦→→∘→ +← (‎ ◦ ‎) 25E6 WHITE BULLET # →∘→ +← (‎ ⸰ ‎) 2E30 RING POINT # →∘→ +← (‎ ˚ ‎) 02DA RING ABOVE + +# °C ℃ + (‎ °C ‎) 00B0 0043 DEGREE SIGN, LATIN CAPITAL LETTER C +← (‎ ℃ ‎) 2103 DEGREE CELSIUS + +# °F ℉ + (‎ °F ‎) 00B0 0046 DEGREE SIGN, LATIN CAPITAL LETTER F +← (‎ ℉ ‎) 2109 DEGREE FAHRENHEIT + +# °̈ ∘̈ ◦̈ ⍤ + (‎ °̈ ‎) 00B0 0308 DEGREE SIGN, COMBINING DIAERESIS +← (‎ ∘̈ ‎) 2218 0308 RING OPERATOR, COMBINING DIAERESIS +← (‎ ◦̈ ‎) 25E6 0308 WHITE BULLET, COMBINING DIAERESIS # →∘̈→ +← (‎ ⍤ ‎) 2364 APL FUNCTIONAL SYMBOL JOT DIAERESIS # →◦̈→→∘̈→ + +# °̲ ∘̲ ○̲ ⍜ + (‎ °̲ ‎) 00B0 0332 DEGREE SIGN, COMBINING LOW LINE +← (‎ ∘̲ ‎) 2218 0332 RING OPERATOR, COMBINING LOW LINE +← (‎ ○̲ ‎) 25CB 0332 WHITE CIRCLE, COMBINING LOW LINE # →∘̲→ +← (‎ ⍜ ‎) 235C APL FUNCTIONAL SYMBOL CIRCLE UNDERBAR # →○̲→→∘̲→ + +# μ µ 𝛍 𝜇 𝝁 𝝻 𝞵 + (‎ µ ‎) 00B5 MICRO SIGN +← (‎ μ ‎) 03BC GREEK SMALL LETTER MU +← (‎ 𝛍 ‎) 1D6CD MATHEMATICAL BOLD SMALL MU # →μ→ +← (‎ 𝜇 ‎) 1D707 MATHEMATICAL ITALIC SMALL MU # →μ→ +← (‎ 𝝁 ‎) 1D741 MATHEMATICAL BOLD ITALIC SMALL MU # →μ→ +← (‎ 𝝻 ‎) 1D77B MATHEMATICAL SANS-SERIF BOLD SMALL MU # →μ→ +← (‎ 𝞵 ‎) 1D7B5 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL MU # →μ→ + +# ¶ ⸿ + (‎ ¶ ‎) 00B6 PILCROW SIGN +← (‎ ⸿ ‎) 2E3F CAPITULUM + +# · ᐧ ‧ ・ ᛫ • ∙ ⋅ ⸱ 𐄁 ꞏ · ・ + (‎ · ‎) 00B7 MIDDLE DOT +← (‎ ᐧ ‎) 1427 CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ‧ ‎) 2027 HYPHENATION POINT +← (‎ ・ ‎) 30FB KATAKANA MIDDLE DOT # →•→ +← (‎ ᛫ ‎) 16EB RUNIC SINGLE PUNCTUATION +← (‎ • ‎) 2022 BULLET +← (‎ ∙ ‎) 2219 BULLET OPERATOR +← (‎ ⋅ ‎) 22C5 DOT OPERATOR +← (‎ ⸱ ‎) 2E31 WORD SEPARATOR MIDDLE DOT +← (‎ 𐄁 ‎) 10101 AEGEAN WORD SEPARATOR DOT +← (‎ ꞏ ‎) A78F LATIN LETTER SINOLOGICAL DOT +← (‎ · ‎) 0387 GREEK ANO TELEIA +← (‎ ・ ‎) FF65 HALFWIDTH KATAKANA MIDDLE DOT # →•→ + +# ·4 ᐧ4 ᔯ + (‎ ·4 ‎) 00B7 0034 MIDDLE DOT, DIGIT FOUR +← (‎ ᐧ4 ‎) 1427 0034 CANADIAN SYLLABICS FINAL MIDDLE DOT, DIGIT FOUR +← (‎ ᔯ ‎) 152F CANADIAN SYLLABICS YWE # →ᐧ4→ + +# ·< ᐧᐸ ᑄ + (‎ ·< ‎) 00B7 003C MIDDLE DOT, LESS-THAN SIGN +← (‎ ᐧᐸ ‎) 1427 1438 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS PA +← (‎ ᑄ ‎) 1444 CANADIAN SYLLABICS PWA # →ᐧᐸ→ + +# ·> ᐧᐳ ᐷ ᑀ ⋗ + (‎ ·> ‎) 00B7 003E MIDDLE DOT, GREATER-THAN SIGN +← (‎ ᐧᐳ ‎) 1427 1433 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS PO +← (‎ ᐷ ‎) 1437 CANADIAN SYLLABICS CARRIER HI # →ᑀ→→ᐧᐳ→ +← (‎ ᑀ ‎) 1440 CANADIAN SYLLABICS PWO # →ᐧᐳ→ +← (‎ ⋗ ‎) 22D7 GREATER-THAN WITH DOT # →ᑀ→→ᐧᐳ→ + +# ·J ·Ꭻ ·ᒍ ᐧᒍ ᒘ + (‎ ·J ‎) 00B7 004A MIDDLE DOT, LATIN CAPITAL LETTER J +← (‎ ·Ꭻ ‎) 00B7 13AB MIDDLE DOT, CHEROKEE LETTER GU # →ᐧᒍ→ +← (‎ ·ᒍ ‎) 00B7 148D MIDDLE DOT, CANADIAN SYLLABICS CO # →ᐧᒍ→ +← (‎ ᐧᒍ ‎) 1427 148D CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS CO +← (‎ ᒘ ‎) 1498 CANADIAN SYLLABICS CWO # →ᐧᒍ→ + +# ·L ᐧL ·Ꮮ ·ᒪ ᐧᒪ ᒶ + (‎ ·L ‎) 00B7 004C MIDDLE DOT, LATIN CAPITAL LETTER L +← (‎ ᐧL ‎) 1427 004C CANADIAN SYLLABICS FINAL MIDDLE DOT, LATIN CAPITAL LETTER L +← (‎ ·Ꮮ ‎) 00B7 13DE MIDDLE DOT, CHEROKEE LETTER TLE # →·ᒪ→→ᐧᒪ→→ᒶ→→ᐧL→ +← (‎ ·ᒪ ‎) 00B7 14AA MIDDLE DOT, CANADIAN SYLLABICS MA # →ᐧᒪ→→ᒶ→→ᐧL→ +← (‎ ᐧᒪ ‎) 1427 14AA CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS MA # →ᒶ→→ᐧL→ +← (‎ ᒶ ‎) 14B6 CANADIAN SYLLABICS MWA # →ᐧL→ + +# ·P ·Ꮲ ·ᑭ ᐧᑭ ᑶ + (‎ ·P ‎) 00B7 0050 MIDDLE DOT, LATIN CAPITAL LETTER P +← (‎ ·Ꮲ ‎) 00B7 13E2 MIDDLE DOT, CHEROKEE LETTER TLV # →ᐧᑭ→ +← (‎ ·ᑭ ‎) 00B7 146D MIDDLE DOT, CANADIAN SYLLABICS KI # →ᐧᑭ→ +← (‎ ᐧᑭ ‎) 1427 146D CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KI +← (‎ ᑶ ‎) 1476 CANADIAN SYLLABICS KWI # →ᐧᑭ→ + +# ·U ·ሀ ·ᑌ ᐧᑌ ᑗ + (‎ ·U ‎) 00B7 0055 MIDDLE DOT, LATIN CAPITAL LETTER U +← (‎ ·ሀ ‎) 00B7 1200 MIDDLE DOT, ETHIOPIC SYLLABLE HA # →·ᑌ→ +← (‎ ·ᑌ ‎) 00B7 144C MIDDLE DOT, CANADIAN SYLLABICS TE +← (‎ ᐧᑌ ‎) 1427 144C CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TE # →·ᑌ→ +← (‎ ᑗ ‎) 1457 CANADIAN SYLLABICS TWE # →ᐧᑌ→→·ᑌ→ + +# ·V ·٧ ·ᐯ ᐧᐯ ᐺ + (‎ ·V ‎) 00B7 0056 MIDDLE DOT, LATIN CAPITAL LETTER V +← (‎ ·٧ ‎) 00B7 0667 MIDDLE DOT, ARABIC-INDIC DIGIT SEVEN # →ᐧᐯ→ +← (‎ ·ᐯ ‎) 00B7 142F MIDDLE DOT, CANADIAN SYLLABICS PE # →ᐧᐯ→ +← (‎ ᐧᐯ ‎) 1427 142F CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS PE +← (‎ ᐺ ‎) 143A CANADIAN SYLLABICS PWE # →ᐧᐯ→ + +# ·b ·ᑲ ᐧᑲ ᑾ + (‎ ·b ‎) 00B7 0062 MIDDLE DOT, LATIN SMALL LETTER B +← (‎ ·ᑲ ‎) 00B7 1472 MIDDLE DOT, CANADIAN SYLLABICS KA # →ᐧᑲ→ +← (‎ ᐧᑲ ‎) 1427 1472 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KA +← (‎ ᑾ ‎) 147E CANADIAN SYLLABICS KWA # →ᐧᑲ→ + +# ·ḃ ·ᑳ ᐧᑳ ᒀ + (‎ ·ḃ ‎) 00B7 0062 0307 MIDDLE DOT, LATIN SMALL LETTER B, COMBINING DOT ABOVE +← (‎ ·ᑳ ‎) 00B7 1473 MIDDLE DOT, CANADIAN SYLLABICS KAA # →ᐧᑳ→ +← (‎ ᐧᑳ ‎) 1427 1473 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KAA +← (‎ ᒀ ‎) 1480 CANADIAN SYLLABICS KWAA # →ᐧᑳ→ + +# ·d ·ᑯ ᐧᑯ ᑺ + (‎ ·d ‎) 00B7 0064 MIDDLE DOT, LATIN SMALL LETTER D +← (‎ ·ᑯ ‎) 00B7 146F MIDDLE DOT, CANADIAN SYLLABICS KO # →ᐧᑯ→ +← (‎ ᐧᑯ ‎) 1427 146F CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KO +← (‎ ᑺ ‎) 147A CANADIAN SYLLABICS KWO # →ᐧᑯ→ + +# ··· ⵈ ⋯ + (‎ ··· ‎) 00B7 00B7 00B7 MIDDLE DOT, MIDDLE DOT, MIDDLE DOT +← (‎ ⵈ ‎) 2D48 TIFINAGH LETTER TUAREG YAQ # →⋯→ +← (‎ ⋯ ‎) 22EF MIDLINE HORIZONTAL ELLIPSIS + +# ·Ʌ ·٨ ·ᐱ ᐧᐱ ᐼ + (‎ ·Ʌ ‎) 00B7 0245 MIDDLE DOT, LATIN CAPITAL LETTER TURNED V +← (‎ ·٨ ‎) 00B7 0668 MIDDLE DOT, ARABIC-INDIC DIGIT EIGHT # →·ᐱ→ +← (‎ ·ᐱ ‎) 00B7 1431 MIDDLE DOT, CANADIAN SYLLABICS PI +← (‎ ᐧᐱ ‎) 1427 1431 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS PI # →·ᐱ→ +← (‎ ᐼ ‎) 143C CANADIAN SYLLABICS PWI # →ᐧᐱ→→·ᐱ→ + +# ·Γ ·Ꮁ ·ᒥ ᐧᒥ ᒮ + (‎ ·Γ ‎) 00B7 0393 MIDDLE DOT, GREEK CAPITAL LETTER GAMMA +← (‎ ·Ꮁ ‎) 00B7 13B1 MIDDLE DOT, CHEROKEE LETTER HU # →·ᒥ→ +← (‎ ·ᒥ ‎) 00B7 14A5 MIDDLE DOT, CANADIAN SYLLABICS MI +← (‎ ᐧᒥ ‎) 1427 14A5 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS MI # →·ᒥ→ +← (‎ ᒮ ‎) 14AE CANADIAN SYLLABICS MWI # →ᐧᒥ→→·ᒥ→ + +# ·Δ ·ᐃ ᐧᐃ ᐎ + (‎ ·Δ ‎) 00B7 0394 MIDDLE DOT, GREEK CAPITAL LETTER DELTA +← (‎ ·ᐃ ‎) 00B7 1403 MIDDLE DOT, CANADIAN SYLLABICS I # →ᐧᐃ→ +← (‎ ᐧᐃ ‎) 1427 1403 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS I +← (‎ ᐎ ‎) 140E CANADIAN SYLLABICS WI # →ᐧᐃ→ + +# ·Ո ·በ ·ᑎ ᐧᑎ ᑙ + (‎ ·Ո ‎) 00B7 0548 MIDDLE DOT, ARMENIAN CAPITAL LETTER VO +← (‎ ·በ ‎) 00B7 1260 MIDDLE DOT, ETHIOPIC SYLLABLE BA # →·ᑎ→ +← (‎ ·ᑎ ‎) 00B7 144E MIDDLE DOT, CANADIAN SYLLABICS TI +← (‎ ᐧᑎ ‎) 1427 144E CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TI # →·ᑎ→ +← (‎ ᑙ ‎) 1459 CANADIAN SYLLABICS TWI # →ᐧᑎ→→·ᑎ→ + +# ·ᐁ ᐧᐁ ᐌ + (‎ ·ᐁ ‎) 00B7 1401 MIDDLE DOT, CANADIAN SYLLABICS E +← (‎ ᐧᐁ ‎) 1427 1401 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS E +← (‎ ᐌ ‎) 140C CANADIAN SYLLABICS WE # →ᐧᐁ→ + +# ·ᐄ ᐧᐄ ᐐ + (‎ ·ᐄ ‎) 00B7 1404 MIDDLE DOT, CANADIAN SYLLABICS II +← (‎ ᐧᐄ ‎) 1427 1404 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS II +← (‎ ᐐ ‎) 1410 CANADIAN SYLLABICS WII # →ᐧᐄ→ + +# ·ᐅ ᐧᐅ ᐒ + (‎ ·ᐅ ‎) 00B7 1405 MIDDLE DOT, CANADIAN SYLLABICS O +← (‎ ᐧᐅ ‎) 1427 1405 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS O +← (‎ ᐒ ‎) 1412 CANADIAN SYLLABICS WO # →ᐧᐅ→ + +# ·ᐆ ᐧᐆ ᐔ + (‎ ·ᐆ ‎) 00B7 1406 MIDDLE DOT, CANADIAN SYLLABICS OO +← (‎ ᐧᐆ ‎) 1427 1406 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS OO +← (‎ ᐔ ‎) 1414 CANADIAN SYLLABICS WOO # →ᐧᐆ→ + +# ·ᐊ ᐧᐊ ᐗ + (‎ ·ᐊ ‎) 00B7 140A MIDDLE DOT, CANADIAN SYLLABICS A +← (‎ ᐧᐊ ‎) 1427 140A CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS A +← (‎ ᐗ ‎) 1417 CANADIAN SYLLABICS WA # →ᐧᐊ→ + +# ·ᐋ ᐧᐋ ᐙ + (‎ ·ᐋ ‎) 00B7 140B MIDDLE DOT, CANADIAN SYLLABICS AA +← (‎ ᐧᐋ ‎) 1427 140B CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS AA +← (‎ ᐙ ‎) 1419 CANADIAN SYLLABICS WAA # →ᐧᐋ→ + +# ·ᐲ ᐧᐲ ᐾ + (‎ ·ᐲ ‎) 00B7 1432 MIDDLE DOT, CANADIAN SYLLABICS PII +← (‎ ᐧᐲ ‎) 1427 1432 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS PII +← (‎ ᐾ ‎) 143E CANADIAN SYLLABICS PWII # →ᐧᐲ→ + +# ·ᐴ ᐧᐴ ᑂ + (‎ ·ᐴ ‎) 00B7 1434 MIDDLE DOT, CANADIAN SYLLABICS POO +← (‎ ᐧᐴ ‎) 1427 1434 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS POO +← (‎ ᑂ ‎) 1442 CANADIAN SYLLABICS PWOO # →ᐧᐴ→ + +# ·ᐹ ᐧᐹ ᑆ + (‎ ·ᐹ ‎) 00B7 1439 MIDDLE DOT, CANADIAN SYLLABICS PAA +← (‎ ᐧᐹ ‎) 1427 1439 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS PAA +← (‎ ᑆ ‎) 1446 CANADIAN SYLLABICS PWAA # →ᐧᐹ→ + +# ·ᑏ ᐧᑏ ᑛ + (‎ ·ᑏ ‎) 00B7 144F MIDDLE DOT, CANADIAN SYLLABICS TII +← (‎ ᐧᑏ ‎) 1427 144F CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TII +← (‎ ᑛ ‎) 145B CANADIAN SYLLABICS TWII # →ᐧᑏ→ + +# ·ᑐ ᐧᑐ ᑔ ᑝ + (‎ ·ᑐ ‎) 00B7 1450 MIDDLE DOT, CANADIAN SYLLABICS TO +← (‎ ᐧᑐ ‎) 1427 1450 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TO +← (‎ ᑔ ‎) 1454 CANADIAN SYLLABICS CARRIER DI # →ᑝ→→ᐧᑐ→ +← (‎ ᑝ ‎) 145D CANADIAN SYLLABICS TWO # →ᐧᑐ→ + +# ·ᑑ ᐧᑑ ᑟ + (‎ ·ᑑ ‎) 00B7 1451 MIDDLE DOT, CANADIAN SYLLABICS TOO +← (‎ ᐧᑑ ‎) 1427 1451 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TOO +← (‎ ᑟ ‎) 145F CANADIAN SYLLABICS TWOO # →ᐧᑑ→ + +# ·ᑕ ᐧᑕ ᑡ + (‎ ·ᑕ ‎) 00B7 1455 MIDDLE DOT, CANADIAN SYLLABICS TA +← (‎ ᐧᑕ ‎) 1427 1455 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TA +← (‎ ᑡ ‎) 1461 CANADIAN SYLLABICS TWA # →ᐧᑕ→ + +# ·ᑖ ᐧᑖ ᑣ + (‎ ·ᑖ ‎) 00B7 1456 MIDDLE DOT, CANADIAN SYLLABICS TAA +← (‎ ᐧᑖ ‎) 1427 1456 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS TAA +← (‎ ᑣ ‎) 1463 CANADIAN SYLLABICS TWAA # →ᐧᑖ→ + +# ·ᑫ ᐧᑫ ᑴ + (‎ ·ᑫ ‎) 00B7 146B MIDDLE DOT, CANADIAN SYLLABICS KE +← (‎ ᐧᑫ ‎) 1427 146B CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KE +← (‎ ᑴ ‎) 1474 CANADIAN SYLLABICS KWE # →ᐧᑫ→ + +# ·ᑮ ᐧᑮ ᑸ + (‎ ·ᑮ ‎) 00B7 146E MIDDLE DOT, CANADIAN SYLLABICS KII +← (‎ ᐧᑮ ‎) 1427 146E CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KII +← (‎ ᑸ ‎) 1478 CANADIAN SYLLABICS KWII # →ᐧᑮ→ + +# ·ᑰ ᐧᑰ ᑼ + (‎ ·ᑰ ‎) 00B7 1470 MIDDLE DOT, CANADIAN SYLLABICS KOO +← (‎ ᐧᑰ ‎) 1427 1470 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KOO +← (‎ ᑼ ‎) 147C CANADIAN SYLLABICS KWOO # →ᐧᑰ→ + +# ·ᒉ ᐧᒉ ᒒ + (‎ ·ᒉ ‎) 00B7 1489 MIDDLE DOT, CANADIAN SYLLABICS CE +← (‎ ᐧᒉ ‎) 1427 1489 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS CE +← (‎ ᒒ ‎) 1492 CANADIAN SYLLABICS CWE # →ᐧᒉ→ + +# ·ᒋ ᐧᒋ ᒔ + (‎ ·ᒋ ‎) 00B7 148B MIDDLE DOT, CANADIAN SYLLABICS CI +← (‎ ᐧᒋ ‎) 1427 148B CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS CI +← (‎ ᒔ ‎) 1494 CANADIAN SYLLABICS CWI # →ᐧᒋ→ + +# ·ᒌ ᐧᒌ ᒖ + (‎ ·ᒌ ‎) 00B7 148C MIDDLE DOT, CANADIAN SYLLABICS CII +← (‎ ᐧᒌ ‎) 1427 148C CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS CII +← (‎ ᒖ ‎) 1496 CANADIAN SYLLABICS CWII # →ᐧᒌ→ + +# ·ᒎ ᐧᒎ ᒚ + (‎ ·ᒎ ‎) 00B7 148E MIDDLE DOT, CANADIAN SYLLABICS COO +← (‎ ᐧᒎ ‎) 1427 148E CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS COO +← (‎ ᒚ ‎) 149A CANADIAN SYLLABICS CWOO # →ᐧᒎ→ + +# ·ᒐ ᐧᒐ ᒜ + (‎ ·ᒐ ‎) 00B7 1490 MIDDLE DOT, CANADIAN SYLLABICS CA +← (‎ ᐧᒐ ‎) 1427 1490 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS CA +← (‎ ᒜ ‎) 149C CANADIAN SYLLABICS CWA # →ᐧᒐ→ + +# ·ᒑ ᐧᒑ ᒞ + (‎ ·ᒑ ‎) 00B7 1491 MIDDLE DOT, CANADIAN SYLLABICS CAA +← (‎ ᐧᒑ ‎) 1427 1491 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS CAA +← (‎ ᒞ ‎) 149E CANADIAN SYLLABICS CWAA # →ᐧᒑ→ + +# ·ᒣ ᐧᒣ ᒬ + (‎ ·ᒣ ‎) 00B7 14A3 MIDDLE DOT, CANADIAN SYLLABICS ME +← (‎ ᐧᒣ ‎) 1427 14A3 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS ME +← (‎ ᒬ ‎) 14AC CANADIAN SYLLABICS MWE # →ᐧᒣ→ + +# ·ᒦ ᐧᒦ ᒰ + (‎ ·ᒦ ‎) 00B7 14A6 MIDDLE DOT, CANADIAN SYLLABICS MII +← (‎ ᐧᒦ ‎) 1427 14A6 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS MII +← (‎ ᒰ ‎) 14B0 CANADIAN SYLLABICS MWII # →ᐧᒦ→ + +# ·ᒧ ᐧᒧ ᒲ + (‎ ·ᒧ ‎) 00B7 14A7 MIDDLE DOT, CANADIAN SYLLABICS MO +← (‎ ᐧᒧ ‎) 1427 14A7 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS MO +← (‎ ᒲ ‎) 14B2 CANADIAN SYLLABICS MWO # →ᐧᒧ→ + +# ·ᒨ ᐧᒨ ᒴ + (‎ ·ᒨ ‎) 00B7 14A8 MIDDLE DOT, CANADIAN SYLLABICS MOO +← (‎ ᐧᒨ ‎) 1427 14A8 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS MOO +← (‎ ᒴ ‎) 14B4 CANADIAN SYLLABICS MWOO # →ᐧᒨ→ + +# ·ᒫ ᐧᒫ ᒸ + (‎ ·ᒫ ‎) 00B7 14AB MIDDLE DOT, CANADIAN SYLLABICS MAA +← (‎ ᐧᒫ ‎) 1427 14AB CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS MAA +← (‎ ᒸ ‎) 14B8 CANADIAN SYLLABICS MWAA # →ᐧᒫ→ + +# ·ᓀ ᐧᓀ ᓉ + (‎ ·ᓀ ‎) 00B7 14C0 MIDDLE DOT, CANADIAN SYLLABICS NE +← (‎ ᐧᓀ ‎) 1427 14C0 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NE +← (‎ ᓉ ‎) 14C9 CANADIAN SYLLABICS NWE # →ᐧᓀ→ + +# ·ᓂ ᐧᓂ ᣆ + (‎ ·ᓂ ‎) 00B7 14C2 MIDDLE DOT, CANADIAN SYLLABICS NI +← (‎ ᐧᓂ ‎) 1427 14C2 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NI +← (‎ ᣆ ‎) 18C6 CANADIAN SYLLABICS NWI # →ᐧᓂ→ + +# ·ᓃ ᐧᓃ ᣈ + (‎ ·ᓃ ‎) 00B7 14C3 MIDDLE DOT, CANADIAN SYLLABICS NII +← (‎ ᐧᓃ ‎) 1427 14C3 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NII +← (‎ ᣈ ‎) 18C8 CANADIAN SYLLABICS NWII # →ᐧᓃ→ + +# ·ᓄ ᐧᓄ ᣊ + (‎ ·ᓄ ‎) 00B7 14C4 MIDDLE DOT, CANADIAN SYLLABICS NO +← (‎ ᐧᓄ ‎) 1427 14C4 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NO +← (‎ ᣊ ‎) 18CA CANADIAN SYLLABICS NWO # →ᐧᓄ→ + +# ·ᓅ ᐧᓅ ᣌ + (‎ ·ᓅ ‎) 00B7 14C5 MIDDLE DOT, CANADIAN SYLLABICS NOO +← (‎ ᐧᓅ ‎) 1427 14C5 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NOO +← (‎ ᣌ ‎) 18CC CANADIAN SYLLABICS NWOO # →ᐧᓅ→ + +# ·ᓇ ᐧᓇ ᓋ + (‎ ·ᓇ ‎) 00B7 14C7 MIDDLE DOT, CANADIAN SYLLABICS NA +← (‎ ᐧᓇ ‎) 1427 14C7 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NA +← (‎ ᓋ ‎) 14CB CANADIAN SYLLABICS NWA # →ᐧᓇ→ + +# ·ᓈ ᐧᓈ ᓍ + (‎ ·ᓈ ‎) 00B7 14C8 MIDDLE DOT, CANADIAN SYLLABICS NAA +← (‎ ᐧᓈ ‎) 1427 14C8 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS NAA +← (‎ ᓍ ‎) 14CD CANADIAN SYLLABICS NWAA # →ᐧᓈ→ + +# ·ᓓ ᐧᓓ ᓜ + (‎ ·ᓓ ‎) 00B7 14D3 MIDDLE DOT, CANADIAN SYLLABICS LE +← (‎ ᐧᓓ ‎) 1427 14D3 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LE +← (‎ ᓜ ‎) 14DC CANADIAN SYLLABICS LWE # →ᐧᓓ→ + +# ·ᓕ ᐧᓕ ᓞ + (‎ ·ᓕ ‎) 00B7 14D5 MIDDLE DOT, CANADIAN SYLLABICS LI +← (‎ ᐧᓕ ‎) 1427 14D5 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LI +← (‎ ᓞ ‎) 14DE CANADIAN SYLLABICS LWI # →ᐧᓕ→ + +# ·ᓖ ᐧᓖ ᓠ + (‎ ·ᓖ ‎) 00B7 14D6 MIDDLE DOT, CANADIAN SYLLABICS LII +← (‎ ᐧᓖ ‎) 1427 14D6 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LII +← (‎ ᓠ ‎) 14E0 CANADIAN SYLLABICS LWII # →ᐧᓖ→ + +# ·ᓗ ᐧᓗ ᓢ + (‎ ·ᓗ ‎) 00B7 14D7 MIDDLE DOT, CANADIAN SYLLABICS LO +← (‎ ᐧᓗ ‎) 1427 14D7 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LO +← (‎ ᓢ ‎) 14E2 CANADIAN SYLLABICS LWO # →ᐧᓗ→ + +# ·ᓘ ᐧᓘ ᓤ + (‎ ·ᓘ ‎) 00B7 14D8 MIDDLE DOT, CANADIAN SYLLABICS LOO +← (‎ ᐧᓘ ‎) 1427 14D8 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LOO +← (‎ ᓤ ‎) 14E4 CANADIAN SYLLABICS LWOO # →ᐧᓘ→ + +# ·ᓚ ᐧᓚ ᓦ + (‎ ·ᓚ ‎) 00B7 14DA MIDDLE DOT, CANADIAN SYLLABICS LA +← (‎ ᐧᓚ ‎) 1427 14DA CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LA +← (‎ ᓦ ‎) 14E6 CANADIAN SYLLABICS LWA # →ᐧᓚ→ + +# ·ᓛ ᐧᓛ ᓨ + (‎ ·ᓛ ‎) 00B7 14DB MIDDLE DOT, CANADIAN SYLLABICS LAA +← (‎ ᐧᓛ ‎) 1427 14DB CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS LAA +← (‎ ᓨ ‎) 14E8 CANADIAN SYLLABICS LWAA # →ᐧᓛ→ + +# ·ᓭ ᐧᓭ ᓶ + (‎ ·ᓭ ‎) 00B7 14ED MIDDLE DOT, CANADIAN SYLLABICS SE +← (‎ ᐧᓭ ‎) 1427 14ED CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SE +← (‎ ᓶ ‎) 14F6 CANADIAN SYLLABICS SWE # →ᐧᓭ→ + +# ·ᓯ ᐧᓯ ᓸ + (‎ ·ᓯ ‎) 00B7 14EF MIDDLE DOT, CANADIAN SYLLABICS SI +← (‎ ᐧᓯ ‎) 1427 14EF CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SI +← (‎ ᓸ ‎) 14F8 CANADIAN SYLLABICS SWI # →ᐧᓯ→ + +# ·ᓰ ᐧᓰ ᓺ + (‎ ·ᓰ ‎) 00B7 14F0 MIDDLE DOT, CANADIAN SYLLABICS SII +← (‎ ᐧᓰ ‎) 1427 14F0 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SII +← (‎ ᓺ ‎) 14FA CANADIAN SYLLABICS SWII # →ᐧᓰ→ + +# ·ᓱ ᐧᓱ ᓼ + (‎ ·ᓱ ‎) 00B7 14F1 MIDDLE DOT, CANADIAN SYLLABICS SO +← (‎ ᐧᓱ ‎) 1427 14F1 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SO +← (‎ ᓼ ‎) 14FC CANADIAN SYLLABICS SWO # →ᐧᓱ→ + +# ·ᓲ ᐧᓲ ᓾ + (‎ ·ᓲ ‎) 00B7 14F2 MIDDLE DOT, CANADIAN SYLLABICS SOO +← (‎ ᐧᓲ ‎) 1427 14F2 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SOO +← (‎ ᓾ ‎) 14FE CANADIAN SYLLABICS SWOO # →ᐧᓲ→ + +# ·ᓴ ᐧᓴ ᔀ + (‎ ·ᓴ ‎) 00B7 14F4 MIDDLE DOT, CANADIAN SYLLABICS SA +← (‎ ᐧᓴ ‎) 1427 14F4 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SA +← (‎ ᔀ ‎) 1500 CANADIAN SYLLABICS SWA # →ᐧᓴ→ + +# ·ᓵ ᐧᓵ ᔂ + (‎ ·ᓵ ‎) 00B7 14F5 MIDDLE DOT, CANADIAN SYLLABICS SAA +← (‎ ᐧᓵ ‎) 1427 14F5 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SAA +← (‎ ᔂ ‎) 1502 CANADIAN SYLLABICS SWAA # →ᐧᓵ→ + +# ·ᔐ ᐧᔐ ᔗ + (‎ ·ᔐ ‎) 00B7 1510 MIDDLE DOT, CANADIAN SYLLABICS SHE +← (‎ ᐧᔐ ‎) 1427 1510 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHE +← (‎ ᔗ ‎) 1517 CANADIAN SYLLABICS SHWE # →ᐧᔐ→ + +# ·ᔑ ᐧᔑ ᔙ + (‎ ·ᔑ ‎) 00B7 1511 MIDDLE DOT, CANADIAN SYLLABICS SHI +← (‎ ᐧᔑ ‎) 1427 1511 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHI +← (‎ ᔙ ‎) 1519 CANADIAN SYLLABICS SHWI # →ᐧᔑ→ + +# ·ᔒ ᐧᔒ ᔛ + (‎ ·ᔒ ‎) 00B7 1512 MIDDLE DOT, CANADIAN SYLLABICS SHII +← (‎ ᐧᔒ ‎) 1427 1512 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHII +← (‎ ᔛ ‎) 151B CANADIAN SYLLABICS SHWII # →ᐧᔒ→ + +# ·ᔓ ᐧᔓ ᔝ + (‎ ·ᔓ ‎) 00B7 1513 MIDDLE DOT, CANADIAN SYLLABICS SHO +← (‎ ᐧᔓ ‎) 1427 1513 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHO +← (‎ ᔝ ‎) 151D CANADIAN SYLLABICS SHWO # →ᐧᔓ→ + +# ·ᔔ ᐧᔔ ᔟ + (‎ ·ᔔ ‎) 00B7 1514 MIDDLE DOT, CANADIAN SYLLABICS SHOO +← (‎ ᐧᔔ ‎) 1427 1514 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHOO +← (‎ ᔟ ‎) 151F CANADIAN SYLLABICS SHWOO # →ᐧᔔ→ + +# ·ᔕ ᐧᔕ ᔡ + (‎ ·ᔕ ‎) 00B7 1515 MIDDLE DOT, CANADIAN SYLLABICS SHA +← (‎ ᐧᔕ ‎) 1427 1515 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHA +← (‎ ᔡ ‎) 1521 CANADIAN SYLLABICS SHWA # →ᐧᔕ→ + +# ·ᔖ ᐧᔖ ᔣ + (‎ ·ᔖ ‎) 00B7 1516 MIDDLE DOT, CANADIAN SYLLABICS SHAA +← (‎ ᐧᔖ ‎) 1427 1516 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHAA +← (‎ ᔣ ‎) 1523 CANADIAN SYLLABICS SHWAA # →ᐧᔖ→ + +# ·ᔨ ᐧᔨ ᔱ + (‎ ·ᔨ ‎) 00B7 1528 MIDDLE DOT, CANADIAN SYLLABICS YI +← (‎ ᐧᔨ ‎) 1427 1528 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS YI +← (‎ ᔱ ‎) 1531 CANADIAN SYLLABICS YWI # →ᐧᔨ→ + +# ·ᔩ ᐧᔩ ᔳ + (‎ ·ᔩ ‎) 00B7 1529 MIDDLE DOT, CANADIAN SYLLABICS YII +← (‎ ᐧᔩ ‎) 1427 1529 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS YII +← (‎ ᔳ ‎) 1533 CANADIAN SYLLABICS YWII # →ᐧᔩ→ + +# ·ᔪ ᐧᔪ ᔵ + (‎ ·ᔪ ‎) 00B7 152A MIDDLE DOT, CANADIAN SYLLABICS YO +← (‎ ᐧᔪ ‎) 1427 152A CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS YO +← (‎ ᔵ ‎) 1535 CANADIAN SYLLABICS YWO # →ᐧᔪ→ + +# ·ᔫ ᐧᔫ ᔷ + (‎ ·ᔫ ‎) 00B7 152B MIDDLE DOT, CANADIAN SYLLABICS YOO +← (‎ ᐧᔫ ‎) 1427 152B CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS YOO +← (‎ ᔷ ‎) 1537 CANADIAN SYLLABICS YWOO # →ᐧᔫ→ + +# ·ᔭ ᐧᔭ ᔹ + (‎ ·ᔭ ‎) 00B7 152D MIDDLE DOT, CANADIAN SYLLABICS YA +← (‎ ᐧᔭ ‎) 1427 152D CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS YA +← (‎ ᔹ ‎) 1539 CANADIAN SYLLABICS YWA # →ᐧᔭ→ + +# ·ᔮ ᐧᔮ ᔻ + (‎ ·ᔮ ‎) 00B7 152E MIDDLE DOT, CANADIAN SYLLABICS YAA +← (‎ ᐧᔮ ‎) 1427 152E CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS YAA +← (‎ ᔻ ‎) 153B CANADIAN SYLLABICS YWAA # →ᐧᔮ→ + +# ·ᕃ ᐧᕃ ᣎ + (‎ ·ᕃ ‎) 00B7 1543 MIDDLE DOT, CANADIAN SYLLABICS R-CREE RE +← (‎ ᐧᕃ ‎) 1427 1543 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS R-CREE RE +← (‎ ᣎ ‎) 18CE CANADIAN SYLLABICS RWEE # →ᐧᕃ→ + +# ·ᕆ ᐧᕆ ᣏ + (‎ ·ᕆ ‎) 00B7 1546 MIDDLE DOT, CANADIAN SYLLABICS RI +← (‎ ᐧᕆ ‎) 1427 1546 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS RI +← (‎ ᣏ ‎) 18CF CANADIAN SYLLABICS RWI # →ᐧᕆ→ + +# ·ᕇ ᐧᕇ ᣐ + (‎ ·ᕇ ‎) 00B7 1547 MIDDLE DOT, CANADIAN SYLLABICS RII +← (‎ ᐧᕇ ‎) 1427 1547 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS RII +← (‎ ᣐ ‎) 18D0 CANADIAN SYLLABICS RWII # →ᐧᕇ→ + +# ·ᕈ ᐧᕈ ᣑ + (‎ ·ᕈ ‎) 00B7 1548 MIDDLE DOT, CANADIAN SYLLABICS RO +← (‎ ᐧᕈ ‎) 1427 1548 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS RO +← (‎ ᣑ ‎) 18D1 CANADIAN SYLLABICS RWO # →ᐧᕈ→ + +# ·ᕉ ᐧᕉ ᣒ + (‎ ·ᕉ ‎) 00B7 1549 MIDDLE DOT, CANADIAN SYLLABICS ROO +← (‎ ᐧᕉ ‎) 1427 1549 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS ROO +← (‎ ᣒ ‎) 18D2 CANADIAN SYLLABICS RWOO # →ᐧᕉ→ + +# ·ᕋ ᐧᕋ ᣓ + (‎ ·ᕋ ‎) 00B7 154B MIDDLE DOT, CANADIAN SYLLABICS RA +← (‎ ᐧᕋ ‎) 1427 154B CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS RA +← (‎ ᣓ ‎) 18D3 CANADIAN SYLLABICS RWA # →ᐧᕋ→ + +# ·ᕌ ᐧᕌ ᕎ + (‎ ·ᕌ ‎) 00B7 154C MIDDLE DOT, CANADIAN SYLLABICS RAA +← (‎ ᐧᕌ ‎) 1427 154C CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS RAA +← (‎ ᕎ ‎) 154E CANADIAN SYLLABICS RWAA # →ᐧᕌ→ + +# ·ᕚ ᐧᕚ ᕛ + (‎ ·ᕚ ‎) 00B7 155A MIDDLE DOT, CANADIAN SYLLABICS FAA +← (‎ ᐧᕚ ‎) 1427 155A CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS FAA +← (‎ ᕛ ‎) 155B CANADIAN SYLLABICS FWAA # →ᐧᕚ→ + +# ·ᕧ ᐧᕧ ᕨ + (‎ ·ᕧ ‎) 00B7 1567 MIDDLE DOT, CANADIAN SYLLABICS THAA +← (‎ ᐧᕧ ‎) 1427 1567 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS THAA +← (‎ ᕨ ‎) 1568 CANADIAN SYLLABICS THWAA # →ᐧᕧ→ + +# ·ᢱ ᐧᢱ ᢳ + (‎ ·ᢱ ‎) 00B7 18B1 MIDDLE DOT, CANADIAN SYLLABICS AY +← (‎ ᐧᢱ ‎) 1427 18B1 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS AY +← (‎ ᢳ ‎) 18B3 CANADIAN SYLLABICS WAY # →ᐧᢱ→ + +# ·ᢴ ᐧᢴ ᢶ + (‎ ·ᢴ ‎) 00B7 18B4 MIDDLE DOT, CANADIAN SYLLABICS POY +← (‎ ᐧᢴ ‎) 1427 18B4 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS POY +← (‎ ᢶ ‎) 18B6 CANADIAN SYLLABICS PWOY # →ᐧᢴ→ + +# ·ᢸ ᐧᢸ ᢹ + (‎ ·ᢸ ‎) 00B7 18B8 MIDDLE DOT, CANADIAN SYLLABICS KAY +← (‎ ᐧᢸ ‎) 1427 18B8 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS KAY +← (‎ ᢹ ‎) 18B9 CANADIAN SYLLABICS KWAY # →ᐧᢸ→ + +# ·ᣀ ᐧᣀ ᣂ + (‎ ·ᣀ ‎) 00B7 18C0 MIDDLE DOT, CANADIAN SYLLABICS SHOY +← (‎ ᐧᣀ ‎) 1427 18C0 CANADIAN SYLLABICS FINAL MIDDLE DOT, CANADIAN SYLLABICS SHOY +← (‎ ᣂ ‎) 18C2 CANADIAN SYLLABICS SHWOY # →ᐧᣀ→ + +# º ᵒ ⁰ + (‎ º ‎) 00BA MASCULINE ORDINAL INDICATOR +← (‎ ᵒ ‎) 1D52 MODIFIER LETTER SMALL O # →⁰→ +← (‎ ⁰ ‎) 2070 SUPERSCRIPT ZERO + +# º/₀₀ ⁰/₀₀ ؉ ‰ + (‎ º/₀₀ ‎) 00BA 002F 2080 2080 MASCULINE ORDINAL INDICATOR, SOLIDUS, SUBSCRIPT ZERO, SUBSCRIPT ZERO +← (‎ ⁰/₀₀ ‎) 2070 002F 2080 2080 SUPERSCRIPT ZERO, SOLIDUS, SUBSCRIPT ZERO, SUBSCRIPT ZERO +← (‎ ؉ ‎) 0609 ARABIC-INDIC PER MILLE SIGN # →‰→→⁰/₀₀→ +← (‎ ‰ ‎) 2030 PER MILLE SIGN # →⁰/₀₀→ + +# º/₀₀₀ ⁰/₀₀₀ ؊ ‱ + (‎ º/₀₀₀ ‎) 00BA 002F 2080 2080 2080 MASCULINE ORDINAL INDICATOR, SOLIDUS, SUBSCRIPT ZERO, SUBSCRIPT ZERO, SUBSCRIPT ZERO +← (‎ ⁰/₀₀₀ ‎) 2070 002F 2080 2080 2080 SUPERSCRIPT ZERO, SOLIDUS, SUBSCRIPT ZERO, SUBSCRIPT ZERO, SUBSCRIPT ZERO +← (‎ ؊ ‎) 060A ARABIC-INDIC PER TEN THOUSAND SIGN # →‱→→⁰/₀₀₀→ +← (‎ ‱ ‎) 2031 PER TEN THOUSAND SIGN # →⁰/₀₀₀→ + +# Å Ȧ + (‎ Å ‎) 00C5 LATIN CAPITAL LETTER A WITH RING ABOVE +← (‎ Ȧ ‎) 0226 LATIN CAPITAL LETTER A WITH DOT ABOVE + +# Ö Ő + (‎ Ö ‎) 00D6 LATIN CAPITAL LETTER O WITH DIAERESIS +← (‎ Ő ‎) 0150 LATIN CAPITAL LETTER O WITH DOUBLE ACUTE + +# Þ Ϸ 𐓄 + (‎ Þ ‎) 00DE LATIN CAPITAL LETTER THORN +← (‎ Ϸ ‎) 03F7 GREEK CAPITAL LETTER SHO +← (‎ 𐓄 ‎) 104C4 OSAGE CAPITAL LETTER PA + +# ß β Ᏸ ꞵ ϐ 𝛃 𝛽 𝜷 𝝱 𝞫 + (‎ ß ‎) 00DF LATIN SMALL LETTER SHARP S +← (‎ β ‎) 03B2 GREEK SMALL LETTER BETA +← (‎ Ᏸ ‎) 13F0 CHEROKEE LETTER YE # →β→ +← (‎ ꞵ ‎) A7B5 LATIN SMALL LETTER BETA # →β→ +← (‎ ϐ ‎) 03D0 GREEK BETA SYMBOL # →β→ +← (‎ 𝛃 ‎) 1D6C3 MATHEMATICAL BOLD SMALL BETA # →β→ +← (‎ 𝛽 ‎) 1D6FD MATHEMATICAL ITALIC SMALL BETA # →β→ +← (‎ 𝜷 ‎) 1D737 MATHEMATICAL BOLD ITALIC SMALL BETA # →β→ +← (‎ 𝝱 ‎) 1D771 MATHEMATICAL SANS-SERIF BOLD SMALL BETA # →β→ +← (‎ 𝞫 ‎) 1D7AB MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL BETA # →β→ + +# å ȧ + (‎ å ‎) 00E5 LATIN SMALL LETTER A WITH RING ABOVE +← (‎ ȧ ‎) 0227 LATIN SMALL LETTER A WITH DOT ABOVE + +# ∂̵ ð 𞣍 + (‎ ð ‎) 00F0 LATIN SMALL LETTER ETH +← (‎ ∂̵ ‎) 2202 0335 PARTIAL DIFFERENTIAL, COMBINING SHORT STROKE OVERLAY +← (‎ 𞣍 ‎) 1E8CD MENDE KIKAKUI DIGIT SEVEN + +# ة ۃ ⍥ ö ﺓ ﺔ + (‎ ö ‎) 00F6 LATIN SMALL LETTER O WITH DIAERESIS +← (‎ ة ‎) 0629 ARABIC LETTER TEH MARBUTA +← (‎ ۃ ‎) 06C3 ARABIC LETTER TEH MARBUTA GOAL +← (‎ ⍥ ‎) 2365 APL FUNCTIONAL SYMBOL CIRCLE DIAERESIS +← (‎ ﺓ ‎) FE93 ARABIC LETTER TEH MARBUTA ISOLATED FORM # →‎ة‎→ +← (‎ ﺔ ‎) FE94 ARABIC LETTER TEH MARBUTA FINAL FORM # →‎ة‎→ + +# ÷ ➗ + (‎ ÷ ‎) 00F7 DIVISION SIGN +← (‎ ➗ ‎) 2797 HEAVY DIVISION SIGN + +# þ ƿ ϸ + (‎ þ ‎) 00FE LATIN SMALL LETTER THORN +← (‎ ƿ ‎) 01BF LATIN LETTER WYNN +← (‎ ϸ ‎) 03F8 GREEK SMALL LETTER SHO + +# Ă Ǎ + (‎ Ă ‎) 0102 LATIN CAPITAL LETTER A WITH BREVE +← (‎ Ǎ ‎) 01CD LATIN CAPITAL LETTER A WITH CARON + +# ă ǎ + (‎ ă ‎) 0103 LATIN SMALL LETTER A WITH BREVE +← (‎ ǎ ‎) 01CE LATIN SMALL LETTER A WITH CARON + +# Ĕ Ě + (‎ Ĕ ‎) 0114 LATIN CAPITAL LETTER E WITH BREVE +← (‎ Ě ‎) 011A LATIN CAPITAL LETTER E WITH CARON + +# ĕ ě + (‎ ĕ ‎) 0115 LATIN SMALL LETTER E WITH BREVE +← (‎ ě ‎) 011B LATIN SMALL LETTER E WITH CARON + +# Ğ Ǧ + (‎ Ğ ‎) 011E LATIN CAPITAL LETTER G WITH BREVE +← (‎ Ǧ ‎) 01E6 LATIN CAPITAL LETTER G WITH CARON + +# ğ ǧ + (‎ ğ ‎) 011F LATIN SMALL LETTER G WITH BREVE +← (‎ ǧ ‎) 01E7 LATIN SMALL LETTER G WITH CARON + +# ģ ǵ + (‎ ģ ‎) 0123 LATIN SMALL LETTER G WITH CEDILLA +← (‎ ǵ ‎) 01F5 LATIN SMALL LETTER G WITH ACUTE + +# Ĭ Ǐ + (‎ Ĭ ‎) 012C LATIN CAPITAL LETTER I WITH BREVE +← (‎ Ǐ ‎) 01CF LATIN CAPITAL LETTER I WITH CARON + +# ĭ ǐ + (‎ ĭ ‎) 012D LATIN SMALL LETTER I WITH BREVE +← (‎ ǐ ‎) 01D0 LATIN SMALL LETTER I WITH CARON + +# ĸ ᴋ κ к ⲕ ꮶ ϰ 𝛋 𝛞 𝜅 𝜘 𝜿 𝝒 𝝹 𝞌 𝞳 𝟆 + (‎ ĸ ‎) 0138 LATIN SMALL LETTER KRA +← (‎ ᴋ ‎) 1D0B LATIN LETTER SMALL CAPITAL K +← (‎ κ ‎) 03BA GREEK SMALL LETTER KAPPA +← (‎ к ‎) 043A CYRILLIC SMALL LETTER KA +← (‎ ⲕ ‎) 2C95 COPTIC SMALL LETTER KAPA # →κ→ +← (‎ ꮶ ‎) ABB6 CHEROKEE SMALL LETTER TSO # →ᴋ→ +← (‎ ϰ ‎) 03F0 GREEK KAPPA SYMBOL # →κ→ +← (‎ 𝛋 ‎) 1D6CB MATHEMATICAL BOLD SMALL KAPPA # →κ→ +← (‎ 𝛞 ‎) 1D6DE MATHEMATICAL BOLD KAPPA SYMBOL # →κ→ +← (‎ 𝜅 ‎) 1D705 MATHEMATICAL ITALIC SMALL KAPPA # →κ→ +← (‎ 𝜘 ‎) 1D718 MATHEMATICAL ITALIC KAPPA SYMBOL # →κ→ +← (‎ 𝜿 ‎) 1D73F MATHEMATICAL BOLD ITALIC SMALL KAPPA # →κ→ +← (‎ 𝝒 ‎) 1D752 MATHEMATICAL BOLD ITALIC KAPPA SYMBOL # →κ→ +← (‎ 𝝹 ‎) 1D779 MATHEMATICAL SANS-SERIF BOLD SMALL KAPPA # →κ→ +← (‎ 𝞌 ‎) 1D78C MATHEMATICAL SANS-SERIF BOLD KAPPA SYMBOL # →κ→ +← (‎ 𝞳 ‎) 1D7B3 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL KAPPA # →κ→ +← (‎ 𝟆 ‎) 1D7C6 MATHEMATICAL SANS-SERIF BOLD ITALIC KAPPA SYMBOL # →κ→ + +# ĸ̩ к̩ қ + (‎ ĸ̩ ‎) 0138 0329 LATIN SMALL LETTER KRA, COMBINING VERTICAL LINE BELOW +← (‎ к̩ ‎) 043A 0329 CYRILLIC SMALL LETTER KA, COMBINING VERTICAL LINE BELOW +← (‎ қ ‎) 049B CYRILLIC SMALL LETTER KA WITH DESCENDER # →к̩→ + +# ĸ̵ к̵ ҟ + (‎ ĸ̵ ‎) 0138 0335 LATIN SMALL LETTER KRA, COMBINING SHORT STROKE OVERLAY +← (‎ к̵ ‎) 043A 0335 CYRILLIC SMALL LETTER KA, COMBINING SHORT STROKE OVERLAY +← (‎ ҟ ‎) 049F CYRILLIC SMALL LETTER KA WITH STROKE # →к̵→ + +# ɲ ņ + (‎ ņ ‎) 0146 LATIN SMALL LETTER N WITH CEDILLA +← (‎ ɲ ‎) 0272 LATIN SMALL LETTER N WITH LEFT HOOK + +# Ŏ Ǒ + (‎ Ŏ ‎) 014E LATIN CAPITAL LETTER O WITH BREVE +← (‎ Ǒ ‎) 01D1 LATIN CAPITAL LETTER O WITH CARON + +# ŏ ǒ + (‎ ŏ ‎) 014F LATIN SMALL LETTER O WITH BREVE +← (‎ ǒ ‎) 01D2 LATIN SMALL LETTER O WITH CARON + +# Ţ Ț + (‎ Ţ ‎) 0162 LATIN CAPITAL LETTER T WITH CEDILLA +← (‎ Ț ‎) 021A LATIN CAPITAL LETTER T WITH COMMA BELOW + +# ƫ Ꮏ ţ ț + (‎ ţ ‎) 0163 LATIN SMALL LETTER T WITH CEDILLA +← (‎ ƫ ‎) 01AB LATIN SMALL LETTER T WITH PALATAL HOOK +← (‎ Ꮏ ‎) 13BF CHEROKEE LETTER HNA # →ƫ→ +← (‎ ț ‎) 021B LATIN SMALL LETTER T WITH COMMA BELOW + +# Ŭ Ǔ + (‎ Ŭ ‎) 016C LATIN CAPITAL LETTER U WITH BREVE +← (‎ Ǔ ‎) 01D3 LATIN CAPITAL LETTER U WITH CARON + +# ŭ ǔ + (‎ ŭ ‎) 016D LATIN SMALL LETTER U WITH BREVE +← (‎ ǔ ‎) 01D4 LATIN SMALL LETTER U WITH CARON + +# ƅ ь ꮟ + (‎ ƅ ‎) 0185 LATIN SMALL LETTER TONE SIX +← (‎ ь ‎) 044C CYRILLIC SMALL LETTER SOFT SIGN +← (‎ ꮟ ‎) AB9F CHEROKEE SMALL LETTER SI # →ь→ + +# ƅi ьi ьı ы + (‎ ƅi ‎) 0185 0069 LATIN SMALL LETTER TONE SIX, LATIN SMALL LETTER I +← (‎ ьi ‎) 044C 0069 CYRILLIC SMALL LETTER SOFT SIGN, LATIN SMALL LETTER I # →ьı→ +← (‎ ьı ‎) 044C 0131 CYRILLIC SMALL LETTER SOFT SIGN, LATIN SMALL LETTER DOTLESS I +← (‎ ы ‎) 044B CYRILLIC SMALL LETTER YERU # →ьı→ + +# Ɔ Ↄ Ͻ ꓛ 𐐣 + (‎ Ɔ ‎) 0186 LATIN CAPITAL LETTER OPEN O +← (‎ Ↄ ‎) 2183 ROMAN NUMERAL REVERSED ONE HUNDRED +← (‎ Ͻ ‎) 03FD GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL +← (‎ ꓛ ‎) A4DB LISU LETTER CHA +← (‎ 𐐣 ‎) 10423 DESERET CAPITAL LETTER EM + +# Ǝ ⴺ ꓱ ∃ + (‎ Ǝ ‎) 018E LATIN CAPITAL LETTER REVERSED E +← (‎ ⴺ ‎) 2D3A TIFINAGH LETTER YADDH +← (‎ ꓱ ‎) A4F1 LISU LETTER EU +← (‎ ∃ ‎) 2203 THERE EXISTS + +# Ə Ә + (‎ Ə ‎) 018F LATIN CAPITAL LETTER SCHWA +← (‎ Ә ‎) 04D8 CYRILLIC CAPITAL LETTER SCHWA + +# Ɛ Ԑ Ꮛ 𐐁 𖼭 𝈡 ℇ + (‎ Ɛ ‎) 0190 LATIN CAPITAL LETTER OPEN E +← (‎ Ԑ ‎) 0510 CYRILLIC CAPITAL LETTER REVERSED ZE +← (‎ Ꮛ ‎) 13CB CHEROKEE LETTER QUV +← (‎ 𐐁 ‎) 10401 DESERET CAPITAL LETTER LONG E +← (‎ 𖼭 ‎) 16F2D MIAO LETTER NYHA +← (‎ 𝈡 ‎) 1D221 GREEK INSTRUMENTAL NOTATION SYMBOL-7 +← (‎ ℇ ‎) 2107 EULER CONSTANT + +# ƨ ᴤ ϩ ꙅ + (‎ ƨ ‎) 01A8 LATIN SMALL LETTER TONE TWO +← (‎ ᴤ ‎) 1D24 LATIN LETTER VOICED LARYNGEAL SPIRANT +← (‎ ϩ ‎) 03E9 COPTIC SMALL LETTER HORI +← (‎ ꙅ ‎) A645 CYRILLIC SMALL LETTER REVERSED DZE + +# Ʃ Σ ⵉ ∑ 𝚺 𝛴 𝜮 𝝨 𝞢 ⅀ + (‎ Ʃ ‎) 01A9 LATIN CAPITAL LETTER ESH +← (‎ Σ ‎) 03A3 GREEK CAPITAL LETTER SIGMA +← (‎ ⵉ ‎) 2D49 TIFINAGH LETTER YI +← (‎ ∑ ‎) 2211 N-ARY SUMMATION +← (‎ 𝚺 ‎) 1D6BA MATHEMATICAL BOLD CAPITAL SIGMA # →Σ→ +← (‎ 𝛴 ‎) 1D6F4 MATHEMATICAL ITALIC CAPITAL SIGMA # →Σ→ +← (‎ 𝜮 ‎) 1D72E MATHEMATICAL BOLD ITALIC CAPITAL SIGMA # →Σ→ +← (‎ 𝝨 ‎) 1D768 MATHEMATICAL SANS-SERIF BOLD CAPITAL SIGMA # →Σ→ +← (‎ 𝞢 ‎) 1D7A2 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL SIGMA # →Σ→ +← (‎ ⅀ ‎) 2140 DOUBLE-STRUCK N-ARY SUMMATION # →∑→ + +# Ʊ ᘮ ᘴ ℧ + (‎ Ʊ ‎) 01B1 LATIN CAPITAL LETTER UPSILON +← (‎ ᘮ ‎) 162E CANADIAN SYLLABICS CARRIER LHU # →℧→ +← (‎ ᘴ ‎) 1634 CANADIAN SYLLABICS CARRIER TLHU # →ᘮ→→℧→ +← (‎ ℧ ‎) 2127 INVERTED OHM SIGN + +# ǝ ə ә + (‎ ǝ ‎) 01DD LATIN SMALL LETTER TURNED E +← (‎ ə ‎) 0259 LATIN SMALL LETTER SCHWA +← (‎ ә ‎) 04D9 CYRILLIC SMALL LETTER SCHWA + +# ǝo əo ᴔ + (‎ ǝo ‎) 01DD 006F LATIN SMALL LETTER TURNED E, LATIN SMALL LETTER O +← (‎ əo ‎) 0259 006F LATIN SMALL LETTER SCHWA, LATIN SMALL LETTER O +← (‎ ᴔ ‎) 1D14 LATIN SMALL LETTER TURNED OE # →əo→ + +# ǝo̵ ǝɵ ꭂ + (‎ ǝo̵ ‎) 01DD 006F 0335 LATIN SMALL LETTER TURNED E, LATIN SMALL LETTER O, COMBINING SHORT STROKE OVERLAY +← (‎ ǝɵ ‎) 01DD 0275 LATIN SMALL LETTER TURNED E, LATIN SMALL LETTER BARRED O +← (‎ ꭂ ‎) AB42 LATIN SMALL LETTER TURNED OE WITH HORIZONTAL STROKE # →ǝɵ→ + +# ǝo̸ ǝø ꭁ + (‎ ǝo̸ ‎) 01DD 006F 0338 LATIN SMALL LETTER TURNED E, LATIN SMALL LETTER O, COMBINING LONG SOLIDUS OVERLAY +← (‎ ǝø ‎) 01DD 00F8 LATIN SMALL LETTER TURNED E, LATIN SMALL LETTER O WITH STROKE +← (‎ ꭁ ‎) AB41 LATIN SMALL LETTER TURNED OE WITH STROKE # →ǝø→ + +# ǝ˞ ə˞ ɚ + (‎ ǝ˞ ‎) 01DD 02DE LATIN SMALL LETTER TURNED E, MODIFIER LETTER RHOTIC HOOK +← (‎ ə˞ ‎) 0259 02DE LATIN SMALL LETTER SCHWA, MODIFIER LETTER RHOTIC HOOK +← (‎ ɚ ‎) 025A LATIN SMALL LETTER SCHWA WITH HOOK # →ə˞→ + +# Ƕ Ԋ + (‎ Ƕ ‎) 01F6 LATIN CAPITAL LETTER HWAIR +← (‎ Ԋ ‎) 050A CYRILLIC CAPITAL LETTER KOMI NJE + +# ȝ ʒ ꝫ ӡ ჳ ⳍ + (‎ ȝ ‎) 021D LATIN SMALL LETTER YOGH +← (‎ ʒ ‎) 0292 LATIN SMALL LETTER EZH +← (‎ ꝫ ‎) A76B LATIN SMALL LETTER ET +← (‎ ӡ ‎) 04E1 CYRILLIC SMALL LETTER ABKHASIAN DZE # →ʒ→ +← (‎ ჳ ‎) 10F3 GEORGIAN LETTER WE # →ʒ→ +← (‎ ⳍ ‎) 2CCD COPTIC SMALL LETTER OLD COPTIC HORI + +# ȷ յ 𝚥 + (‎ ȷ ‎) 0237 LATIN SMALL LETTER DOTLESS J +← (‎ յ ‎) 0575 ARMENIAN SMALL LETTER YI +← (‎ 𝚥 ‎) 1D6A5 MATHEMATICAL ITALIC SMALL DOTLESS J + +# ɂ ꭾ + (‎ ɂ ‎) 0242 LATIN SMALL LETTER GLOTTAL STOP +← (‎ ꭾ ‎) AB7E CHEROKEE SMALL LETTER HE + +# Ʌ ٨ ۸ Λ Л ᐱ ⴷ ꓥ ꛎ 𐊍 𖼽 𐒰 𝚲 𝛬 𝜦 𝝠 𝞚 + (‎ Ʌ ‎) 0245 LATIN CAPITAL LETTER TURNED V +← (‎ ٨ ‎) 0668 ARABIC-INDIC DIGIT EIGHT # →Λ→ +← (‎ ۸ ‎) 06F8 EXTENDED ARABIC-INDIC DIGIT EIGHT # →‎٨‎→→Λ→ +← (‎ Λ ‎) 039B GREEK CAPITAL LETTER LAMDA +← (‎ Л ‎) 041B CYRILLIC CAPITAL LETTER EL # →Λ→ +← (‎ ᐱ ‎) 1431 CANADIAN SYLLABICS PI +← (‎ ⴷ ‎) 2D37 TIFINAGH LETTER YAD +← (‎ ꓥ ‎) A4E5 LISU LETTER NGA +← (‎ ꛎ ‎) A6CE BAMUM LETTER MI # →Λ→ +← (‎ 𐊍 ‎) 1028D LYCIAN LETTER L # →Λ→ +← (‎ 𖼽 ‎) 16F3D MIAO LETTER ZZA +← (‎ 𐒰 ‎) 104B0 OSAGE CAPITAL LETTER A +← (‎ 𝚲 ‎) 1D6B2 MATHEMATICAL BOLD CAPITAL LAMDA # →Λ→ +← (‎ 𝛬 ‎) 1D6EC MATHEMATICAL ITALIC CAPITAL LAMDA # →Λ→ +← (‎ 𝜦 ‎) 1D726 MATHEMATICAL BOLD ITALIC CAPITAL LAMDA # →Λ→ +← (‎ 𝝠 ‎) 1D760 MATHEMATICAL SANS-SERIF BOLD CAPITAL LAMDA # →Λ→ +← (‎ 𝞚 ‎) 1D79A MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL LAMDA # →Λ→ + +# Ʌ· ٨· ᐱ· ᐱᐧ ᐽ + (‎ Ʌ· ‎) 0245 00B7 LATIN CAPITAL LETTER TURNED V, MIDDLE DOT +← (‎ ٨· ‎) 0668 00B7 ARABIC-INDIC DIGIT EIGHT, MIDDLE DOT # →ᐱ·→ +← (‎ ᐱ· ‎) 1431 00B7 CANADIAN SYLLABICS PI, MIDDLE DOT +← (‎ ᐱᐧ ‎) 1431 1427 CANADIAN SYLLABICS PI, CANADIAN SYLLABICS FINAL MIDDLE DOT # →ᐱ·→ +← (‎ ᐽ ‎) 143D CANADIAN SYLLABICS WEST-CREE PWI # →ᐱᐧ→→ᐱ·→ + +# Ʌ̦ Л̦ Л̡ Ӆ + (‎ Ʌ̦ ‎) 0245 0326 LATIN CAPITAL LETTER TURNED V, COMBINING COMMA BELOW +← (‎ Л̦ ‎) 041B 0326 CYRILLIC CAPITAL LETTER EL, COMBINING COMMA BELOW # →Л̡→ +← (‎ Л̡ ‎) 041B 0321 CYRILLIC CAPITAL LETTER EL, COMBINING PALATALIZED HOOK BELOW +← (‎ Ӆ ‎) 04C5 CYRILLIC CAPITAL LETTER EL WITH TAIL # →Л̡→ + +# ɋ ᶐ + (‎ ɋ ‎) 024B LATIN SMALL LETTER Q WITH HOOK TAIL +← (‎ ᶐ ‎) 1D90 LATIN SMALL LETTER ALPHA WITH RETROFLEX HOOK + +# ɔ ᴐ ↄ ͻ 𐑋 + (‎ ɔ ‎) 0254 LATIN SMALL LETTER OPEN O +← (‎ ᴐ ‎) 1D10 LATIN LETTER SMALL CAPITAL OPEN O +← (‎ ↄ ‎) 2184 LATIN SMALL LETTER REVERSED C +← (‎ ͻ ‎) 037B GREEK SMALL REVERSED LUNATE SIGMA SYMBOL +← (‎ 𐑋 ‎) 1044B DESERET SMALL LETTER EM + +# ɔe ꭢ + (‎ ɔe ‎) 0254 0065 LATIN SMALL LETTER OPEN O, LATIN SMALL LETTER E +← (‎ ꭢ ‎) AB62 LATIN SMALL LETTER OPEN OE + +# ɔ̸ ꬿ + (‎ ɔ̸ ‎) 0254 0338 LATIN SMALL LETTER OPEN O, COMBINING LONG SOLIDUS OVERLAY +← (‎ ꬿ ‎) AB3F LATIN SMALL LETTER OPEN O WITH STROKE + +# ꞓ ɛ ε є ԑ ⲉ 𐐩 ⋴ 𑣎 ꮛ ϵ 𝛆 𝛜 𝜀 𝜖 𝜺 𝝐 𝝴 𝞊 𝞮 𝟄 + (‎ ɛ ‎) 025B LATIN SMALL LETTER OPEN E +← (‎ ꞓ ‎) A793 LATIN SMALL LETTER C WITH BAR # →є→ +← (‎ ε ‎) 03B5 GREEK SMALL LETTER EPSILON +← (‎ є ‎) 0454 CYRILLIC SMALL LETTER UKRAINIAN IE +← (‎ ԑ ‎) 0511 CYRILLIC SMALL LETTER REVERSED ZE +← (‎ ⲉ ‎) 2C89 COPTIC SMALL LETTER EIE # →є→ +← (‎ 𐐩 ‎) 10429 DESERET SMALL LETTER LONG E +← (‎ ⋴ ‎) 22F4 SMALL ELEMENT OF WITH VERTICAL BAR AT END OF HORIZONTAL STROKE +← (‎ 𑣎 ‎) 118CE WARANG CITI SMALL LETTER YUJ # →ε→ +← (‎ ꮛ ‎) AB9B CHEROKEE SMALL LETTER QUV +← (‎ ϵ ‎) 03F5 GREEK LUNATE EPSILON SYMBOL # →ε→ +← (‎ 𝛆 ‎) 1D6C6 MATHEMATICAL BOLD SMALL EPSILON # →ε→ +← (‎ 𝛜 ‎) 1D6DC MATHEMATICAL BOLD EPSILON SYMBOL # →ε→ +← (‎ 𝜀 ‎) 1D700 MATHEMATICAL ITALIC SMALL EPSILON # →ε→ +← (‎ 𝜖 ‎) 1D716 MATHEMATICAL ITALIC EPSILON SYMBOL # →ε→ +← (‎ 𝜺 ‎) 1D73A MATHEMATICAL BOLD ITALIC SMALL EPSILON # →ε→ +← (‎ 𝝐 ‎) 1D750 MATHEMATICAL BOLD ITALIC EPSILON SYMBOL # →ε→ +← (‎ 𝝴 ‎) 1D774 MATHEMATICAL SANS-SERIF BOLD SMALL EPSILON # →ε→ +← (‎ 𝞊 ‎) 1D78A MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL # →ε→ +← (‎ 𝞮 ‎) 1D7AE MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL EPSILON # →ε→ +← (‎ 𝟄 ‎) 1D7C4 MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL # →ε→ + +# ɜ ᴈ з + (‎ ɜ ‎) 025C LATIN SMALL LETTER REVERSED OPEN E +← (‎ ᴈ ‎) 1D08 LATIN SMALL LETTER TURNED OPEN E +← (‎ з ‎) 0437 CYRILLIC SMALL LETTER ZE + +# ɜ̦ з̦ з̡ ҙ + (‎ ɜ̦ ‎) 025C 0326 LATIN SMALL LETTER REVERSED OPEN E, COMBINING COMMA BELOW +← (‎ з̦ ‎) 0437 0326 CYRILLIC SMALL LETTER ZE, COMBINING COMMA BELOW # →з̡→ +← (‎ з̡ ‎) 0437 0321 CYRILLIC SMALL LETTER ZE, COMBINING PALATALIZED HOOK BELOW +← (‎ ҙ ‎) 0499 CYRILLIC SMALL LETTER ZE WITH DESCENDER # →з̡→ + +# ɞ 𐑂 + (‎ ɞ ‎) 025E LATIN SMALL LETTER CLOSED REVERSED OPEN E +← (‎ 𐑂 ‎) 10442 DESERET SMALL LETTER VEE + +# ɢ ԍ ᏻ ꮐ + (‎ ɢ ‎) 0262 LATIN LETTER SMALL CAPITAL G +← (‎ ԍ ‎) 050D CYRILLIC SMALL LETTER KOMI SJE +← (‎ ᏻ ‎) 13FB CHEROKEE SMALL LETTER YU +← (‎ ꮐ ‎) AB90 CHEROKEE SMALL LETTER NAH + +# ɰ պ ሣ + (‎ ɰ ‎) 0270 LATIN SMALL LETTER TURNED M WITH LONG LEG +← (‎ պ ‎) 057A ARMENIAN SMALL LETTER PEH +← (‎ ሣ ‎) 1223 ETHIOPIC SYLLABLE SZAA # →պ→ + +# ɷ 𐐿 + (‎ ɷ ‎) 0277 LATIN SMALL LETTER CLOSED OMEGA +← (‎ 𐐿 ‎) 1043F DESERET SMALL LETTER KAY + +# ɸ φ ф ⲫ ϕ 𝛗 𝛟 𝜑 𝜙 𝝋 𝝓 𝞅 𝞍 𝞿 𝟇 + (‎ ɸ ‎) 0278 LATIN SMALL LETTER PHI +← (‎ φ ‎) 03C6 GREEK SMALL LETTER PHI +← (‎ ф ‎) 0444 CYRILLIC SMALL LETTER EF +← (‎ ⲫ ‎) 2CAB COPTIC SMALL LETTER FI # →ϕ→ +← (‎ ϕ ‎) 03D5 GREEK PHI SYMBOL +← (‎ 𝛗 ‎) 1D6D7 MATHEMATICAL BOLD SMALL PHI # →φ→ +← (‎ 𝛟 ‎) 1D6DF MATHEMATICAL BOLD PHI SYMBOL # →φ→ +← (‎ 𝜑 ‎) 1D711 MATHEMATICAL ITALIC SMALL PHI # →φ→ +← (‎ 𝜙 ‎) 1D719 MATHEMATICAL ITALIC PHI SYMBOL # →φ→ +← (‎ 𝝋 ‎) 1D74B MATHEMATICAL BOLD ITALIC SMALL PHI # →φ→ +← (‎ 𝝓 ‎) 1D753 MATHEMATICAL BOLD ITALIC PHI SYMBOL # →φ→ +← (‎ 𝞅 ‎) 1D785 MATHEMATICAL SANS-SERIF BOLD SMALL PHI # →φ→ +← (‎ 𝞍 ‎) 1D78D MATHEMATICAL SANS-SERIF BOLD PHI SYMBOL # →φ→ +← (‎ 𝞿 ‎) 1D7BF MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PHI # →φ→ +← (‎ 𝟇 ‎) 1D7C7 MATHEMATICAL SANS-SERIF BOLD ITALIC PHI SYMBOL # →φ→ + +# ɾ̴ ᵳ + (‎ ɾ̴ ‎) 027E 0334 LATIN SMALL LETTER R WITH FISHHOOK, COMBINING TILDE OVERLAY +← (‎ ᵳ ‎) 1D73 LATIN SMALL LETTER R WITH FISHHOOK AND MIDDLE TILDE + +# ɿ ℩ + (‎ ɿ ‎) 027F LATIN SMALL LETTER REVERSED R WITH FISHHOOK +← (‎ ℩ ‎) 2129 TURNED GREEK SMALL LETTER IOTA + +# ʀ ꭱ ꮢ + (‎ ʀ ‎) 0280 LATIN LETTER SMALL CAPITAL R +← (‎ ꭱ ‎) AB71 CHEROKEE SMALL LETTER E +← (‎ ꮢ ‎) ABA2 CHEROKEE SMALL LETTER SV + +# ʃ ∫ ꭍ + (‎ ʃ ‎) 0283 LATIN SMALL LETTER ESH +← (‎ ∫ ‎) 222B INTEGRAL +← (‎ ꭍ ‎) AB4D LATIN SMALL LETTER BASELINE ESH + +# ʃʃ ∫∫ ∬ + (‎ ʃʃ ‎) 0283 0283 LATIN SMALL LETTER ESH, LATIN SMALL LETTER ESH +← (‎ ∫∫ ‎) 222B 222B INTEGRAL, INTEGRAL +← (‎ ∬ ‎) 222C DOUBLE INTEGRAL # →∫∫→ + +# ʃʃʃ ∫∫∫ ∭ + (‎ ʃʃʃ ‎) 0283 0283 0283 LATIN SMALL LETTER ESH, LATIN SMALL LETTER ESH, LATIN SMALL LETTER ESH +← (‎ ∫∫∫ ‎) 222B 222B 222B INTEGRAL, INTEGRAL, INTEGRAL +← (‎ ∭ ‎) 222D TRIPLE INTEGRAL # →∫∫∫→ + +# ʃʃʃʃ ∫∫∫∫ ⨌ + (‎ ʃʃʃʃ ‎) 0283 0283 0283 0283 LATIN SMALL LETTER ESH, LATIN SMALL LETTER ESH, LATIN SMALL LETTER ESH, LATIN SMALL LETTER ESH +← (‎ ∫∫∫∫ ‎) 222B 222B 222B 222B INTEGRAL, INTEGRAL, INTEGRAL, INTEGRAL +← (‎ ⨌ ‎) 2A0C QUADRUPLE INTEGRAL OPERATOR # →∫∫∫∫→ + +# ʊ̵ ᵿ + (‎ ʊ̵ ‎) 028A 0335 LATIN SMALL LETTER UPSILON, COMBINING SHORT STROKE OVERLAY +← (‎ ᵿ ‎) 1D7F LATIN SMALL LETTER UPSILON WITH STROKE + +# ʌ ᴧ 𐓘 + (‎ ʌ ‎) 028C LATIN SMALL LETTER TURNED V +← (‎ ᴧ ‎) 1D27 GREEK LETTER SMALL CAPITAL LAMDA +← (‎ 𐓘 ‎) 104D8 OSAGE SMALL LETTER A + +# ʍ ᴍ м ꮇ + (‎ ʍ ‎) 028D LATIN SMALL LETTER TURNED W +← (‎ ᴍ ‎) 1D0D LATIN LETTER SMALL CAPITAL M # →м→ +← (‎ м ‎) 043C CYRILLIC SMALL LETTER EM +← (‎ ꮇ ‎) AB87 CHEROKEE SMALL LETTER LU # →ᴍ→→м→ + +# ʍ̦ м̦ м̡ ӎ + (‎ ʍ̦ ‎) 028D 0326 LATIN SMALL LETTER TURNED W, COMBINING COMMA BELOW +← (‎ м̦ ‎) 043C 0326 CYRILLIC SMALL LETTER EM, COMBINING COMMA BELOW # →м̡→ +← (‎ м̡ ‎) 043C 0321 CYRILLIC SMALL LETTER EM, COMBINING PALATALIZED HOOK BELOW +← (‎ ӎ ‎) 04CE CYRILLIC SMALL LETTER EM WITH TAIL # →м̡→ + +# ʘ ⵙ Ꙩ ⊙ ☉ ⨀ 𐓃 + (‎ ʘ ‎) 0298 LATIN LETTER BILABIAL CLICK +← (‎ ⵙ ‎) 2D59 TIFINAGH LETTER YAS # →⊙→ +← (‎ Ꙩ ‎) A668 CYRILLIC CAPITAL LETTER MONOCULAR O +← (‎ ⊙ ‎) 2299 CIRCLED DOT OPERATOR +← (‎ ☉ ‎) 2609 SUN # →⊙→ +← (‎ ⨀ ‎) 2A00 N-ARY CIRCLED DOT OPERATOR # →⊙→ +← (‎ 𐓃 ‎) 104C3 OSAGE CAPITAL LETTER OIN # →Ꙩ→ + +# ʙ в ᏼ + (‎ ʙ ‎) 0299 LATIN LETTER SMALL CAPITAL B +← (‎ в ‎) 0432 CYRILLIC SMALL LETTER VE +← (‎ ᏼ ‎) 13FC CHEROKEE SMALL LETTER YV + +# ʚ 𐐪 ꞝ + (‎ ʚ ‎) 029A LATIN SMALL LETTER CLOSED OPEN E +← (‎ 𐐪 ‎) 1042A DESERET SMALL LETTER LONG A +← (‎ ꞝ ‎) A79D LATIN SMALL LETTER VOLAPUK OE + +# ʜ н ꮋ + (‎ ʜ ‎) 029C LATIN LETTER SMALL CAPITAL H +← (‎ н ‎) 043D CYRILLIC SMALL LETTER EN +← (‎ ꮋ ‎) AB8B CHEROKEE SMALL LETTER MI + +# ʜ̦ н̦ н̡ ӈ ӊ + (‎ ʜ̦ ‎) 029C 0326 LATIN LETTER SMALL CAPITAL H, COMBINING COMMA BELOW +← (‎ н̦ ‎) 043D 0326 CYRILLIC SMALL LETTER EN, COMBINING COMMA BELOW # →н̡→ +← (‎ н̡ ‎) 043D 0321 CYRILLIC SMALL LETTER EN, COMBINING PALATALIZED HOOK BELOW +← (‎ ӈ ‎) 04C8 CYRILLIC SMALL LETTER EN WITH HOOK # →н̡→ +← (‎ ӊ ‎) 04CA CYRILLIC SMALL LETTER EN WITH TAIL # →н̡→ + +# ʜ̩ н̩ ң + (‎ ʜ̩ ‎) 029C 0329 LATIN LETTER SMALL CAPITAL H, COMBINING VERTICAL LINE BELOW +← (‎ н̩ ‎) 043D 0329 CYRILLIC SMALL LETTER EN, COMBINING VERTICAL LINE BELOW +← (‎ ң ‎) 04A3 CYRILLIC SMALL LETTER EN WITH DESCENDER # →н̩→ + +# ʟ ⳑ 𐑃 ꮮ + (‎ ʟ ‎) 029F LATIN LETTER SMALL CAPITAL L +← (‎ ⳑ ‎) 2CD1 COPTIC SMALL LETTER L-SHAPED HA +← (‎ 𐑃 ‎) 10443 DESERET SMALL LETTER ETH +← (‎ ꮮ ‎) ABAE CHEROKEE SMALL LETTER TLE + +# ʡ ꛍ + (‎ ʡ ‎) 02A1 LATIN LETTER GLOTTAL STOP WITH STROKE +← (‎ ꛍ ‎) A6CD BAMUM LETTER LU + +# ᣴ ʳ + (‎ ʳ ‎) 02B3 MODIFIER LETTER SMALL R +← (‎ ᣴ ‎) 18F4 CANADIAN SYLLABICS BEAVER DENE R + +# ՙ ʿ ˓ + (‎ ʿ ‎) 02BF MODIFIER LETTER LEFT HALF RING +← (‎ ՙ ‎) 0559 ARMENIAN MODIFIER LETTER LEFT HALF RING +← (‎ ˓ ‎) 02D3 MODIFIER LETTER CENTRED LEFT HALF RING + +# ˁ ˤ + (‎ ˁ ‎) 02C1 MODIFIER LETTER REVERSED GLOTTAL STOP +← (‎ ˤ ‎) 02E4 MODIFIER LETTER SMALL REVERSED GLOTTAL STOP + +# ˇ ꙾ ˘ + (‎ ˇ ‎) 02C7 CARON +← (‎ ꙾ ‎) A67E CYRILLIC KAVYKA # →˘→ +← (‎ ˘ ‎) 02D8 BREVE + +# ˉbi ъi ъı ꙑ + (‎ ˉbi ‎) 02C9 0062 0069 MODIFIER LETTER MACRON, LATIN SMALL LETTER B, LATIN SMALL LETTER I +← (‎ ъi ‎) 044A 0069 CYRILLIC SMALL LETTER HARD SIGN, LATIN SMALL LETTER I # →ъı→ +← (‎ ъı ‎) 044A 0131 CYRILLIC SMALL LETTER HARD SIGN, LATIN SMALL LETTER DOTLESS I +← (‎ ꙑ ‎) A651 CYRILLIC SMALL LETTER YERU WITH BACK YER # →ъı→ + +# ˏ ͵ + (‎ ˏ ‎) 02CF MODIFIER LETTER LOW ACUTE ACCENT +← (‎ ͵ ‎) 0375 GREEK LOWER NUMERAL SIGN + +# ॱ ൎ ˙ + (‎ ˙ ‎) 02D9 DOT ABOVE +← (‎ ॱ ‎) 0971 DEVANAGARI SIGN HIGH SPACING DOT +← (‎ ൎ ‎) 0D4E MALAYALAM LETTER DOT REPH + +# ᣳ ˡ + (‎ ˡ ‎) 02E1 MODIFIER LETTER SMALL L +← (‎ ᣳ ‎) 18F3 CANADIAN SYLLABICS BEAVER DENE L + +# ᣵ ᣛ ˢ + (‎ ˢ ‎) 02E2 MODIFIER LETTER SMALL S +← (‎ ᣵ ‎) 18F5 CANADIAN SYLLABICS CARRIER DENTAL S +← (‎ ᣛ ‎) 18DB CANADIAN SYLLABICS OJIBWAY SH + +# ˪ ˻ ꜖ + (‎ ˪ ‎) 02EA MODIFIER LETTER YIN DEPARTING TONE MARK +← (‎ ˻ ‎) 02FB MODIFIER LETTER BEGIN LOW TONE +← (‎ ꜖ ‎) A716 MODIFIER LETTER EXTRA-LOW LEFT-STEM TONE BAR + +# ˫ ꜔ + (‎ ˫ ‎) 02EB MODIFIER LETTER YANG DEPARTING TONE MARK +← (‎ ꜔ ‎) A714 MODIFIER LETTER MID LEFT-STEM TONE BAR + +# ˳ 。 + (‎ ˳ ‎) 02F3 MODIFIER LETTER LOW RING +← (‎ 。 ‎) 3002 IDEOGRAPHIC FULL STOP + +# ̀ ॓ ̀ + (‎ ̀ ‎) 0300 COMBINING GRAVE ACCENT +← (‎ ॓ ‎) 0953 DEVANAGARI GRAVE ACCENT +← (‎ ̀ ‎) 0340 COMBINING GRAVE TONE MARK + +# ́ َ ֜ ֝ ؘ ݇ ॔ ́ + (‎ ́ ‎) 0301 COMBINING ACUTE ACCENT +← (‎ َ ‎) 064E ARABIC FATHA +← (‎ ֜ ‎) 059C HEBREW ACCENT GERESH +← (‎ ֝ ‎) 059D HEBREW ACCENT GERESH MUQDAM # →֜→ +← (‎ ؘ ‎) 0618 ARABIC SMALL FATHA # →َ→ +← (‎ ݇ ‎) 0747 SYRIAC OBLIQUE LINE ABOVE +← (‎ ॔ ‎) 0954 DEVANAGARI ACUTE ACCENT +← (‎ ́ ‎) 0341 COMBINING ACUTE TONE MARK + +# ̂ ̑ ٛ ߮ ᳐ ꛰ + (‎ ̂ ‎) 0302 COMBINING CIRCUMFLEX ACCENT +← (‎ ̑ ‎) 0311 COMBINING INVERTED BREVE +← (‎ ٛ ‎) 065B ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ߮ ‎) 07EE NKO COMBINING LONG DESCENDING TONE +← (‎ ᳐ ‎) 1CD0 VEDIC TONE KARSHANA +← (‎ ꛰ ‎) A6F0 BAMUM COMBINING MARK KOQNDON + +# ̃ ͂ ٓ + (‎ ̃ ‎) 0303 COMBINING TILDE +← (‎ ͂ ‎) 0342 COMBINING GREEK PERISPOMENI +← (‎ ٓ ‎) 0653 ARABIC MADDAH ABOVE + +# ̄ ̅ ٙ ߫ ᳒ ꛱ + (‎ ̄ ‎) 0304 COMBINING MACRON +← (‎ ̅ ‎) 0305 COMBINING OVERLINE +← (‎ ٙ ‎) 0659 ARABIC ZWARAKAY +← (‎ ߫ ‎) 07EB NKO COMBINING SHORT HIGH TONE +← (‎ ᳒ ‎) 1CD2 VEDIC TONE PRENKHA +← (‎ ꛱ ‎) A6F1 BAMUM COMBINING MARK TUKWENTIS + +# ̆ ̌ ͮ ٘ ٚ ꙼ + (‎ ̆ ‎) 0306 COMBINING BREVE +← (‎ ̌ ‎) 030C COMBINING CARON +← (‎ ͮ ‎) 036E COMBINING LATIN SMALL LETTER V # →̌→ +← (‎ ٘ ‎) 0658 ARABIC MARK NOON GHUNNA +← (‎ ٚ ‎) 065A ARABIC VOWEL SIGN SMALL V ABOVE # →̌→ +← (‎ ꙼ ‎) A67C COMBINING CYRILLIC KAVYKA + +# ̆̇ ̐ ँ ঁ ઁ ଁ ۨ ఀ ಁ ഁ 𑒿 + (‎ ̆̇ ‎) 0306 0307 COMBINING BREVE, COMBINING DOT ABOVE +← (‎ ̐ ‎) 0310 COMBINING CANDRABINDU +← (‎ ँ ‎) 0901 DEVANAGARI SIGN CANDRABINDU # →̐→ +← (‎ ঁ ‎) 0981 BENGALI SIGN CANDRABINDU # →̐→ +← (‎ ઁ ‎) 0A81 GUJARATI SIGN CANDRABINDU # →̐→ +← (‎ ଁ ‎) 0B01 ORIYA SIGN CANDRABINDU # →̐→ +← (‎ ۨ ‎) 06E8 ARABIC SMALL HIGH NOON # →̐→ +← (‎ ఀ ‎) 0C00 TELUGU SIGN COMBINING CANDRABINDU ABOVE # →ँ→→̐→ +← (‎ ಁ ‎) 0C81 KANNADA SIGN CANDRABINDU # →ँ→→̐→ +← (‎ ഁ ‎) 0D01 MALAYALAM SIGN CANDRABINDU # →ँ→→̐→ +← (‎ 𑒿 ‎) 114BF TIRHUTA SIGN CANDRABINDU # →ঁ→→̐→ + +# ̇ ं ਂ ં ் ͘ ֹ ֺ ׁ ׂ ׄ ۬ ݀ ݁ ߭ ࣪ + (‎ ̇ ‎) 0307 COMBINING DOT ABOVE +← (‎ ं ‎) 0902 DEVANAGARI SIGN ANUSVARA +← (‎ ਂ ‎) 0A02 GURMUKHI SIGN BINDI +← (‎ ં ‎) 0A82 GUJARATI SIGN ANUSVARA +← (‎ ் ‎) 0BCD TAMIL SIGN VIRAMA +← (‎ ͘ ‎) 0358 COMBINING DOT ABOVE RIGHT +← (‎ ֹ ‎) 05B9 HEBREW POINT HOLAM +← (‎ ֺ ‎) 05BA HEBREW POINT HOLAM HASER FOR VAV # →ׁ→ +← (‎ ׁ ‎) 05C1 HEBREW POINT SHIN DOT +← (‎ ׂ ‎) 05C2 HEBREW POINT SIN DOT +← (‎ ׄ ‎) 05C4 HEBREW MARK UPPER DOT +← (‎ ۬ ‎) 06EC ARABIC ROUNDED HIGH STOP WITH FILLED CENTRE +← (‎ ݀ ‎) 0740 SYRIAC FEMININE DOT # →݁→ +← (‎ ݁ ‎) 0741 SYRIAC QUSHSHAYA +← (‎ ߭ ‎) 07ED NKO COMBINING SHORT RISING TONE +← (‎ ࣪ ‎) 08EA ARABIC TONE ONE DOT ABOVE + +# ̈ ߳ ࣫ + (‎ ̈ ‎) 0308 COMBINING DIAERESIS +← (‎ ߳ ‎) 07F3 NKO COMBINING DOUBLE DOT ABOVE +← (‎ ࣫ ‎) 08EB ARABIC TONE TWO DOTS ABOVE + +# ̉ 〬 + (‎ ̉ ‎) 0309 COMBINING HOOK ABOVE +← (‎ 〬 ‎) 302C IDEOGRAPHIC DEPARTING TONE MARK + +# ̊ ْ ஂ ํ ໍ ံ ំ ゚ ͦ ֯ ۟ ៓ ⷪ 𑌀 + (‎ ̊ ‎) 030A COMBINING RING ABOVE +← (‎ ْ ‎) 0652 ARABIC SUKUN +← (‎ ஂ ‎) 0B82 TAMIL SIGN ANUSVARA +← (‎ ํ ‎) 0E4D THAI CHARACTER NIKHAHIT +← (‎ ໍ ‎) 0ECD LAO NIGGAHITA +← (‎ ံ ‎) 1036 MYANMAR SIGN ANUSVARA +← (‎ ំ ‎) 17C6 KHMER SIGN NIKAHIT +← (‎ ゚ ‎) 309A COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK +← (‎ ͦ ‎) 0366 COMBINING LATIN SMALL LETTER O +← (‎ ֯ ‎) 05AF HEBREW MARK MASORA CIRCLE +← (‎ ۟ ‎) 06DF ARABIC SMALL HIGH ROUNDED ZERO # →ْ→ +← (‎ ៓ ‎) 17D3 KHMER SIGN BATHAMASAT +← (‎ ⷪ ‎) 2DEA COMBINING CYRILLIC LETTER O # →ͦ→ +← (‎ 𑌀 ‎) 11300 GRANTHA SIGN COMBINING ANUSVARA ABOVE # →ஂ→ + +# ̊า ํา ำ + (‎ ̊า ‎) 030A 0E32 COMBINING RING ABOVE, THAI CHARACTER SARA AA +← (‎ ํา ‎) 0E4D 0E32 THAI CHARACTER NIKHAHIT, THAI CHARACTER SARA AA +← (‎ ำ ‎) 0E33 THAI CHARACTER SARA AM # →ํา→ + +# ̊າ ໍາ ຳ + (‎ ̊າ ‎) 030A 0EB2 COMBINING RING ABOVE, LAO VOWEL SIGN AA +← (‎ ໍາ ‎) 0ECD 0EB2 LAO NIGGAHITA, LAO VOWEL SIGN AA +← (‎ ຳ ‎) 0EB3 LAO VOWEL SIGN AM # →ໍາ→ + +# ̋ ً ࣰ + (‎ ̋ ‎) 030B COMBINING DOUBLE ACUTE ACCENT +← (‎ ً ‎) 064B ARABIC FATHATAN +← (‎ ࣰ ‎) 08F0 ARABIC OPEN FATHATAN # →ً→ + +# ٰ ̍ + (‎ ̍ ‎) 030D COMBINING VERTICAL LINE ABOVE +← (‎ ٰ ‎) 0670 ARABIC LETTER SUPERSCRIPT ALEF + +# ̎ ᳚ + (‎ ̎ ‎) 030E COMBINING DOUBLE VERTICAL LINE ABOVE +← (‎ ᳚ ‎) 1CDA VEDIC TONE DOUBLE SVARITA + +# ̒ ٗ + (‎ ̒ ‎) 0312 COMBINING TURNED COMMA ABOVE +← (‎ ٗ ‎) 0657 ARABIC INVERTED DAMMA + +# ̓ ُ ̕ ؙ ࣳ ̓ + (‎ ̓ ‎) 0313 COMBINING COMMA ABOVE +← (‎ ُ ‎) 064F ARABIC DAMMA +← (‎ ̕ ‎) 0315 COMBINING COMMA ABOVE RIGHT +← (‎ ؙ ‎) 0619 ARABIC SMALL DAMMA # →ُ→ +← (‎ ࣳ ‎) 08F3 ARABIC SMALL HIGH WAW # →ُ→ +← (‎ ̓ ‎) 0343 COMBINING GREEK KORONIS + +# ̔ ٝ + (‎ ̔ ‎) 0314 COMBINING REVERSED COMMA ABOVE +← (‎ ٝ ‎) 065D ARABIC REVERSED DAMMA + +# ̖ ᳭ + (‎ ̖ ‎) 0316 COMBINING GRAVE ACCENT BELOW +← (‎ ᳭ ‎) 1CED VEDIC SIGN TIRYAK + +# ِ ̗ ؚ + (‎ ̗ ‎) 0317 COMBINING ACUTE ACCENT BELOW +← (‎ ِ ‎) 0650 ARABIC KASRA +← (‎ ؚ ‎) 061A ARABIC SMALL KASRA # →ِ→ + +# ̱ ̠ ॒ + (‎ ̠ ‎) 0320 COMBINING MINUS SIGN BELOW +← (‎ ̱ ‎) 0331 COMBINING MACRON BELOW +← (‎ ॒ ‎) 0952 DEVANAGARI STRESS SIGN ANUDATTA + +# ̦ ̧ ̹ ̡ + (‎ ̡ ‎) 0321 COMBINING PALATALIZED HOOK BELOW +← (‎ ̦ ‎) 0326 COMBINING COMMA BELOW +← (‎ ̧ ‎) 0327 COMBINING CEDILLA +← (‎ ̹ ‎) 0339 COMBINING RIGHT HALF RING BELOW # →̧→ + +# ̨ ͅ ̢ ᪷ + (‎ ̢ ‎) 0322 COMBINING RETROFLEX HOOK BELOW +← (‎ ̨ ‎) 0328 COMBINING OGONEK +← (‎ ͅ ‎) 0345 COMBINING GREEK YPOGEGRAMMENI # →̨→ +← (‎ ᪷ ‎) 1AB7 COMBINING OPEN MARK BELOW # →̨→ + +# ̣ ִ ़ ় ਼ ઼ ଼ ׅ ٜ ࣭ ᳝ 𐨺 𑓃 𑇊 + (‎ ̣ ‎) 0323 COMBINING DOT BELOW +← (‎ ִ ‎) 05B4 HEBREW POINT HIRIQ +← (‎ ़ ‎) 093C DEVANAGARI SIGN NUKTA +← (‎ ় ‎) 09BC BENGALI SIGN NUKTA +← (‎ ਼ ‎) 0A3C GURMUKHI SIGN NUKTA +← (‎ ઼ ‎) 0ABC GUJARATI SIGN NUKTA +← (‎ ଼ ‎) 0B3C ORIYA SIGN NUKTA +← (‎ ׅ ‎) 05C5 HEBREW MARK LOWER DOT +← (‎ ٜ ‎) 065C ARABIC VOWEL SIGN DOT BELOW +← (‎ ࣭ ‎) 08ED ARABIC TONE ONE DOT BELOW +← (‎ ᳝ ‎) 1CDD VEDIC TONE DOT BELOW +← (‎ 𐨺 ‎) 10A3A KHAROSHTHI SIGN DOT BELOW +← (‎ 𑓃 ‎) 114C3 TIRHUTA SIGN NUKTA # →়→ +← (‎ 𑇊 ‎) 111CA SHARADA SIGN NUKTA # →़→ + +# ̤ ࣮ ᳞ + (‎ ̤ ‎) 0324 COMBINING DIAERESIS BELOW +← (‎ ࣮ ‎) 08EE ARABIC TONE TWO DOTS BELOW +← (‎ ᳞ ‎) 1CDE VEDIC TONE TWO DOTS BELOW + +# ̥ ༷ 〭 + (‎ ̥ ‎) 0325 COMBINING RING BELOW +← (‎ ༷ ‎) 0F37 TIBETAN MARK NGAS BZUNG SGOR RTAGS +← (‎ 〭 ‎) 302D IDEOGRAPHIC ENTERING TONE MARK + +# ̩ ٖ ᳜ + (‎ ̩ ‎) 0329 COMBINING VERTICAL LINE BELOW +← (‎ ٖ ‎) 0656 ARABIC SUBSCRIPT ALEF +← (‎ ᳜ ‎) 1CDC VEDIC TONE KATHAKA ANUDATTA + +# ̫ ᳕ + (‎ ̫ ‎) 032B COMBINING INVERTED DOUBLE ARCH BELOW +← (‎ ᳕ ‎) 1CD5 VEDIC TONE YAJURVEDIC AGGRAVATED INDEPENDENT SVARITA + +# ̭ ᳙ + (‎ ̭ ‎) 032D COMBINING CIRCUMFLEX ACCENT BELOW +← (‎ ᳙ ‎) 1CD9 VEDIC TONE YAJURVEDIC KATHAKA INDEPENDENT SVARITA SCHROEDER + +# ̮ ᳘ + (‎ ̮ ‎) 032E COMBINING BREVE BELOW +← (‎ ᳘ ‎) 1CD8 VEDIC TONE CANDRA BELOW + +# ̳ ͇ + (‎ ̳ ‎) 0333 COMBINING DOUBLE LOW LINE +← (‎ ͇ ‎) 0347 COMBINING EQUALS SIGN BELOW + +# ̵ ̶ + (‎ ̵ ‎) 0335 COMBINING SHORT STROKE OVERLAY +← (‎ ̶ ‎) 0336 COMBINING LONG STROKE OVERLAY + +# ̸ ̷ + (‎ ̷ ‎) 0337 COMBINING SHORT SOLIDUS OVERLAY +← (‎ ̸ ‎) 0338 COMBINING LONG SOLIDUS OVERLAY + +# ͐ ͗ ࣸ ࣿ + (‎ ͐ ‎) 0350 COMBINING RIGHT ARROWHEAD ABOVE +← (‎ ͗ ‎) 0357 COMBINING RIGHT HALF RING ABOVE # →ࣿ→→ࣸ→ +← (‎ ࣸ ‎) 08F8 ARABIC RIGHT ARROWHEAD ABOVE +← (‎ ࣿ ‎) 08FF ARABIC MARK SIDEWAYS NOON GHUNNA # →ࣸ→ + +# ͒ ऀ + (‎ ͒ ‎) 0352 COMBINING FERMATA +← (‎ ऀ ‎) 0900 DEVANAGARI SIGN INVERTED CANDRABINDU + +# ͔ ࣹ + (‎ ͔ ‎) 0354 COMBINING LEFT ARROWHEAD BELOW +← (‎ ࣹ ‎) 08F9 ARABIC LEFT ARROWHEAD BELOW + +# ͕ ࣺ + (‎ ͕ ‎) 0355 COMBINING RIGHT ARROWHEAD BELOW +← (‎ ࣺ ‎) 08FA ARABIC RIGHT ARROWHEAD BELOW + +# ͣ ⷶ + (‎ ͣ ‎) 0363 COMBINING LATIN SMALL LETTER A +← (‎ ⷶ ‎) 2DF6 COMBINING CYRILLIC LETTER A + +# ͤ ⷷ + (‎ ͤ ‎) 0364 COMBINING LATIN SMALL LETTER E +← (‎ ⷷ ‎) 2DF7 COMBINING CYRILLIC LETTER IE + +# ͨ ⷭ + (‎ ͨ ‎) 0368 COMBINING LATIN SMALL LETTER C +← (‎ ⷭ ‎) 2DED COMBINING CYRILLIC LETTER ES + +# ͯ ⷯ + (‎ ͯ ‎) 036F COMBINING LATIN SMALL LETTER X +← (‎ ⷯ ‎) 2DEF COMBINING CYRILLIC LETTER HA + +# Ⱶ Ͱ Ꭸ Ꮀ ꚱ + (‎ Ͱ ‎) 0370 GREEK CAPITAL LETTER HETA +← (‎ Ⱶ ‎) 2C75 LATIN CAPITAL LETTER HALF H # →Ꮀ→ +← (‎ Ꭸ ‎) 13A8 CHEROKEE LETTER GE +← (‎ Ꮀ ‎) 13B0 CHEROKEE LETTER HO +← (‎ ꚱ ‎) A6B1 BAMUM LETTER NDAA + +# И Ͷ ꚡ 𐐥 𝈋 + (‎ Ͷ ‎) 0376 GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA +← (‎ И ‎) 0418 CYRILLIC CAPITAL LETTER I +← (‎ ꚡ ‎) A6A1 BAMUM LETTER KA +← (‎ 𐐥 ‎) 10425 DESERET CAPITAL LETTER ENG # →И→ +← (‎ 𝈋 ‎) 1D20B GREEK VOCAL NOTATION SYMBOL-12 + +# ᴎ и ͷ 𐑍 + (‎ ͷ ‎) 0377 GREEK SMALL LETTER PAMPHYLIAN DIGAMMA +← (‎ ᴎ ‎) 1D0E LATIN LETTER SMALL CAPITAL REVERSED N # →и→ +← (‎ и ‎) 0438 CYRILLIC SMALL LETTER I +← (‎ 𐑍 ‎) 1044D DESERET SMALL LETTER ENG # →и→ + +# ꜿ ͽ + (‎ ͽ ‎) 037D GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL +← (‎ ꜿ ‎) A73F LATIN SMALL LETTER REVERSED C WITH DOT + +# Γ Г Ꮁ ᒥ Ⲅ 𖼇 ℾ 𝚪 𝛤 𝜞 𝝘 𝞒 + (‎ Γ ‎) 0393 GREEK CAPITAL LETTER GAMMA +← (‎ Г ‎) 0413 CYRILLIC CAPITAL LETTER GHE +← (‎ Ꮁ ‎) 13B1 CHEROKEE LETTER HU +← (‎ ᒥ ‎) 14A5 CANADIAN SYLLABICS MI +← (‎ Ⲅ ‎) 2C84 COPTIC CAPITAL LETTER GAMMA +← (‎ 𖼇 ‎) 16F07 MIAO LETTER FA +← (‎ ℾ ‎) 213E DOUBLE-STRUCK CAPITAL GAMMA +← (‎ 𝚪 ‎) 1D6AA MATHEMATICAL BOLD CAPITAL GAMMA +← (‎ 𝛤 ‎) 1D6E4 MATHEMATICAL ITALIC CAPITAL GAMMA +← (‎ 𝜞 ‎) 1D71E MATHEMATICAL BOLD ITALIC CAPITAL GAMMA +← (‎ 𝝘 ‎) 1D758 MATHEMATICAL SANS-SERIF BOLD CAPITAL GAMMA +← (‎ 𝞒 ‎) 1D792 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL GAMMA + +# Γ' Гˈ Г' Ґ + (‎ Γ' ‎) 0393 0027 GREEK CAPITAL LETTER GAMMA, APOSTROPHE +← (‎ Гˈ ‎) 0413 02C8 CYRILLIC CAPITAL LETTER GHE, MODIFIER LETTER VERTICAL LINE +← (‎ Г' ‎) 0413 0027 CYRILLIC CAPITAL LETTER GHE, APOSTROPHE # →Гˈ→ +← (‎ Ґ ‎) 0490 CYRILLIC CAPITAL LETTER GHE WITH UPTURN # →Гˈ→ + +# Γ· Ꮁ· ᒥ· ᒥᐧ ᒯ + (‎ Γ· ‎) 0393 00B7 GREEK CAPITAL LETTER GAMMA, MIDDLE DOT +← (‎ Ꮁ· ‎) 13B1 00B7 CHEROKEE LETTER HU, MIDDLE DOT # →ᒥ·→ +← (‎ ᒥ· ‎) 14A5 00B7 CANADIAN SYLLABICS MI, MIDDLE DOT +← (‎ ᒥᐧ ‎) 14A5 1427 CANADIAN SYLLABICS MI, CANADIAN SYLLABICS FINAL MIDDLE DOT # →ᒥ·→ +← (‎ ᒯ ‎) 14AF CANADIAN SYLLABICS WEST-CREE MWI # →ᒥᐧ→→ᒥ·→ + +# Γ̵ Г̵ Ғ + (‎ Γ̵ ‎) 0393 0335 GREEK CAPITAL LETTER GAMMA, COMBINING SHORT STROKE OVERLAY +← (‎ Г̵ ‎) 0413 0335 CYRILLIC CAPITAL LETTER GHE, COMBINING SHORT STROKE OVERLAY +← (‎ Ғ ‎) 0492 CYRILLIC CAPITAL LETTER GHE WITH STROKE # →Г̵→ + +# Δ ᐃ Ⲇ ⵠ 𐊅 𐊣 𖼚 ∆ △ 🜂 𝚫 𝛥 𝜟 𝝙 𝞓 + (‎ Δ ‎) 0394 GREEK CAPITAL LETTER DELTA +← (‎ ᐃ ‎) 1403 CANADIAN SYLLABICS I +← (‎ Ⲇ ‎) 2C86 COPTIC CAPITAL LETTER DALDA +← (‎ ⵠ ‎) 2D60 TIFINAGH LETTER YAV +← (‎ 𐊅 ‎) 10285 LYCIAN LETTER D +← (‎ 𐊣 ‎) 102A3 CARIAN LETTER L +← (‎ 𖼚 ‎) 16F1A MIAO LETTER TLHA +← (‎ ∆ ‎) 2206 INCREMENT +← (‎ △ ‎) 25B3 WHITE UP-POINTING TRIANGLE +← (‎ 🜂 ‎) 1F702 ALCHEMICAL SYMBOL FOR FIRE # →△→ +← (‎ 𝚫 ‎) 1D6AB MATHEMATICAL BOLD CAPITAL DELTA +← (‎ 𝛥 ‎) 1D6E5 MATHEMATICAL ITALIC CAPITAL DELTA +← (‎ 𝜟 ‎) 1D71F MATHEMATICAL BOLD ITALIC CAPITAL DELTA +← (‎ 𝝙 ‎) 1D759 MATHEMATICAL SANS-SERIF BOLD CAPITAL DELTA +← (‎ 𝞓 ‎) 1D793 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL DELTA + +# Δ· ᐃ· ᐃᐧ ᐏ + (‎ Δ· ‎) 0394 00B7 GREEK CAPITAL LETTER DELTA, MIDDLE DOT +← (‎ ᐃ· ‎) 1403 00B7 CANADIAN SYLLABICS I, MIDDLE DOT # →ᐃᐧ→ +← (‎ ᐃᐧ ‎) 1403 1427 CANADIAN SYLLABICS I, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐏ ‎) 140F CANADIAN SYLLABICS WEST-CREE WI # →ᐃᐧ→ + +# Δ̲ ⍙ + (‎ Δ̲ ‎) 0394 0332 GREEK CAPITAL LETTER DELTA, COMBINING LOW LINE +← (‎ ⍙ ‎) 2359 APL FUNCTIONAL SYMBOL DELTA UNDERBAR + +# Δᐠ ᐃᐠ ᐬ + (‎ Δᐠ ‎) 0394 1420 GREEK CAPITAL LETTER DELTA, CANADIAN SYLLABICS FINAL GRAVE +← (‎ ᐃᐠ ‎) 1403 1420 CANADIAN SYLLABICS I, CANADIAN SYLLABICS FINAL GRAVE +← (‎ ᐬ ‎) 142C CANADIAN SYLLABICS IN # →ᐃᐠ→ + +# Ξ 𝚵 𝛯 𝜩 𝝣 𝞝 + (‎ Ξ ‎) 039E GREEK CAPITAL LETTER XI +← (‎ 𝚵 ‎) 1D6B5 MATHEMATICAL BOLD CAPITAL XI +← (‎ 𝛯 ‎) 1D6EF MATHEMATICAL ITALIC CAPITAL XI +← (‎ 𝜩 ‎) 1D729 MATHEMATICAL BOLD ITALIC CAPITAL XI +← (‎ 𝝣 ‎) 1D763 MATHEMATICAL SANS-SERIF BOLD CAPITAL XI +← (‎ 𝞝 ‎) 1D79D MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL XI + +# Π П Ⲡ ꛛ ∏ ℿ 𝚷 𝛱 𝜫 𝝥 𝞟 + (‎ Π ‎) 03A0 GREEK CAPITAL LETTER PI +← (‎ П ‎) 041F CYRILLIC CAPITAL LETTER PE +← (‎ Ⲡ ‎) 2CA0 COPTIC CAPITAL LETTER PI +← (‎ ꛛ ‎) A6DB BAMUM LETTER NA +← (‎ ∏ ‎) 220F N-ARY PRODUCT +← (‎ ℿ ‎) 213F DOUBLE-STRUCK CAPITAL PI +← (‎ 𝚷 ‎) 1D6B7 MATHEMATICAL BOLD CAPITAL PI +← (‎ 𝛱 ‎) 1D6F1 MATHEMATICAL ITALIC CAPITAL PI +← (‎ 𝜫 ‎) 1D72B MATHEMATICAL BOLD ITALIC CAPITAL PI +← (‎ 𝝥 ‎) 1D765 MATHEMATICAL SANS-SERIF BOLD CAPITAL PI +← (‎ 𝞟 ‎) 1D79F MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PI + +# Φ ቀ Ф Փ ᛰ Ⲫ 𐊳 𝚽 𝛷 𝜱 𝝫 𝞥 + (‎ Φ ‎) 03A6 GREEK CAPITAL LETTER PHI +← (‎ ቀ ‎) 1240 ETHIOPIC SYLLABLE QA # →Փ→ +← (‎ Ф ‎) 0424 CYRILLIC CAPITAL LETTER EF +← (‎ Փ ‎) 0553 ARMENIAN CAPITAL LETTER PIWR +← (‎ ᛰ ‎) 16F0 RUNIC BELGTHOR SYMBOL +← (‎ Ⲫ ‎) 2CAA COPTIC CAPITAL LETTER FI +← (‎ 𐊳 ‎) 102B3 CARIAN LETTER NN +← (‎ 𝚽 ‎) 1D6BD MATHEMATICAL BOLD CAPITAL PHI +← (‎ 𝛷 ‎) 1D6F7 MATHEMATICAL ITALIC CAPITAL PHI +← (‎ 𝜱 ‎) 1D731 MATHEMATICAL BOLD ITALIC CAPITAL PHI +← (‎ 𝝫 ‎) 1D76B MATHEMATICAL SANS-SERIF BOLD CAPITAL PHI +← (‎ 𝞥 ‎) 1D7A5 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PHI + +# Ψ Ѱ ᛘ Ⲯ 𐊵 𐓑 𝚿 𝛹 𝜳 𝝭 𝞧 + (‎ Ψ ‎) 03A8 GREEK CAPITAL LETTER PSI +← (‎ Ѱ ‎) 0470 CYRILLIC CAPITAL LETTER PSI +← (‎ ᛘ ‎) 16D8 RUNIC LETTER LONG-BRANCH-MADR M +← (‎ Ⲯ ‎) 2CAE COPTIC CAPITAL LETTER PSI +← (‎ 𐊵 ‎) 102B5 CARIAN LETTER N +← (‎ 𐓑 ‎) 104D1 OSAGE CAPITAL LETTER GHA +← (‎ 𝚿 ‎) 1D6BF MATHEMATICAL BOLD CAPITAL PSI +← (‎ 𝛹 ‎) 1D6F9 MATHEMATICAL ITALIC CAPITAL PSI +← (‎ 𝜳 ‎) 1D733 MATHEMATICAL BOLD ITALIC CAPITAL PSI +← (‎ 𝝭 ‎) 1D76D MATHEMATICAL SANS-SERIF BOLD CAPITAL PSI +← (‎ 𝞧 ‎) 1D7A7 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PSI + +# Ω ᘯ ᘵ 𐊶 Ω 𝛀 𝛺 𝜴 𝝮 𝞨 + (‎ Ω ‎) 03A9 GREEK CAPITAL LETTER OMEGA +← (‎ ᘯ ‎) 162F CANADIAN SYLLABICS CARRIER LHO +← (‎ ᘵ ‎) 1635 CANADIAN SYLLABICS CARRIER TLHO # →ᘯ→ +← (‎ 𐊶 ‎) 102B6 CARIAN LETTER TT2 +← (‎ Ω ‎) 2126 OHM SIGN +← (‎ 𝛀 ‎) 1D6C0 MATHEMATICAL BOLD CAPITAL OMEGA +← (‎ 𝛺 ‎) 1D6FA MATHEMATICAL ITALIC CAPITAL OMEGA +← (‎ 𝜴 ‎) 1D734 MATHEMATICAL BOLD ITALIC CAPITAL OMEGA +← (‎ 𝝮 ‎) 1D76E MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA +← (‎ 𝞨 ‎) 1D7A8 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA + +# ẟ δ ծ ᕷ ⸹ 𝛅 𝛿 𝜹 𝝳 𝞭 + (‎ δ ‎) 03B4 GREEK SMALL LETTER DELTA +← (‎ ẟ ‎) 1E9F LATIN SMALL LETTER DELTA +← (‎ ծ ‎) 056E ARMENIAN SMALL LETTER CA +← (‎ ᕷ ‎) 1577 CANADIAN SYLLABICS NUNAVIK HO +← (‎ ⸹ ‎) 2E39 TOP HALF SECTION SIGN +← (‎ 𝛅 ‎) 1D6C5 MATHEMATICAL BOLD SMALL DELTA +← (‎ 𝛿 ‎) 1D6FF MATHEMATICAL ITALIC SMALL DELTA +← (‎ 𝜹 ‎) 1D739 MATHEMATICAL BOLD ITALIC SMALL DELTA +← (‎ 𝝳 ‎) 1D773 MATHEMATICAL SANS-SERIF BOLD SMALL DELTA +← (‎ 𝞭 ‎) 1D7AD MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL DELTA + +# ꞓ̲ ε̲ ⍷ + (‎ ε̲ ‎) 03B5 0332 GREEK SMALL LETTER EPSILON, COMBINING LOW LINE +← (‎ ꞓ̲ ‎) A793 0332 LATIN SMALL LETTER C WITH BAR, COMBINING LOW LINE +← (‎ ⍷ ‎) 2377 APL FUNCTIONAL SYMBOL EPSILON UNDERBAR + +# ζ 𝛇 𝜁 𝜻 𝝵 𝞯 + (‎ ζ ‎) 03B6 GREEK SMALL LETTER ZETA +← (‎ 𝛇 ‎) 1D6C7 MATHEMATICAL BOLD SMALL ZETA +← (‎ 𝜁 ‎) 1D701 MATHEMATICAL ITALIC SMALL ZETA +← (‎ 𝜻 ‎) 1D73B MATHEMATICAL BOLD ITALIC SMALL ZETA +← (‎ 𝝵 ‎) 1D775 MATHEMATICAL SANS-SERIF BOLD SMALL ZETA +← (‎ 𝞯 ‎) 1D7AF MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ZETA + +# λ Ⲗ 𐓛 𝛌 𝜆 𝝀 𝝺 𝞴 + (‎ λ ‎) 03BB GREEK SMALL LETTER LAMDA +← (‎ Ⲗ ‎) 2C96 COPTIC CAPITAL LETTER LAULA +← (‎ 𐓛 ‎) 104DB OSAGE SMALL LETTER AH +← (‎ 𝛌 ‎) 1D6CC MATHEMATICAL BOLD SMALL LAMDA +← (‎ 𝜆 ‎) 1D706 MATHEMATICAL ITALIC SMALL LAMDA +← (‎ 𝝀 ‎) 1D740 MATHEMATICAL BOLD ITALIC SMALL LAMDA +← (‎ 𝝺 ‎) 1D77A MATHEMATICAL SANS-SERIF BOLD SMALL LAMDA +← (‎ 𝞴 ‎) 1D7B4 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL LAMDA + +# ξ 𝛏 𝜉 𝝃 𝝽 𝞷 + (‎ ξ ‎) 03BE GREEK SMALL LETTER XI +← (‎ 𝛏 ‎) 1D6CF MATHEMATICAL BOLD SMALL XI +← (‎ 𝜉 ‎) 1D709 MATHEMATICAL ITALIC SMALL XI +← (‎ 𝝃 ‎) 1D743 MATHEMATICAL BOLD ITALIC SMALL XI +← (‎ 𝝽 ‎) 1D77D MATHEMATICAL SANS-SERIF BOLD SMALL XI +← (‎ 𝞷 ‎) 1D7B7 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL XI + +# π п ᴨ ℼ ϖ 𝛑 𝛡 𝜋 𝜛 𝝅 𝝕 𝝿 𝞏 𝞹 𝟉 + (‎ π ‎) 03C0 GREEK SMALL LETTER PI +← (‎ п ‎) 043F CYRILLIC SMALL LETTER PE +← (‎ ᴨ ‎) 1D28 GREEK LETTER SMALL CAPITAL PI # →п→ +← (‎ ℼ ‎) 213C DOUBLE-STRUCK SMALL PI +← (‎ ϖ ‎) 03D6 GREEK PI SYMBOL +← (‎ 𝛑 ‎) 1D6D1 MATHEMATICAL BOLD SMALL PI +← (‎ 𝛡 ‎) 1D6E1 MATHEMATICAL BOLD PI SYMBOL +← (‎ 𝜋 ‎) 1D70B MATHEMATICAL ITALIC SMALL PI +← (‎ 𝜛 ‎) 1D71B MATHEMATICAL ITALIC PI SYMBOL +← (‎ 𝝅 ‎) 1D745 MATHEMATICAL BOLD ITALIC SMALL PI +← (‎ 𝝕 ‎) 1D755 MATHEMATICAL BOLD ITALIC PI SYMBOL +← (‎ 𝝿 ‎) 1D77F MATHEMATICAL SANS-SERIF BOLD SMALL PI +← (‎ 𝞏 ‎) 1D78F MATHEMATICAL SANS-SERIF BOLD PI SYMBOL +← (‎ 𝞹 ‎) 1D7B9 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PI +← (‎ 𝟉 ‎) 1D7C9 MATHEMATICAL SANS-SERIF BOLD ITALIC PI SYMBOL + +# ς ϛ 𝛓 𝜍 𝝇 𝞁 𝞻 + (‎ ς ‎) 03C2 GREEK SMALL LETTER FINAL SIGMA +← (‎ ϛ ‎) 03DB GREEK SMALL LETTER STIGMA +← (‎ 𝛓 ‎) 1D6D3 MATHEMATICAL BOLD SMALL FINAL SIGMA +← (‎ 𝜍 ‎) 1D70D MATHEMATICAL ITALIC SMALL FINAL SIGMA +← (‎ 𝝇 ‎) 1D747 MATHEMATICAL BOLD ITALIC SMALL FINAL SIGMA +← (‎ 𝞁 ‎) 1D781 MATHEMATICAL SANS-SERIF BOLD SMALL FINAL SIGMA +← (‎ 𝞻 ‎) 1D7BB MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL FINAL SIGMA + +# ᴛ τ т ꭲ 𝛕 𝜏 𝝉 𝞃 𝞽 + (‎ τ ‎) 03C4 GREEK SMALL LETTER TAU +← (‎ ᴛ ‎) 1D1B LATIN LETTER SMALL CAPITAL T +← (‎ т ‎) 0442 CYRILLIC SMALL LETTER TE +← (‎ ꭲ ‎) AB72 CHEROKEE SMALL LETTER I # →ᴛ→ +← (‎ 𝛕 ‎) 1D6D5 MATHEMATICAL BOLD SMALL TAU +← (‎ 𝜏 ‎) 1D70F MATHEMATICAL ITALIC SMALL TAU +← (‎ 𝝉 ‎) 1D749 MATHEMATICAL BOLD ITALIC SMALL TAU +← (‎ 𝞃 ‎) 1D783 MATHEMATICAL SANS-SERIF BOLD SMALL TAU +← (‎ 𝞽 ‎) 1D7BD MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL TAU + +# χ ⲭ ꭓ ꭕ 𝛘 𝜒 𝝌 𝞆 𝟀 + (‎ χ ‎) 03C7 GREEK SMALL LETTER CHI +← (‎ ⲭ ‎) 2CAD COPTIC SMALL LETTER KHI +← (‎ ꭓ ‎) AB53 LATIN SMALL LETTER CHI +← (‎ ꭕ ‎) AB55 LATIN SMALL LETTER CHI WITH LOW LEFT SERIF +← (‎ 𝛘 ‎) 1D6D8 MATHEMATICAL BOLD SMALL CHI +← (‎ 𝜒 ‎) 1D712 MATHEMATICAL ITALIC SMALL CHI +← (‎ 𝝌 ‎) 1D74C MATHEMATICAL BOLD ITALIC SMALL CHI +← (‎ 𝞆 ‎) 1D786 MATHEMATICAL SANS-SERIF BOLD SMALL CHI +← (‎ 𝟀 ‎) 1D7C0 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL CHI + +# ψ ѱ 𐓹 𝛙 𝜓 𝝍 𝞇 𝟁 + (‎ ψ ‎) 03C8 GREEK SMALL LETTER PSI +← (‎ ѱ ‎) 0471 CYRILLIC SMALL LETTER PSI +← (‎ 𐓹 ‎) 104F9 OSAGE SMALL LETTER GHA +← (‎ 𝛙 ‎) 1D6D9 MATHEMATICAL BOLD SMALL PSI +← (‎ 𝜓 ‎) 1D713 MATHEMATICAL ITALIC SMALL PSI +← (‎ 𝝍 ‎) 1D74D MATHEMATICAL BOLD ITALIC SMALL PSI +← (‎ 𝞇 ‎) 1D787 MATHEMATICAL SANS-SERIF BOLD SMALL PSI +← (‎ 𝟁 ‎) 1D7C1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PSI + +# ω ⲱ ꙍ ⍵ ꞷ 𝛚 𝜔 𝝎 𝞈 𝟂 + (‎ ω ‎) 03C9 GREEK SMALL LETTER OMEGA +← (‎ ⲱ ‎) 2CB1 COPTIC SMALL LETTER OOU +← (‎ ꙍ ‎) A64D CYRILLIC SMALL LETTER BROAD OMEGA # →ꞷ→ +← (‎ ⍵ ‎) 2375 APL FUNCTIONAL SYMBOL OMEGA +← (‎ ꞷ ‎) A7B7 LATIN SMALL LETTER OMEGA +← (‎ 𝛚 ‎) 1D6DA MATHEMATICAL BOLD SMALL OMEGA +← (‎ 𝜔 ‎) 1D714 MATHEMATICAL ITALIC SMALL OMEGA +← (‎ 𝝎 ‎) 1D74E MATHEMATICAL BOLD ITALIC SMALL OMEGA +← (‎ 𝞈 ‎) 1D788 MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA +← (‎ 𝟂 ‎) 1D7C2 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA + +# ω̲ ⍹ + (‎ ω̲ ‎) 03C9 0332 GREEK SMALL LETTER OMEGA, COMBINING LOW LINE +← (‎ ⍹ ‎) 2379 APL FUNCTIONAL SYMBOL OMEGA UNDERBAR + +# ϗ ⳤ + (‎ ϗ ‎) 03D7 GREEK KAI SYMBOL +← (‎ ⳤ ‎) 2CE4 COPTIC SYMBOL KAI + +# Ϙ 𐊭 𐌒 + (‎ Ϙ ‎) 03D8 GREEK LETTER ARCHAIC KOPPA +← (‎ 𐊭 ‎) 102AD CARIAN LETTER T +← (‎ 𐌒 ‎) 10312 OLD ITALIC LETTER KU + +# ϝ 𝟋 + (‎ ϝ ‎) 03DD GREEK SMALL LETTER DIGAMMA +← (‎ 𝟋 ‎) 1D7CB MATHEMATICAL BOLD SMALL DIGAMMA + +# Ϭ Ⳝ + (‎ Ϭ ‎) 03EC COPTIC CAPITAL LETTER SHIMA +← (‎ Ⳝ ‎) 2CDC COPTIC CAPITAL LETTER OLD NUBIAN SHIMA + +# Ꜿ Ͽ + (‎ Ͽ ‎) 03FF GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL +← (‎ Ꜿ ‎) A73E LATIN CAPITAL LETTER REVERSED C WITH DOT + +# Ꞓ Є Ⲉ € + (‎ Є ‎) 0404 CYRILLIC CAPITAL LETTER UKRAINIAN IE +← (‎ Ꞓ ‎) A792 LATIN CAPITAL LETTER C WITH BAR +← (‎ Ⲉ ‎) 2C88 COPTIC CAPITAL LETTER EIE +← (‎ € ‎) 20AC EURO SIGN + +# Ћ 𐓍 + (‎ Ћ ‎) 040B CYRILLIC CAPITAL LETTER TSHE +← (‎ 𐓍 ‎) 104CD OSAGE CAPITAL LETTER DHA + +# Ѝ Й + (‎ Ѝ ‎) 040D CYRILLIC CAPITAL LETTER I WITH GRAVE +← (‎ Й ‎) 0419 CYRILLIC CAPITAL LETTER SHORT I + +# Ѝ̦ Й̦ Й̡ Ҋ + (‎ Ѝ̦ ‎) 040D 0326 CYRILLIC CAPITAL LETTER I WITH GRAVE, COMBINING COMMA BELOW +← (‎ Й̦ ‎) 0419 0326 CYRILLIC CAPITAL LETTER SHORT I, COMBINING COMMA BELOW # →Й̡→ +← (‎ Й̡ ‎) 0419 0321 CYRILLIC CAPITAL LETTER SHORT I, COMBINING PALATALIZED HOOK BELOW +← (‎ Ҋ ‎) 048A CYRILLIC CAPITAL LETTER SHORT I WITH TAIL # →Й̡→ + +# Ж̩ Җ + (‎ Ж̩ ‎) 0416 0329 CYRILLIC CAPITAL LETTER ZHE, COMBINING VERTICAL LINE BELOW +← (‎ Җ ‎) 0496 CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER + +# Ш Ⲽ + (‎ Ш ‎) 0428 CYRILLIC CAPITAL LETTER SHA +← (‎ Ⲽ ‎) 2CBC COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI + +# Ъl Ъ1 ЪІ Ꙑ + (‎ Ъ1 ‎) 042A 0031 CYRILLIC CAPITAL LETTER HARD SIGN, DIGIT ONE +← (‎ Ъl ‎) 042A 006C CYRILLIC CAPITAL LETTER HARD SIGN, LATIN SMALL LETTER L # →ЪІ→ +← (‎ ЪІ ‎) 042A 0406 CYRILLIC CAPITAL LETTER HARD SIGN, CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I +← (‎ Ꙑ ‎) A650 CYRILLIC CAPITAL LETTER YERU WITH BACK YER # →ЪІ→ + +# Э ℈ + (‎ Э ‎) 042D CYRILLIC CAPITAL LETTER E +← (‎ ℈ ‎) 2108 SCRUPLE + +# ж̩ җ + (‎ ж̩ ‎) 0436 0329 CYRILLIC SMALL LETTER ZHE, COMBINING VERTICAL LINE BELOW +← (‎ җ ‎) 0497 CYRILLIC SMALL LETTER ZHE WITH DESCENDER + +# й ѝ + (‎ й ‎) 0439 CYRILLIC SMALL LETTER SHORT I +← (‎ ѝ ‎) 045D CYRILLIC SMALL LETTER I WITH GRAVE + +# й̦ й̡ ҋ + (‎ й̡ ‎) 0439 0321 CYRILLIC SMALL LETTER SHORT I, COMBINING PALATALIZED HOOK BELOW +← (‎ й̦ ‎) 0439 0326 CYRILLIC SMALL LETTER SHORT I, COMBINING COMMA BELOW +← (‎ ҋ ‎) 048B CYRILLIC SMALL LETTER SHORT I WITH TAIL + +# л ᴫ + (‎ л ‎) 043B CYRILLIC SMALL LETTER EL +← (‎ ᴫ ‎) 1D2B CYRILLIC LETTER SMALL CAPITAL EL + +# л̦ л̡ ӆ + (‎ л̡ ‎) 043B 0321 CYRILLIC SMALL LETTER EL, COMBINING PALATALIZED HOOK BELOW +← (‎ л̦ ‎) 043B 0326 CYRILLIC SMALL LETTER EL, COMBINING COMMA BELOW +← (‎ ӆ ‎) 04C6 CYRILLIC SMALL LETTER EL WITH TAIL + +# ᴛ̩ т̩ ҭ + (‎ т̩ ‎) 0442 0329 CYRILLIC SMALL LETTER TE, COMBINING VERTICAL LINE BELOW +← (‎ ᴛ̩ ‎) 1D1B 0329 LATIN LETTER SMALL CAPITAL T, COMBINING VERTICAL LINE BELOW +← (‎ ҭ ‎) 04AD CYRILLIC SMALL LETTER TE WITH DESCENDER + +# ш ⲽ + (‎ ш ‎) 0448 CYRILLIC SMALL LETTER SHA +← (‎ ⲽ ‎) 2CBD COPTIC SMALL LETTER CRYPTOGRAMMIC NI + +# ᴙ я + (‎ я ‎) 044F CYRILLIC SMALL LETTER YA +← (‎ ᴙ ‎) 1D19 LATIN LETTER SMALL CAPITAL REVERSED R + +# љ ꭠ + (‎ љ ‎) 0459 CYRILLIC SMALL LETTER LJE +← (‎ ꭠ ‎) AB60 LATIN SMALL LETTER SAKHA YAT + +# Ѡ Ꮗ ᗯ 𝈢 + (‎ Ѡ ‎) 0460 CYRILLIC CAPITAL LETTER OMEGA +← (‎ Ꮗ ‎) 13C7 CHEROKEE LETTER QUE +← (‎ ᗯ ‎) 15EF CANADIAN SYLLABICS CARRIER GU +← (‎ 𝈢 ‎) 1D222 GREEK INSTRUMENTAL NOTATION SYMBOL-8 + +# Ѡ· Ꮗ· ᗯ· ᗯᐧ ᣭ + (‎ Ѡ· ‎) 0460 00B7 CYRILLIC CAPITAL LETTER OMEGA, MIDDLE DOT +← (‎ Ꮗ· ‎) 13C7 00B7 CHEROKEE LETTER QUE, MIDDLE DOT # →ᗯ·→→ᗯᐧ→ +← (‎ ᗯ· ‎) 15EF 00B7 CANADIAN SYLLABICS CARRIER GU, MIDDLE DOT # →ᗯᐧ→ +← (‎ ᗯᐧ ‎) 15EF 1427 CANADIAN SYLLABICS CARRIER GU, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣭ ‎) 18ED CANADIAN SYLLABICS CARRIER GWU # →ᗯᐧ→ + +# Ѡ҆҇ Ѡ҃ Ѽ + (‎ Ѡ҃ ‎) 0460 0483 CYRILLIC CAPITAL LETTER OMEGA, COMBINING CYRILLIC TITLO +← (‎ Ѡ҆҇ ‎) 0460 0486 0487 CYRILLIC CAPITAL LETTER OMEGA, COMBINING CYRILLIC PSILI PNEUMATA, COMBINING CYRILLIC POKRYTIE # →Ѽ→ +← (‎ Ѽ ‎) 047C CYRILLIC CAPITAL LETTER OMEGA WITH TITLO + +# Ҷ Ӌ + (‎ Ҷ ‎) 04B6 CYRILLIC CAPITAL LETTER CHE WITH DESCENDER +← (‎ Ӌ ‎) 04CB CYRILLIC CAPITAL LETTER KHAKASSIAN CHE + +# ҷ ӌ + (‎ ҷ ‎) 04B7 CYRILLIC SMALL LETTER CHE WITH DESCENDER +← (‎ ӌ ‎) 04CC CYRILLIC SMALL LETTER KHAKASSIAN CHE + +# Ҽ̨ Ҿ + (‎ Ҽ̨ ‎) 04BC 0328 CYRILLIC CAPITAL LETTER ABKHASIAN CHE, COMBINING OGONEK +← (‎ Ҿ ‎) 04BE CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER + +# Ӄ 𐒼 + (‎ Ӄ ‎) 04C3 CYRILLIC CAPITAL LETTER KA WITH HOOK +← (‎ 𐒼 ‎) 104BC OSAGE CAPITAL LETTER KA + +# Ӿ 𝈂 + (‎ Ӿ ‎) 04FE CYRILLIC CAPITAL LETTER HA WITH STROKE +← (‎ 𝈂 ‎) 1D202 GREEK VOCAL NOTATION SYMBOL-3 + +# ኮ Ի + (‎ Ի ‎) 053B ARMENIAN CAPITAL LETTER INI +← (‎ ኮ ‎) 12AE ETHIOPIC SYLLABLE KO + +# ሆ Մ + (‎ Մ ‎) 0544 ARMENIAN CAPITAL LETTER MEN +← (‎ ሆ ‎) 1206 ETHIOPIC SYLLABLE HO + +# Ո በ ᑎ ꓵ ∩ ⋂ 𝉅 + (‎ Ո ‎) 0548 ARMENIAN CAPITAL LETTER VO +← (‎ በ ‎) 1260 ETHIOPIC SYLLABLE BA +← (‎ ᑎ ‎) 144E CANADIAN SYLLABICS TI +← (‎ ꓵ ‎) A4F5 LISU LETTER UE # →∩→→ᑎ→ +← (‎ ∩ ‎) 2229 INTERSECTION # →ᑎ→ +← (‎ ⋂ ‎) 22C2 N-ARY INTERSECTION # →∩→→ᑎ→ +← (‎ 𝉅 ‎) 1D245 GREEK MUSICAL LEIMMA # →∩→→ᑎ→ + +# Ո' ᑎᑊ በ' ᑎ' ᑨ + (‎ Ո' ‎) 0548 0027 ARMENIAN CAPITAL LETTER VO, APOSTROPHE +← (‎ ᑎᑊ ‎) 144E 144A CANADIAN SYLLABICS TI, CANADIAN SYLLABICS WEST-CREE P # →ᑎ'→ +← (‎ በ' ‎) 1260 0027 ETHIOPIC SYLLABLE BA, APOSTROPHE # →ᑎ'→ +← (‎ ᑎ' ‎) 144E 0027 CANADIAN SYLLABICS TI, APOSTROPHE +← (‎ ᑨ ‎) 1468 CANADIAN SYLLABICS TTI # →ᑎᑊ→→ᑎ'→ + +# Ո· በ· ᑎ· ᑎᐧ ᑚ + (‎ Ո· ‎) 0548 00B7 ARMENIAN CAPITAL LETTER VO, MIDDLE DOT +← (‎ በ· ‎) 1260 00B7 ETHIOPIC SYLLABLE BA, MIDDLE DOT # →ᑎ·→ +← (‎ ᑎ· ‎) 144E 00B7 CANADIAN SYLLABICS TI, MIDDLE DOT +← (‎ ᑎᐧ ‎) 144E 1427 CANADIAN SYLLABICS TI, CANADIAN SYLLABICS FINAL MIDDLE DOT # →ᑎ·→ +← (‎ ᑚ ‎) 145A CANADIAN SYLLABICS WEST-CREE TWI # →ᑎᐧ→→ᑎ·→ + +# ጣ Պ + (‎ Պ ‎) 054A ARMENIAN CAPITAL LETTER PEH +← (‎ ጣ ‎) 1323 ETHIOPIC SYLLABLE THAA + +# ቡ Ռ + (‎ Ռ ‎) 054C ARMENIAN CAPITAL LETTER RA +← (‎ ቡ ‎) 1261 ETHIOPIC SYLLABLE BU + +# Ք ₽ + (‎ Ք ‎) 0554 ARMENIAN CAPITAL LETTER KEH +← (‎ ₽ ‎) 20BD RUBLE SIGN + +# եւ և + (‎ եւ ‎) 0565 0582 ARMENIAN SMALL LETTER ECH, ARMENIAN SMALL LETTER YIWN +← (‎ և ‎) 0587 ARMENIAN SMALL LIGATURE ECH YIWN + +# ձ ኔ + (‎ ձ ‎) 0571 ARMENIAN SMALL LETTER JA +← (‎ ኔ ‎) 1294 ETHIOPIC SYLLABLE NEE + +# մե ﬔ + (‎ մե ‎) 0574 0565 ARMENIAN SMALL LETTER MEN, ARMENIAN SMALL LETTER ECH +← (‎ ﬔ ‎) FB14 ARMENIAN SMALL LIGATURE MEN ECH + +# մի ﬕ + (‎ մի ‎) 0574 056B ARMENIAN SMALL LETTER MEN, ARMENIAN SMALL LETTER INI +← (‎ ﬕ ‎) FB15 ARMENIAN SMALL LIGATURE MEN INI + +# մխ ﬗ + (‎ մխ ‎) 0574 056D ARMENIAN SMALL LETTER MEN, ARMENIAN SMALL LETTER XEH +← (‎ ﬗ ‎) FB17 ARMENIAN SMALL LIGATURE MEN XEH + +# մն ﬓ + (‎ մն ‎) 0574 0576 ARMENIAN SMALL LETTER MEN, ARMENIAN SMALL LETTER NOW +← (‎ ﬓ ‎) FB13 ARMENIAN SMALL LIGATURE MEN NOW + +# վն ﬖ + (‎ վն ‎) 057E 0576 ARMENIAN SMALL LETTER VEW, ARMENIAN SMALL LETTER NOW +← (‎ ﬖ ‎) FB16 ARMENIAN SMALL LIGATURE VEW NOW + +# ֖ ֭ + (‎ ֖ ‎) 0596 HEBREW ACCENT TIPEHA +← (‎ ֭ ‎) 05AD HEBREW ACCENT DEHI + +# ֘ ֮ + (‎ ֘ ‎) 0598 HEBREW ACCENT ZARQA +← (‎ ֮ ‎) 05AE HEBREW ACCENT ZINOR + +# ֙ ֨ + (‎ ֙ ‎) 0599 HEBREW ACCENT PASHTA +← (‎ ֨ ‎) 05A8 HEBREW ACCENT QADMA + +# ֚ ֤ + (‎ ֚ ‎) 059A HEBREW ACCENT YETIV +← (‎ ֤ ‎) 05A4 HEBREW ACCENT MAHAPAKH + +# א ℵ ﬡ + (‎ א ‎) 05D0 HEBREW LETTER ALEF +← (‎ ℵ ‎) 2135 ALEF SYMBOL +← (‎ ﬡ ‎) FB21 HEBREW LETTER WIDE ALEF + +# אל ﭏ + (‎ אל ‎) 05D0 05DC HEBREW LETTER ALEF, HEBREW LETTER LAMED +← (‎ ﭏ ‎) FB4F HEBREW LIGATURE ALEF LAMED + +# ב ℶ + (‎ ב ‎) 05D1 HEBREW LETTER BET +← (‎ ℶ ‎) 2136 BET SYMBOL + +# ג ℷ + (‎ ג ‎) 05D2 HEBREW LETTER GIMEL +← (‎ ℷ ‎) 2137 GIMEL SYMBOL + +# ד ℸ ﬢ + (‎ ד ‎) 05D3 HEBREW LETTER DALET +← (‎ ℸ ‎) 2138 DALET SYMBOL +← (‎ ﬢ ‎) FB22 HEBREW LETTER WIDE DALET + +# ה ﬣ + (‎ ה ‎) 05D4 HEBREW LETTER HE +← (‎ ﬣ ‎) FB23 HEBREW LETTER WIDE HE + +# כ ﬤ + (‎ כ ‎) 05DB HEBREW LETTER KAF +← (‎ ﬤ ‎) FB24 HEBREW LETTER WIDE KAF + +# ל ﬥ + (‎ ל ‎) 05DC HEBREW LETTER LAMED +← (‎ ﬥ ‎) FB25 HEBREW LETTER WIDE LAMED + +# ם ﬦ + (‎ ם ‎) 05DD HEBREW LETTER FINAL MEM +← (‎ ﬦ ‎) FB26 HEBREW LETTER WIDE FINAL MEM + +# ע ﬠ + (‎ ע ‎) 05E2 HEBREW LETTER AYIN +← (‎ ﬠ ‎) FB20 HEBREW LETTER ALTERNATIVE AYIN + +# ר ﬧ + (‎ ר ‎) 05E8 HEBREW LETTER RESH +← (‎ ﬧ ‎) FB27 HEBREW LETTER WIDE RESH + +# ת ﬨ + (‎ ת ‎) 05EA HEBREW LETTER TAV +← (‎ ﬨ ‎) FB28 HEBREW LETTER WIDE TAV + +# ، ٬ ⸲ + (‎ ، ‎) 060C ARABIC COMMA +← (‎ ٬ ‎) 066C ARABIC THOUSANDS SEPARATOR +← (‎ ⸲ ‎) 2E32 TURNED COMMA + +# ع ؏ 𞸏 𞸯 𞹏 𞹯 𞺏 𞺯 ﻉ ﻊ ﻋ ﻌ + (‎ ؏ ‎) 060F ARABIC SIGN MISRA +← (‎ ع ‎) 0639 ARABIC LETTER AIN +← (‎ 𞸏 ‎) 1EE0F ARABIC MATHEMATICAL AIN # →‎ع‎→ +← (‎ 𞸯 ‎) 1EE2F ARABIC MATHEMATICAL INITIAL AIN # →‎ع‎→ +← (‎ 𞹏 ‎) 1EE4F ARABIC MATHEMATICAL TAILED AIN # →‎ع‎→ +← (‎ 𞹯 ‎) 1EE6F ARABIC MATHEMATICAL STRETCHED AIN # →‎ع‎→ +← (‎ 𞺏 ‎) 1EE8F ARABIC MATHEMATICAL LOOPED AIN +← (‎ 𞺯 ‎) 1EEAF ARABIC MATHEMATICAL DOUBLE-STRUCK AIN # →‎ع‎→ +← (‎ ﻉ ‎) FEC9 ARABIC LETTER AIN ISOLATED FORM # →‎ع‎→ +← (‎ ﻊ ‎) FECA ARABIC LETTER AIN FINAL FORM # →‎ع‎→ +← (‎ ﻋ ‎) FECB ARABIC LETTER AIN INITIAL FORM # →‎ع‎→ +← (‎ ﻌ ‎) FECC ARABIC LETTER AIN MEDIAL FORM # →‎ع‎→ + +# ؛ ⸵ + (‎ ؛ ‎) 061B ARABIC SEMICOLON +← (‎ ⸵ ‎) 2E35 TURNED SEMICOLON + +# ؟ ⸮ + (‎ ؟ ‎) 061F ARABIC QUESTION MARK +← (‎ ⸮ ‎) 2E2E REVERSED QUESTION MARK + +# ء ﺀ + (‎ ء ‎) 0621 ARABIC LETTER HAMZA +← (‎ ﺀ ‎) FE80 ARABIC LETTER HAMZA ISOLATED FORM + +# ء͈ ۽ + (‎ ء͈ ‎) 0621 0348 ARABIC LETTER HAMZA, COMBINING DOUBLE VERTICAL LINE BELOW +← (‎ ۽ ‎) 06FD ARABIC SIGN SINDHI AMPERSAND + +# آ ﺁ ﺂ + (‎ آ ‎) 0622 ARABIC LETTER ALEF WITH MADDA ABOVE +← (‎ ﺁ ‎) FE81 ARABIC LETTER ALEF WITH MADDA ABOVE ISOLATED FORM +← (‎ ﺂ ‎) FE82 ARABIC LETTER ALEF WITH MADDA ABOVE FINAL FORM + +# وٴ ٴو ؤ ٶ ﺅ ﺆ + (‎ ؤ ‎) 0624 ARABIC LETTER WAW WITH HAMZA ABOVE +← (‎ وٴ ‎) 0648 0674 ARABIC LETTER WAW, ARABIC LETTER HIGH HAMZA # →‎ٶ‎→ +← (‎ ٴو ‎) 0674 0648 ARABIC LETTER HIGH HAMZA, ARABIC LETTER WAW # →‎ٶ‎→ +← (‎ ٶ ‎) 0676 ARABIC LETTER HIGH HAMZA WAW +← (‎ ﺅ ‎) FE85 ARABIC LETTER WAW WITH HAMZA ABOVE ISOLATED FORM +← (‎ ﺆ ‎) FE86 ARABIC LETTER WAW WITH HAMZA ABOVE FINAL FORM + +# ىٴ يٴ ٴى ئ ٸ ﺉ ﺊ ﺋ ﺌ + (‎ ئ ‎) 0626 ARABIC LETTER YEH WITH HAMZA ABOVE +← (‎ ىٴ ‎) 0649 0674 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA # →‎يٴ‎→→‎ٸ‎→ +← (‎ يٴ ‎) 064A 0674 ARABIC LETTER YEH, ARABIC LETTER HIGH HAMZA # →‎ٸ‎→ +← (‎ ٴى ‎) 0674 0649 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA # →‎ٸ‎→ +← (‎ ٸ ‎) 0678 ARABIC LETTER HIGH HAMZA YEH +← (‎ ﺉ ‎) FE89 ARABIC LETTER YEH WITH HAMZA ABOVE ISOLATED FORM +← (‎ ﺊ ‎) FE8A ARABIC LETTER YEH WITH HAMZA ABOVE FINAL FORM +← (‎ ﺋ ‎) FE8B ARABIC LETTER YEH WITH HAMZA ABOVE INITIAL FORM +← (‎ ﺌ ‎) FE8C ARABIC LETTER YEH WITH HAMZA ABOVE MEDIAL FORM + +# ىٴl ٴىl ىٴ1 ئا ﯪ ﯫ + (‎ ئا ‎) 0626 0627 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER ALEF +← (‎ ىٴl ‎) 0649 0674 006C ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, LATIN SMALL LETTER L +← (‎ ٴىl ‎) 0674 0649 006C ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, LATIN SMALL LETTER L +← (‎ ىٴ1 ‎) 0649 0674 0031 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, DIGIT ONE +← (‎ ﯪ ‎) FBEA ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF ISOLATED FORM +← (‎ ﯫ ‎) FBEB ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF FINAL FORM + +# ىٴج ٴىج ئج ﰀ ﲗ + (‎ ئج ‎) 0626 062C ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER JEEM +← (‎ ىٴج ‎) 0649 0674 062C ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER JEEM +← (‎ ٴىج ‎) 0674 0649 062C ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER JEEM +← (‎ ﰀ ‎) FC00 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH JEEM ISOLATED FORM +← (‎ ﲗ ‎) FC97 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH JEEM INITIAL FORM + +# ىٴح ٴىح ئح ﰁ ﲘ + (‎ ئح ‎) 0626 062D ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER HAH +← (‎ ىٴح ‎) 0649 0674 062D ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER HAH +← (‎ ٴىح ‎) 0674 0649 062D ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HAH +← (‎ ﰁ ‎) FC01 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HAH ISOLATED FORM +← (‎ ﲘ ‎) FC98 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HAH INITIAL FORM + +# ىٴخ ٴىخ ئخ ﲙ + (‎ ئخ ‎) 0626 062E ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER KHAH +← (‎ ىٴخ ‎) 0649 0674 062E ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER KHAH +← (‎ ٴىخ ‎) 0674 0649 062E ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER KHAH +← (‎ ﲙ ‎) FC99 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH KHAH INITIAL FORM + +# ىٴر ٴىر ئر ﱤ + (‎ ئر ‎) 0626 0631 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER REH +← (‎ ىٴر ‎) 0649 0674 0631 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER REH +← (‎ ٴىر ‎) 0674 0649 0631 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER REH +← (‎ ﱤ ‎) FC64 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH REH FINAL FORM + +# ىٴز ٴىز ئز ﱥ + (‎ ئز ‎) 0626 0632 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER ZAIN +← (‎ ىٴز ‎) 0649 0674 0632 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER ZAIN +← (‎ ٴىز ‎) 0674 0649 0632 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER ZAIN +← (‎ ﱥ ‎) FC65 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ZAIN FINAL FORM + +# ىٴم ٴىم ئم ﰂ ﱦ ﲚ ﳟ + (‎ ئم ‎) 0626 0645 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER MEEM +← (‎ ىٴم ‎) 0649 0674 0645 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER MEEM +← (‎ ٴىم ‎) 0674 0649 0645 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER MEEM +← (‎ ﰂ ‎) FC02 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM ISOLATED FORM +← (‎ ﱦ ‎) FC66 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM FINAL FORM +← (‎ ﲚ ‎) FC9A ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM INITIAL FORM +← (‎ ﳟ ‎) FCDF ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM MEDIAL FORM + +# ىٴن ٴىن ئن ﱧ + (‎ ئن ‎) 0626 0646 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER NOON +← (‎ ىٴن ‎) 0649 0674 0646 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER NOON +← (‎ ٴىن ‎) 0674 0649 0646 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER NOON +← (‎ ﱧ ‎) FC67 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH NOON FINAL FORM + +# ىٴo ٴىo ىٴه ئه ئە ﯬ ﯭ ﲛ ﳠ + (‎ ئه ‎) 0626 0647 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER HEH +← (‎ ىٴo ‎) 0649 0674 006F ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, LATIN SMALL LETTER O +← (‎ ٴىo ‎) 0674 0649 006F ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, LATIN SMALL LETTER O +← (‎ ىٴه ‎) 0649 0674 0647 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER HEH +← (‎ ئە ‎) 0626 06D5 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER AE # →‎ٴىo‎→ +← (‎ ﯬ ‎) FBEC ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH AE ISOLATED FORM # →‎ئە‎→→‎ٴىo‎→ +← (‎ ﯭ ‎) FBED ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH AE FINAL FORM # →‎ئە‎→→‎ٴىo‎→ +← (‎ ﲛ ‎) FC9B ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HEH INITIAL FORM +← (‎ ﳠ ‎) FCE0 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HEH MEDIAL FORM + +# ىٴو ٴىو ئو ﯮ ﯯ + (‎ ئو ‎) 0626 0648 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER WAW +← (‎ ىٴو ‎) 0649 0674 0648 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER WAW +← (‎ ٴىو ‎) 0674 0649 0648 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER WAW +← (‎ ﯮ ‎) FBEE ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH WAW ISOLATED FORM +← (‎ ﯯ ‎) FBEF ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH WAW FINAL FORM + +# ىٴى ٴىى ئى ئي ﯹ ﯺ ﯻ ﰃ ﰄ ﱨ ﱩ + (‎ ئى ‎) 0626 0649 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER ALEF MAKSURA +← (‎ ىٴى ‎) 0649 0674 0649 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA +← (‎ ٴىى ‎) 0674 0649 0649 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER ALEF MAKSURA +← (‎ ئي ‎) 0626 064A ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER YEH # →‎ٴىى‎→ +← (‎ ﯹ ‎) FBF9 ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﯺ ‎) FBFA ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA FINAL FORM +← (‎ ﯻ ‎) FBFB ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA INITIAL FORM +← (‎ ﰃ ‎) FC03 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰄ ‎) FC04 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YEH ISOLATED FORM # →‎ئي‎→→‎ٴىى‎→ +← (‎ ﱨ ‎) FC68 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF MAKSURA FINAL FORM +← (‎ ﱩ ‎) FC69 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YEH FINAL FORM # →‎ئي‎→→‎ٴىى‎→ + +# ىٴو̆ ٴىو̆ ئۆ ﯲ ﯳ + (‎ ئۆ ‎) 0626 06C6 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER OE +← (‎ ىٴو̆ ‎) 0649 0674 0648 0306 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER WAW, COMBINING BREVE +← (‎ ٴىو̆ ‎) 0674 0649 0648 0306 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER WAW, COMBINING BREVE +← (‎ ﯲ ‎) FBF2 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH OE ISOLATED FORM +← (‎ ﯳ ‎) FBF3 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH OE FINAL FORM + +# ىٴو̓ ٴىو̓ ئۇ ﯰ ﯱ + (‎ ئۇ ‎) 0626 06C7 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER U +← (‎ ىٴو̓ ‎) 0649 0674 0648 0313 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER WAW, COMBINING COMMA ABOVE +← (‎ ٴىو̓ ‎) 0674 0649 0648 0313 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER WAW, COMBINING COMMA ABOVE +← (‎ ﯰ ‎) FBF0 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH U ISOLATED FORM +← (‎ ﯱ ‎) FBF1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH U FINAL FORM + +# ىٴوٰ ٴىوٰ ئۈ ﯴ ﯵ + (‎ ئۈ ‎) 0626 06C8 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER YU +← (‎ ىٴوٰ ‎) 0649 0674 0648 0670 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER WAW, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ٴىوٰ ‎) 0674 0649 0648 0670 ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER WAW, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ﯴ ‎) FBF4 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YU ISOLATED FORM +← (‎ ﯵ ‎) FBF5 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YU FINAL FORM + +# ىٴٻ ٴىٻ ئې ﯶ ﯷ ﯸ + (‎ ئې ‎) 0626 06D0 ARABIC LETTER YEH WITH HAMZA ABOVE, ARABIC LETTER E +← (‎ ىٴٻ ‎) 0649 0674 067B ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HIGH HAMZA, ARABIC LETTER BEEH +← (‎ ٴىٻ ‎) 0674 0649 067B ARABIC LETTER HIGH HAMZA, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER BEEH +← (‎ ﯶ ‎) FBF6 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E ISOLATED FORM +← (‎ ﯷ ‎) FBF7 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E FINAL FORM +← (‎ ﯸ ‎) FBF8 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E INITIAL FORM + +# ب 𞸁 𞸡 𞹡 𞺁 𞺡 ﺏ ﺐ ﺑ ﺒ + (‎ ب ‎) 0628 ARABIC LETTER BEH +← (‎ 𞸁 ‎) 1EE01 ARABIC MATHEMATICAL BEH +← (‎ 𞸡 ‎) 1EE21 ARABIC MATHEMATICAL INITIAL BEH +← (‎ 𞹡 ‎) 1EE61 ARABIC MATHEMATICAL STRETCHED BEH +← (‎ 𞺁 ‎) 1EE81 ARABIC MATHEMATICAL LOOPED BEH +← (‎ 𞺡 ‎) 1EEA1 ARABIC MATHEMATICAL DOUBLE-STRUCK BEH +← (‎ ﺏ ‎) FE8F ARABIC LETTER BEH ISOLATED FORM +← (‎ ﺐ ‎) FE90 ARABIC LETTER BEH FINAL FORM +← (‎ ﺑ ‎) FE91 ARABIC LETTER BEH INITIAL FORM +← (‎ ﺒ ‎) FE92 ARABIC LETTER BEH MEDIAL FORM + +# بo به ﲠ ﳢ + (‎ بo ‎) 0628 006F ARABIC LETTER BEH, LATIN SMALL LETTER O +← (‎ به ‎) 0628 0647 ARABIC LETTER BEH, ARABIC LETTER HEH +← (‎ ﲠ ‎) FCA0 ARABIC LIGATURE BEH WITH HEH INITIAL FORM # →‎به‎→ +← (‎ ﳢ ‎) FCE2 ARABIC LIGATURE BEH WITH HEH MEDIAL FORM # →‎به‎→ + +# بج ﰅ ﲜ + (‎ بج ‎) 0628 062C ARABIC LETTER BEH, ARABIC LETTER JEEM +← (‎ ﰅ ‎) FC05 ARABIC LIGATURE BEH WITH JEEM ISOLATED FORM +← (‎ ﲜ ‎) FC9C ARABIC LIGATURE BEH WITH JEEM INITIAL FORM + +# بح ﰆ ﲝ + (‎ بح ‎) 0628 062D ARABIC LETTER BEH, ARABIC LETTER HAH +← (‎ ﰆ ‎) FC06 ARABIC LIGATURE BEH WITH HAH ISOLATED FORM +← (‎ ﲝ ‎) FC9D ARABIC LIGATURE BEH WITH HAH INITIAL FORM + +# بحى بحي ﷂ + (‎ بحى ‎) 0628 062D 0649 ARABIC LETTER BEH, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ بحي ‎) 0628 062D 064A ARABIC LETTER BEH, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﷂ ‎) FDC2 ARABIC LIGATURE BEH WITH HAH WITH YEH FINAL FORM # →‎بحي‎→ + +# بخ نج ﰇ ﱋ ﲞ ﳒ + (‎ بخ ‎) 0628 062E ARABIC LETTER BEH, ARABIC LETTER KHAH +← (‎ نج ‎) 0646 062C ARABIC LETTER NOON, ARABIC LETTER JEEM # →‎ﳒ‎→→‎ﲞ‎→ +← (‎ ﰇ ‎) FC07 ARABIC LIGATURE BEH WITH KHAH ISOLATED FORM +← (‎ ﱋ ‎) FC4B ARABIC LIGATURE NOON WITH JEEM ISOLATED FORM # →‎نج‎→→‎ﳒ‎→→‎ﲞ‎→ +← (‎ ﲞ ‎) FC9E ARABIC LIGATURE BEH WITH KHAH INITIAL FORM +← (‎ ﳒ ‎) FCD2 ARABIC LIGATURE NOON WITH JEEM INITIAL FORM # →‎ﲞ‎→ + +# بخى بخي ﶞ + (‎ بخى ‎) 0628 062E 0649 ARABIC LETTER BEH, ARABIC LETTER KHAH, ARABIC LETTER ALEF MAKSURA +← (‎ بخي ‎) 0628 062E 064A ARABIC LETTER BEH, ARABIC LETTER KHAH, ARABIC LETTER YEH +← (‎ ﶞ ‎) FD9E ARABIC LIGATURE BEH WITH KHAH WITH YEH FINAL FORM # →‎بخي‎→ + +# بر ﱪ + (‎ بر ‎) 0628 0631 ARABIC LETTER BEH, ARABIC LETTER REH +← (‎ ﱪ ‎) FC6A ARABIC LIGATURE BEH WITH REH FINAL FORM + +# بز ﱫ + (‎ بز ‎) 0628 0632 ARABIC LETTER BEH, ARABIC LETTER ZAIN +← (‎ ﱫ ‎) FC6B ARABIC LIGATURE BEH WITH ZAIN FINAL FORM + +# بم ﰈ ﱬ ﲟ ﳡ + (‎ بم ‎) 0628 0645 ARABIC LETTER BEH, ARABIC LETTER MEEM +← (‎ ﰈ ‎) FC08 ARABIC LIGATURE BEH WITH MEEM ISOLATED FORM +← (‎ ﱬ ‎) FC6C ARABIC LIGATURE BEH WITH MEEM FINAL FORM +← (‎ ﲟ ‎) FC9F ARABIC LIGATURE BEH WITH MEEM INITIAL FORM +← (‎ ﳡ ‎) FCE1 ARABIC LIGATURE BEH WITH MEEM MEDIAL FORM + +# بن ﱭ + (‎ بن ‎) 0628 0646 ARABIC LETTER BEH, ARABIC LETTER NOON +← (‎ ﱭ ‎) FC6D ARABIC LIGATURE BEH WITH NOON FINAL FORM + +# بى بي ﰉ ﰊ ﱮ ﱯ + (‎ بى ‎) 0628 0649 ARABIC LETTER BEH, ARABIC LETTER ALEF MAKSURA +← (‎ بي ‎) 0628 064A ARABIC LETTER BEH, ARABIC LETTER YEH +← (‎ ﰉ ‎) FC09 ARABIC LIGATURE BEH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰊ ‎) FC0A ARABIC LIGATURE BEH WITH YEH ISOLATED FORM # →‎بي‎→ +← (‎ ﱮ ‎) FC6E ARABIC LIGATURE BEH WITH ALEF MAKSURA FINAL FORM +← (‎ ﱯ ‎) FC6F ARABIC LIGATURE BEH WITH YEH FINAL FORM # →‎بي‎→ + +# بٔ ࢡ + (‎ بٔ ‎) 0628 0654 ARABIC LETTER BEH, ARABIC HAMZA ABOVE +← (‎ ࢡ ‎) 08A1 ARABIC LETTER BEH WITH HAMZA ABOVE + +# بۛ ݑ + (‎ بۛ ‎) 0628 06DB ARABIC LETTER BEH, ARABIC SMALL HIGH THREE DOTS +← (‎ ݑ ‎) 0751 ARABIC LETTER BEH WITH DOT BELOW AND THREE DOTS ABOVE + +# بۢ ࢶ + (‎ بۢ ‎) 0628 06E2 ARABIC LETTER BEH, ARABIC SMALL HIGH MEEM ISOLATED FORM +← (‎ ࢶ ‎) 08B6 ARABIC LETTER BEH WITH SMALL MEEM ABOVE + +# ت 𞸕 𞸵 𞹵 𞺕 𞺵 ﺕ ﺖ ﺗ ﺘ + (‎ ت ‎) 062A ARABIC LETTER TEH +← (‎ 𞸕 ‎) 1EE15 ARABIC MATHEMATICAL TEH +← (‎ 𞸵 ‎) 1EE35 ARABIC MATHEMATICAL INITIAL TEH +← (‎ 𞹵 ‎) 1EE75 ARABIC MATHEMATICAL STRETCHED TEH +← (‎ 𞺕 ‎) 1EE95 ARABIC MATHEMATICAL LOOPED TEH +← (‎ 𞺵 ‎) 1EEB5 ARABIC MATHEMATICAL DOUBLE-STRUCK TEH +← (‎ ﺕ ‎) FE95 ARABIC LETTER TEH ISOLATED FORM +← (‎ ﺖ ‎) FE96 ARABIC LETTER TEH FINAL FORM +← (‎ ﺗ ‎) FE97 ARABIC LETTER TEH INITIAL FORM +← (‎ ﺘ ‎) FE98 ARABIC LETTER TEH MEDIAL FORM + +# تo ته ﲥ ﳤ + (‎ تo ‎) 062A 006F ARABIC LETTER TEH, LATIN SMALL LETTER O +← (‎ ته ‎) 062A 0647 ARABIC LETTER TEH, ARABIC LETTER HEH +← (‎ ﲥ ‎) FCA5 ARABIC LIGATURE TEH WITH HEH INITIAL FORM # →‎ته‎→ +← (‎ ﳤ ‎) FCE4 ARABIC LIGATURE TEH WITH HEH MEDIAL FORM # →‎ته‎→ + +# تج ﰋ ﲡ + (‎ تج ‎) 062A 062C ARABIC LETTER TEH, ARABIC LETTER JEEM +← (‎ ﰋ ‎) FC0B ARABIC LIGATURE TEH WITH JEEM ISOLATED FORM +← (‎ ﲡ ‎) FCA1 ARABIC LIGATURE TEH WITH JEEM INITIAL FORM + +# تجم ﵐ + (‎ تجم ‎) 062A 062C 0645 ARABIC LETTER TEH, ARABIC LETTER JEEM, ARABIC LETTER MEEM +← (‎ ﵐ ‎) FD50 ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM + +# تجى تجي ﶟ ﶠ + (‎ تجى ‎) 062A 062C 0649 ARABIC LETTER TEH, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ تجي ‎) 062A 062C 064A ARABIC LETTER TEH, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﶟ ‎) FD9F ARABIC LIGATURE TEH WITH JEEM WITH YEH FINAL FORM # →‎تجي‎→ +← (‎ ﶠ ‎) FDA0 ARABIC LIGATURE TEH WITH JEEM WITH ALEF MAKSURA FINAL FORM + +# تح ﰌ ﲢ + (‎ تح ‎) 062A 062D ARABIC LETTER TEH, ARABIC LETTER HAH +← (‎ ﰌ ‎) FC0C ARABIC LIGATURE TEH WITH HAH ISOLATED FORM +← (‎ ﲢ ‎) FCA2 ARABIC LIGATURE TEH WITH HAH INITIAL FORM + +# تحج ﵑ ﵒ + (‎ تحج ‎) 062A 062D 062C ARABIC LETTER TEH, ARABIC LETTER HAH, ARABIC LETTER JEEM +← (‎ ﵑ ‎) FD51 ARABIC LIGATURE TEH WITH HAH WITH JEEM FINAL FORM +← (‎ ﵒ ‎) FD52 ARABIC LIGATURE TEH WITH HAH WITH JEEM INITIAL FORM + +# تحم ﵓ + (‎ تحم ‎) 062A 062D 0645 ARABIC LETTER TEH, ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ ﵓ ‎) FD53 ARABIC LIGATURE TEH WITH HAH WITH MEEM INITIAL FORM + +# تخ ﰍ ﲣ + (‎ تخ ‎) 062A 062E ARABIC LETTER TEH, ARABIC LETTER KHAH +← (‎ ﰍ ‎) FC0D ARABIC LIGATURE TEH WITH KHAH ISOLATED FORM +← (‎ ﲣ ‎) FCA3 ARABIC LIGATURE TEH WITH KHAH INITIAL FORM + +# تخم ﵔ + (‎ تخم ‎) 062A 062E 0645 ARABIC LETTER TEH, ARABIC LETTER KHAH, ARABIC LETTER MEEM +← (‎ ﵔ ‎) FD54 ARABIC LIGATURE TEH WITH KHAH WITH MEEM INITIAL FORM + +# تخى تخي ﶡ ﶢ + (‎ تخى ‎) 062A 062E 0649 ARABIC LETTER TEH, ARABIC LETTER KHAH, ARABIC LETTER ALEF MAKSURA +← (‎ تخي ‎) 062A 062E 064A ARABIC LETTER TEH, ARABIC LETTER KHAH, ARABIC LETTER YEH +← (‎ ﶡ ‎) FDA1 ARABIC LIGATURE TEH WITH KHAH WITH YEH FINAL FORM # →‎تخي‎→ +← (‎ ﶢ ‎) FDA2 ARABIC LIGATURE TEH WITH KHAH WITH ALEF MAKSURA FINAL FORM + +# تر ﱰ + (‎ تر ‎) 062A 0631 ARABIC LETTER TEH, ARABIC LETTER REH +← (‎ ﱰ ‎) FC70 ARABIC LIGATURE TEH WITH REH FINAL FORM + +# تز ﱱ + (‎ تز ‎) 062A 0632 ARABIC LETTER TEH, ARABIC LETTER ZAIN +← (‎ ﱱ ‎) FC71 ARABIC LIGATURE TEH WITH ZAIN FINAL FORM + +# تم ﰎ ﱲ ﲤ ﳣ + (‎ تم ‎) 062A 0645 ARABIC LETTER TEH, ARABIC LETTER MEEM +← (‎ ﰎ ‎) FC0E ARABIC LIGATURE TEH WITH MEEM ISOLATED FORM +← (‎ ﱲ ‎) FC72 ARABIC LIGATURE TEH WITH MEEM FINAL FORM +← (‎ ﲤ ‎) FCA4 ARABIC LIGATURE TEH WITH MEEM INITIAL FORM +← (‎ ﳣ ‎) FCE3 ARABIC LIGATURE TEH WITH MEEM MEDIAL FORM + +# تمج ﵕ + (‎ تمج ‎) 062A 0645 062C ARABIC LETTER TEH, ARABIC LETTER MEEM, ARABIC LETTER JEEM +← (‎ ﵕ ‎) FD55 ARABIC LIGATURE TEH WITH MEEM WITH JEEM INITIAL FORM + +# تمح ﵖ + (‎ تمح ‎) 062A 0645 062D ARABIC LETTER TEH, ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﵖ ‎) FD56 ARABIC LIGATURE TEH WITH MEEM WITH HAH INITIAL FORM + +# تمخ ﵗ + (‎ تمخ ‎) 062A 0645 062E ARABIC LETTER TEH, ARABIC LETTER MEEM, ARABIC LETTER KHAH +← (‎ ﵗ ‎) FD57 ARABIC LIGATURE TEH WITH MEEM WITH KHAH INITIAL FORM + +# تمى تمي ﶣ ﶤ + (‎ تمى ‎) 062A 0645 0649 ARABIC LETTER TEH, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ تمي ‎) 062A 0645 064A ARABIC LETTER TEH, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶣ ‎) FDA3 ARABIC LIGATURE TEH WITH MEEM WITH YEH FINAL FORM # →‎تمي‎→ +← (‎ ﶤ ‎) FDA4 ARABIC LIGATURE TEH WITH MEEM WITH ALEF MAKSURA FINAL FORM + +# تن ﱳ + (‎ تن ‎) 062A 0646 ARABIC LETTER TEH, ARABIC LETTER NOON +← (‎ ﱳ ‎) FC73 ARABIC LIGATURE TEH WITH NOON FINAL FORM + +# تى تي ﰏ ﰐ ﱴ ﱵ + (‎ تى ‎) 062A 0649 ARABIC LETTER TEH, ARABIC LETTER ALEF MAKSURA +← (‎ تي ‎) 062A 064A ARABIC LETTER TEH, ARABIC LETTER YEH +← (‎ ﰏ ‎) FC0F ARABIC LIGATURE TEH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰐ ‎) FC10 ARABIC LIGATURE TEH WITH YEH ISOLATED FORM # →‎تي‎→ +← (‎ ﱴ ‎) FC74 ARABIC LIGATURE TEH WITH ALEF MAKSURA FINAL FORM +← (‎ ﱵ ‎) FC75 ARABIC LIGATURE TEH WITH YEH FINAL FORM # →‎تي‎→ + +# ىۛ ٮۛ ںۛ یۛ ث ؿ پ ڽ ۑ 𞸖 𞸶 𞹶 𞺖 𞺶 ﭖ ﭗ ﭘ ﭙ ﺙ ﺚ ﺛ ﺜ + (‎ ث ‎) 062B ARABIC LETTER THEH +← (‎ ىۛ ‎) 0649 06DB ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS # →‎ٮۛ‎→ +← (‎ ٮۛ ‎) 066E 06DB ARABIC LETTER DOTLESS BEH, ARABIC SMALL HIGH THREE DOTS +← (‎ ںۛ ‎) 06BA 06DB ARABIC LETTER NOON GHUNNA, ARABIC SMALL HIGH THREE DOTS # →‎ىۛ‎→→‎ٮۛ‎→ +← (‎ یۛ ‎) 06CC 06DB ARABIC LETTER FARSI YEH, ARABIC SMALL HIGH THREE DOTS # →‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ؿ ‎) 063F ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE # →‎یۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ پ ‎) 067E ARABIC LETTER PEH # →‎ڽ‎→→‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ڽ ‎) 06BD ARABIC LETTER NOON WITH THREE DOTS ABOVE # →‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ۑ ‎) 06D1 ARABIC LETTER YEH WITH THREE DOTS BELOW # →‎پ‎→→‎ڽ‎→→‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ 𞸖 ‎) 1EE16 ARABIC MATHEMATICAL THEH +← (‎ 𞸶 ‎) 1EE36 ARABIC MATHEMATICAL INITIAL THEH +← (‎ 𞹶 ‎) 1EE76 ARABIC MATHEMATICAL STRETCHED THEH +← (‎ 𞺖 ‎) 1EE96 ARABIC MATHEMATICAL LOOPED THEH +← (‎ 𞺶 ‎) 1EEB6 ARABIC MATHEMATICAL DOUBLE-STRUCK THEH +← (‎ ﭖ ‎) FB56 ARABIC LETTER PEH ISOLATED FORM # →‎پ‎→→‎ڽ‎→→‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ﭗ ‎) FB57 ARABIC LETTER PEH FINAL FORM # →‎پ‎→→‎ڽ‎→→‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ﭘ ‎) FB58 ARABIC LETTER PEH INITIAL FORM # →‎پ‎→→‎ڽ‎→→‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ﭙ ‎) FB59 ARABIC LETTER PEH MEDIAL FORM # →‎پ‎→→‎ڽ‎→→‎ںۛ‎→→‎ىۛ‎→→‎ٮۛ‎→ +← (‎ ﺙ ‎) FE99 ARABIC LETTER THEH ISOLATED FORM +← (‎ ﺚ ‎) FE9A ARABIC LETTER THEH FINAL FORM +← (‎ ﺛ ‎) FE9B ARABIC LETTER THEH INITIAL FORM +← (‎ ﺜ ‎) FE9C ARABIC LETTER THEH MEDIAL FORM + +# ىۛج ثج ﰑ + (‎ ثج ‎) 062B 062C ARABIC LETTER THEH, ARABIC LETTER JEEM +← (‎ ىۛج ‎) 0649 06DB 062C ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER JEEM +← (‎ ﰑ ‎) FC11 ARABIC LIGATURE THEH WITH JEEM ISOLATED FORM + +# ىۛر ثر ﱶ + (‎ ثر ‎) 062B 0631 ARABIC LETTER THEH, ARABIC LETTER REH +← (‎ ىۛر ‎) 0649 06DB 0631 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER REH +← (‎ ﱶ ‎) FC76 ARABIC LIGATURE THEH WITH REH FINAL FORM + +# ىۛز ثز ﱷ + (‎ ثز ‎) 062B 0632 ARABIC LETTER THEH, ARABIC LETTER ZAIN +← (‎ ىۛز ‎) 0649 06DB 0632 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER ZAIN +← (‎ ﱷ ‎) FC77 ARABIC LIGATURE THEH WITH ZAIN FINAL FORM + +# ىۛم ثم ﰒ ﱸ ﲦ ﳥ + (‎ ثم ‎) 062B 0645 ARABIC LETTER THEH, ARABIC LETTER MEEM +← (‎ ىۛم ‎) 0649 06DB 0645 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER MEEM +← (‎ ﰒ ‎) FC12 ARABIC LIGATURE THEH WITH MEEM ISOLATED FORM +← (‎ ﱸ ‎) FC78 ARABIC LIGATURE THEH WITH MEEM FINAL FORM +← (‎ ﲦ ‎) FCA6 ARABIC LIGATURE THEH WITH MEEM INITIAL FORM +← (‎ ﳥ ‎) FCE5 ARABIC LIGATURE THEH WITH MEEM MEDIAL FORM + +# ىۛن ثن ﱹ + (‎ ثن ‎) 062B 0646 ARABIC LETTER THEH, ARABIC LETTER NOON +← (‎ ىۛن ‎) 0649 06DB 0646 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER NOON +← (‎ ﱹ ‎) FC79 ARABIC LIGATURE THEH WITH NOON FINAL FORM + +# ىۛo ىۛه ثه ﳦ + (‎ ثه ‎) 062B 0647 ARABIC LETTER THEH, ARABIC LETTER HEH +← (‎ ىۛo ‎) 0649 06DB 006F ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, LATIN SMALL LETTER O +← (‎ ىۛه ‎) 0649 06DB 0647 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER HEH +← (‎ ﳦ ‎) FCE6 ARABIC LIGATURE THEH WITH HEH MEDIAL FORM + +# ىۛى ثى ثي ﰓ ﰔ ﱺ ﱻ + (‎ ثى ‎) 062B 0649 ARABIC LETTER THEH, ARABIC LETTER ALEF MAKSURA +← (‎ ىۛى ‎) 0649 06DB 0649 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER ALEF MAKSURA +← (‎ ثي ‎) 062B 064A ARABIC LETTER THEH, ARABIC LETTER YEH # →‎ىۛى‎→ +← (‎ ﰓ ‎) FC13 ARABIC LIGATURE THEH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰔ ‎) FC14 ARABIC LIGATURE THEH WITH YEH ISOLATED FORM # →‎ثي‎→→‎ىۛى‎→ +← (‎ ﱺ ‎) FC7A ARABIC LIGATURE THEH WITH ALEF MAKSURA FINAL FORM +← (‎ ﱻ ‎) FC7B ARABIC LIGATURE THEH WITH YEH FINAL FORM # →‎ثي‎→→‎ىۛى‎→ + +# ج 𞸂 𞸢 𞹂 𞹢 𞺂 𞺢 ﺝ ﺞ ﺟ ﺠ + (‎ ج ‎) 062C ARABIC LETTER JEEM +← (‎ 𞸂 ‎) 1EE02 ARABIC MATHEMATICAL JEEM +← (‎ 𞸢 ‎) 1EE22 ARABIC MATHEMATICAL INITIAL JEEM +← (‎ 𞹂 ‎) 1EE42 ARABIC MATHEMATICAL TAILED JEEM +← (‎ 𞹢 ‎) 1EE62 ARABIC MATHEMATICAL STRETCHED JEEM +← (‎ 𞺂 ‎) 1EE82 ARABIC MATHEMATICAL LOOPED JEEM +← (‎ 𞺢 ‎) 1EEA2 ARABIC MATHEMATICAL DOUBLE-STRUCK JEEM +← (‎ ﺝ ‎) FE9D ARABIC LETTER JEEM ISOLATED FORM +← (‎ ﺞ ‎) FE9E ARABIC LETTER JEEM FINAL FORM +← (‎ ﺟ ‎) FE9F ARABIC LETTER JEEM INITIAL FORM +← (‎ ﺠ ‎) FEA0 ARABIC LETTER JEEM MEDIAL FORM + +# جح ﰕ ﲧ + (‎ جح ‎) 062C 062D ARABIC LETTER JEEM, ARABIC LETTER HAH +← (‎ ﰕ ‎) FC15 ARABIC LIGATURE JEEM WITH HAH ISOLATED FORM +← (‎ ﲧ ‎) FCA7 ARABIC LIGATURE JEEM WITH HAH INITIAL FORM + +# جحى جحي ﶦ ﶾ + (‎ جحى ‎) 062C 062D 0649 ARABIC LETTER JEEM, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ جحي ‎) 062C 062D 064A ARABIC LETTER JEEM, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶦ ‎) FDA6 ARABIC LIGATURE JEEM WITH HAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﶾ ‎) FDBE ARABIC LIGATURE JEEM WITH HAH WITH YEH FINAL FORM # →‎جحي‎→ + +# جل جلlلo جل جلlله جل جل1له جل جلاله ﷻ + (‎ جل جل1له ‎) 062C 0644 0020 062C 0644 0031 0644 0647 ARABIC LETTER JEEM, ARABIC LETTER LAM, SPACE, ARABIC LETTER JEEM, ARABIC LETTER LAM, DIGIT ONE, ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ جل جلlلo ‎) 062C 0644 0020 062C 0644 006C 0644 006F ARABIC LETTER JEEM, ARABIC LETTER LAM, SPACE, ARABIC LETTER JEEM, ARABIC LETTER LAM, LATIN SMALL LETTER L, ARABIC LETTER LAM, LATIN SMALL LETTER O # →‎جل جلاله‎→ +← (‎ جل جلlله ‎) 062C 0644 0020 062C 0644 006C 0644 0647 ARABIC LETTER JEEM, ARABIC LETTER LAM, SPACE, ARABIC LETTER JEEM, ARABIC LETTER LAM, LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ جل جلاله ‎) 062C 0644 0020 062C 0644 0627 0644 0647 ARABIC LETTER JEEM, ARABIC LETTER LAM, SPACE, ARABIC LETTER JEEM, ARABIC LETTER LAM, ARABIC LETTER ALEF, ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ ﷻ ‎) FDFB ARABIC LIGATURE JALLAJALALOUHOU # →‎جل جلاله‎→ + +# جم ﰖ ﲨ + (‎ جم ‎) 062C 0645 ARABIC LETTER JEEM, ARABIC LETTER MEEM +← (‎ ﰖ ‎) FC16 ARABIC LIGATURE JEEM WITH MEEM ISOLATED FORM +← (‎ ﲨ ‎) FCA8 ARABIC LIGATURE JEEM WITH MEEM INITIAL FORM + +# جمح ﵘ ﵙ + (‎ جمح ‎) 062C 0645 062D ARABIC LETTER JEEM, ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﵘ ‎) FD58 ARABIC LIGATURE JEEM WITH MEEM WITH HAH FINAL FORM +← (‎ ﵙ ‎) FD59 ARABIC LIGATURE JEEM WITH MEEM WITH HAH INITIAL FORM + +# جمى جمي ﶥ ﶧ + (‎ جمى ‎) 062C 0645 0649 ARABIC LETTER JEEM, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ جمي ‎) 062C 0645 064A ARABIC LETTER JEEM, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶥ ‎) FDA5 ARABIC LIGATURE JEEM WITH MEEM WITH YEH FINAL FORM # →‎جمي‎→ +← (‎ ﶧ ‎) FDA7 ARABIC LIGATURE JEEM WITH MEEM WITH ALEF MAKSURA FINAL FORM + +# جى جي ﴁ ﴂ ﴝ ﴞ + (‎ جى ‎) 062C 0649 ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ جي ‎) 062C 064A ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﴁ ‎) FD01 ARABIC LIGATURE JEEM WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﴂ ‎) FD02 ARABIC LIGATURE JEEM WITH YEH ISOLATED FORM # →‎جي‎→ +← (‎ ﴝ ‎) FD1D ARABIC LIGATURE JEEM WITH ALEF MAKSURA FINAL FORM +← (‎ ﴞ ‎) FD1E ARABIC LIGATURE JEEM WITH YEH FINAL FORM # →‎جي‎→ + +# ح 𞸇 𞸧 𞹇 𞹧 𞺇 𞺧 ﺡ ﺢ ﺣ ﺤ + (‎ ح ‎) 062D ARABIC LETTER HAH +← (‎ 𞸇 ‎) 1EE07 ARABIC MATHEMATICAL HAH +← (‎ 𞸧 ‎) 1EE27 ARABIC MATHEMATICAL INITIAL HAH +← (‎ 𞹇 ‎) 1EE47 ARABIC MATHEMATICAL TAILED HAH +← (‎ 𞹧 ‎) 1EE67 ARABIC MATHEMATICAL STRETCHED HAH +← (‎ 𞺇 ‎) 1EE87 ARABIC MATHEMATICAL LOOPED HAH +← (‎ 𞺧 ‎) 1EEA7 ARABIC MATHEMATICAL DOUBLE-STRUCK HAH +← (‎ ﺡ ‎) FEA1 ARABIC LETTER HAH ISOLATED FORM +← (‎ ﺢ ‎) FEA2 ARABIC LETTER HAH FINAL FORM +← (‎ ﺣ ‎) FEA3 ARABIC LETTER HAH INITIAL FORM +← (‎ ﺤ ‎) FEA4 ARABIC LETTER HAH MEDIAL FORM + +# حج ﰗ ﲩ + (‎ حج ‎) 062D 062C ARABIC LETTER HAH, ARABIC LETTER JEEM +← (‎ ﰗ ‎) FC17 ARABIC LIGATURE HAH WITH JEEM ISOLATED FORM +← (‎ ﲩ ‎) FCA9 ARABIC LIGATURE HAH WITH JEEM INITIAL FORM + +# حجى حجي ﶿ + (‎ حجى ‎) 062D 062C 0649 ARABIC LETTER HAH, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ حجي ‎) 062D 062C 064A ARABIC LETTER HAH, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﶿ ‎) FDBF ARABIC LIGATURE HAH WITH JEEM WITH YEH FINAL FORM # →‎حجي‎→ + +# حم ﰘ ﲪ + (‎ حم ‎) 062D 0645 ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ ﰘ ‎) FC18 ARABIC LIGATURE HAH WITH MEEM ISOLATED FORM +← (‎ ﲪ ‎) FCAA ARABIC LIGATURE HAH WITH MEEM INITIAL FORM + +# حمى حمي ﵚ ﵛ + (‎ حمى ‎) 062D 0645 0649 ARABIC LETTER HAH, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ حمي ‎) 062D 0645 064A ARABIC LETTER HAH, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﵚ ‎) FD5A ARABIC LIGATURE HAH WITH MEEM WITH YEH FINAL FORM # →‎حمي‎→ +← (‎ ﵛ ‎) FD5B ARABIC LIGATURE HAH WITH MEEM WITH ALEF MAKSURA FINAL FORM + +# حى حي ﳿ ﴀ ﴛ ﴜ + (‎ حى ‎) 062D 0649 ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ حي ‎) 062D 064A ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﳿ ‎) FCFF ARABIC LIGATURE HAH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﴀ ‎) FD00 ARABIC LIGATURE HAH WITH YEH ISOLATED FORM # →‎حي‎→ +← (‎ ﴛ ‎) FD1B ARABIC LIGATURE HAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﴜ ‎) FD1C ARABIC LIGATURE HAH WITH YEH FINAL FORM # →‎حي‎→ + +# حٔ ځ ݲ + (‎ حٔ ‎) 062D 0654 ARABIC LETTER HAH, ARABIC HAMZA ABOVE +← (‎ ځ ‎) 0681 ARABIC LETTER HAH WITH HAMZA ABOVE +← (‎ ݲ ‎) 0772 ARABIC LETTER HAH WITH SMALL ARABIC LETTER TAH ABOVE + +# حۛ څ + (‎ حۛ ‎) 062D 06DB ARABIC LETTER HAH, ARABIC SMALL HIGH THREE DOTS +← (‎ څ ‎) 0685 ARABIC LETTER HAH WITH THREE DOTS ABOVE + +# خ 𞸗 𞸷 𞹗 𞹷 𞺗 𞺷 ﺥ ﺦ ﺧ ﺨ + (‎ خ ‎) 062E ARABIC LETTER KHAH +← (‎ 𞸗 ‎) 1EE17 ARABIC MATHEMATICAL KHAH +← (‎ 𞸷 ‎) 1EE37 ARABIC MATHEMATICAL INITIAL KHAH +← (‎ 𞹗 ‎) 1EE57 ARABIC MATHEMATICAL TAILED KHAH +← (‎ 𞹷 ‎) 1EE77 ARABIC MATHEMATICAL STRETCHED KHAH +← (‎ 𞺗 ‎) 1EE97 ARABIC MATHEMATICAL LOOPED KHAH +← (‎ 𞺷 ‎) 1EEB7 ARABIC MATHEMATICAL DOUBLE-STRUCK KHAH +← (‎ ﺥ ‎) FEA5 ARABIC LETTER KHAH ISOLATED FORM +← (‎ ﺦ ‎) FEA6 ARABIC LETTER KHAH FINAL FORM +← (‎ ﺧ ‎) FEA7 ARABIC LETTER KHAH INITIAL FORM +← (‎ ﺨ ‎) FEA8 ARABIC LETTER KHAH MEDIAL FORM + +# خج ﰙ ﲫ + (‎ خج ‎) 062E 062C ARABIC LETTER KHAH, ARABIC LETTER JEEM +← (‎ ﰙ ‎) FC19 ARABIC LIGATURE KHAH WITH JEEM ISOLATED FORM +← (‎ ﲫ ‎) FCAB ARABIC LIGATURE KHAH WITH JEEM INITIAL FORM + +# خح ﰚ + (‎ خح ‎) 062E 062D ARABIC LETTER KHAH, ARABIC LETTER HAH +← (‎ ﰚ ‎) FC1A ARABIC LIGATURE KHAH WITH HAH ISOLATED FORM + +# خم ﰛ ﲬ + (‎ خم ‎) 062E 0645 ARABIC LETTER KHAH, ARABIC LETTER MEEM +← (‎ ﰛ ‎) FC1B ARABIC LIGATURE KHAH WITH MEEM ISOLATED FORM +← (‎ ﲬ ‎) FCAC ARABIC LIGATURE KHAH WITH MEEM INITIAL FORM + +# خى خي ﴃ ﴄ ﴟ ﴠ + (‎ خى ‎) 062E 0649 ARABIC LETTER KHAH, ARABIC LETTER ALEF MAKSURA +← (‎ خي ‎) 062E 064A ARABIC LETTER KHAH, ARABIC LETTER YEH +← (‎ ﴃ ‎) FD03 ARABIC LIGATURE KHAH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﴄ ‎) FD04 ARABIC LIGATURE KHAH WITH YEH ISOLATED FORM # →‎خي‎→ +← (‎ ﴟ ‎) FD1F ARABIC LIGATURE KHAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﴠ ‎) FD20 ARABIC LIGATURE KHAH WITH YEH FINAL FORM # →‎خي‎→ + +# د 𐋡 𞸃 𞺃 𞺣 ﺩ ﺪ + (‎ د ‎) 062F ARABIC LETTER DAL +← (‎ 𐋡 ‎) 102E1 COPTIC EPACT DIGIT ONE +← (‎ 𞸃 ‎) 1EE03 ARABIC MATHEMATICAL DAL +← (‎ 𞺃 ‎) 1EE83 ARABIC MATHEMATICAL LOOPED DAL +← (‎ 𞺣 ‎) 1EEA3 ARABIC MATHEMATICAL DOUBLE-STRUCK DAL +← (‎ ﺩ ‎) FEA9 ARABIC LETTER DAL ISOLATED FORM +← (‎ ﺪ ‎) FEAA ARABIC LETTER DAL FINAL FORM + +# د̂ دٛ ۮ + (‎ د̂ ‎) 062F 0302 ARABIC LETTER DAL, COMBINING CIRCUMFLEX ACCENT +← (‎ دٛ ‎) 062F 065B ARABIC LETTER DAL, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ۮ ‎) 06EE ARABIC LETTER DAL WITH INVERTED V # →‎دٛ‎→ + +# د̤̣ د࣮࣭ ࢮ + (‎ د̤̣ ‎) 062F 0324 0323 ARABIC LETTER DAL, COMBINING DIAERESIS BELOW, COMBINING DOT BELOW +← (‎ د࣮࣭ ‎) 062F 08EE 08ED ARABIC LETTER DAL, ARABIC TONE TWO DOTS BELOW, ARABIC TONE ONE DOT BELOW +← (‎ ࢮ ‎) 08AE ARABIC LETTER DAL WITH THREE DOTS BELOW # →‎د࣮࣭‎→ + +# دؕ ڈ ﮈ ﮉ + (‎ دؕ ‎) 062F 0615 ARABIC LETTER DAL, ARABIC SMALL HIGH TAH +← (‎ ڈ ‎) 0688 ARABIC LETTER DDAL +← (‎ ﮈ ‎) FB88 ARABIC LETTER DDAL ISOLATED FORM # →‎ڈ‎→ +← (‎ ﮉ ‎) FB89 ARABIC LETTER DDAL FINAL FORM # →‎ڈ‎→ + +# دۛ ڎ ﮆ ﮇ + (‎ دۛ ‎) 062F 06DB ARABIC LETTER DAL, ARABIC SMALL HIGH THREE DOTS +← (‎ ڎ ‎) 068E ARABIC LETTER DUL +← (‎ ﮆ ‎) FB86 ARABIC LETTER DUL ISOLATED FORM # →‎ڎ‎→ +← (‎ ﮇ ‎) FB87 ARABIC LETTER DUL FINAL FORM # →‎ڎ‎→ + +# ذ 𞸘 𞺘 𞺸 ﺫ ﺬ + (‎ ذ ‎) 0630 ARABIC LETTER THAL +← (‎ 𞸘 ‎) 1EE18 ARABIC MATHEMATICAL THAL +← (‎ 𞺘 ‎) 1EE98 ARABIC MATHEMATICAL LOOPED THAL +← (‎ 𞺸 ‎) 1EEB8 ARABIC MATHEMATICAL DOUBLE-STRUCK THAL +← (‎ ﺫ ‎) FEAB ARABIC LETTER THAL ISOLATED FORM +← (‎ ﺬ ‎) FEAC ARABIC LETTER THAL FINAL FORM + +# ذٰ ﱛ + (‎ ذٰ ‎) 0630 0670 ARABIC LETTER THAL, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ﱛ ‎) FC5B ARABIC LIGATURE THAL WITH SUPERSCRIPT ALEF ISOLATED FORM + +# ر 𞸓 𞺓 𞺳 ﺭ ﺮ + (‎ ر ‎) 0631 ARABIC LETTER REH +← (‎ 𞸓 ‎) 1EE13 ARABIC MATHEMATICAL REH +← (‎ 𞺓 ‎) 1EE93 ARABIC MATHEMATICAL LOOPED REH +← (‎ 𞺳 ‎) 1EEB3 ARABIC MATHEMATICAL DOUBLE-STRUCK REH +← (‎ ﺭ ‎) FEAD ARABIC LETTER REH ISOLATED FORM +← (‎ ﺮ ‎) FEAE ARABIC LETTER REH FINAL FORM + +# ر̂ رٛ ۯ + (‎ ر̂ ‎) 0631 0302 ARABIC LETTER REH, COMBINING CIRCUMFLEX ACCENT +← (‎ رٛ ‎) 0631 065B ARABIC LETTER REH, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ۯ ‎) 06EF ARABIC LETTER REH WITH INVERTED V # →‎رٛ‎→ + +# ر̆ رٚ ڒ + (‎ ر̆ ‎) 0631 0306 ARABIC LETTER REH, COMBINING BREVE +← (‎ رٚ ‎) 0631 065A ARABIC LETTER REH, ARABIC VOWEL SIGN SMALL V ABOVE +← (‎ ڒ ‎) 0692 ARABIC LETTER REH WITH SMALL V # →‎رٚ‎→ + +# ر̆̇ رۨ ࢹ + (‎ ر̆̇ ‎) 0631 0306 0307 ARABIC LETTER REH, COMBINING BREVE, COMBINING DOT ABOVE +← (‎ رۨ ‎) 0631 06E8 ARABIC LETTER REH, ARABIC SMALL HIGH NOON +← (‎ ࢹ ‎) 08B9 ARABIC LETTER REH WITH SMALL NOON ABOVE # →‎رۨ‎→ + +# رؕ ڑ ﮌ ﮍ + (‎ رؕ ‎) 0631 0615 ARABIC LETTER REH, ARABIC SMALL HIGH TAH +← (‎ ڑ ‎) 0691 ARABIC LETTER RREH +← (‎ ﮌ ‎) FB8C ARABIC LETTER RREH ISOLATED FORM # →‎ڑ‎→ +← (‎ ﮍ ‎) FB8D ARABIC LETTER RREH FINAL FORM # →‎ڑ‎→ + +# رسول ﷶ + (‎ رسول ‎) 0631 0633 0648 0644 ARABIC LETTER REH, ARABIC LETTER SEEN, ARABIC LETTER WAW, ARABIC LETTER LAM +← (‎ ﷶ ‎) FDF6 ARABIC LIGATURE RASOUL ISOLATED FORM + +# رىlل رى1ل ریال ﷼ + (‎ رى1ل ‎) 0631 0649 0031 0644 ARABIC LETTER REH, ARABIC LETTER ALEF MAKSURA, DIGIT ONE, ARABIC LETTER LAM +← (‎ رىlل ‎) 0631 0649 006C 0644 ARABIC LETTER REH, ARABIC LETTER ALEF MAKSURA, LATIN SMALL LETTER L, ARABIC LETTER LAM # →‎ریال‎→ +← (‎ ریال ‎) 0631 06CC 0627 0644 ARABIC LETTER REH, ARABIC LETTER FARSI YEH, ARABIC LETTER ALEF, ARABIC LETTER LAM +← (‎ ﷼ ‎) FDFC RIAL SIGN # →‎ریال‎→ + +# رٔ ݬ + (‎ رٔ ‎) 0631 0654 ARABIC LETTER REH, ARABIC HAMZA ABOVE +← (‎ ݬ ‎) 076C ARABIC LETTER REH WITH HAMZA ABOVE + +# رٰ ﱜ + (‎ رٰ ‎) 0631 0670 ARABIC LETTER REH, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ﱜ ‎) FC5C ARABIC LIGATURE REH WITH SUPERSCRIPT ALEF ISOLATED FORM + +# رۛ ژ ﮊ ﮋ + (‎ رۛ ‎) 0631 06DB ARABIC LETTER REH, ARABIC SMALL HIGH THREE DOTS +← (‎ ژ ‎) 0698 ARABIC LETTER JEH +← (‎ ﮊ ‎) FB8A ARABIC LETTER JEH ISOLATED FORM # →‎ژ‎→ +← (‎ ﮋ ‎) FB8B ARABIC LETTER JEH FINAL FORM # →‎ژ‎→ + +# ز 𞸆 𞺆 𞺦 ﺯ ﺰ + (‎ ز ‎) 0632 ARABIC LETTER ZAIN +← (‎ 𞸆 ‎) 1EE06 ARABIC MATHEMATICAL ZAIN +← (‎ 𞺆 ‎) 1EE86 ARABIC MATHEMATICAL LOOPED ZAIN +← (‎ 𞺦 ‎) 1EEA6 ARABIC MATHEMATICAL DOUBLE-STRUCK ZAIN +← (‎ ﺯ ‎) FEAF ARABIC LETTER ZAIN ISOLATED FORM +← (‎ ﺰ ‎) FEB0 ARABIC LETTER ZAIN FINAL FORM + +# ز̂ زٛ ࢲ + (‎ ز̂ ‎) 0632 0302 ARABIC LETTER ZAIN, COMBINING CIRCUMFLEX ACCENT +← (‎ زٛ ‎) 0632 065B ARABIC LETTER ZAIN, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ࢲ ‎) 08B2 ARABIC LETTER ZAIN WITH INVERTED V ABOVE # →‎زٛ‎→ + +# س 𞸎 𞸮 𞹎 𞹮 𞺎 𞺮 ﺱ ﺲ ﺳ ﺴ + (‎ س ‎) 0633 ARABIC LETTER SEEN +← (‎ 𞸎 ‎) 1EE0E ARABIC MATHEMATICAL SEEN +← (‎ 𞸮 ‎) 1EE2E ARABIC MATHEMATICAL INITIAL SEEN +← (‎ 𞹎 ‎) 1EE4E ARABIC MATHEMATICAL TAILED SEEN +← (‎ 𞹮 ‎) 1EE6E ARABIC MATHEMATICAL STRETCHED SEEN +← (‎ 𞺎 ‎) 1EE8E ARABIC MATHEMATICAL LOOPED SEEN +← (‎ 𞺮 ‎) 1EEAE ARABIC MATHEMATICAL DOUBLE-STRUCK SEEN +← (‎ ﺱ ‎) FEB1 ARABIC LETTER SEEN ISOLATED FORM +← (‎ ﺲ ‎) FEB2 ARABIC LETTER SEEN FINAL FORM +← (‎ ﺳ ‎) FEB3 ARABIC LETTER SEEN INITIAL FORM +← (‎ ﺴ ‎) FEB4 ARABIC LETTER SEEN MEDIAL FORM + +# سo سه ﳨ ﴱ + (‎ سo ‎) 0633 006F ARABIC LETTER SEEN, LATIN SMALL LETTER O +← (‎ سه ‎) 0633 0647 ARABIC LETTER SEEN, ARABIC LETTER HEH +← (‎ ﳨ ‎) FCE8 ARABIC LIGATURE SEEN WITH HEH MEDIAL FORM # →‎سه‎→ +← (‎ ﴱ ‎) FD31 ARABIC LIGATURE SEEN WITH HEH INITIAL FORM # →‎سه‎→ + +# س̂ سٛ ݾ + (‎ س̂ ‎) 0633 0302 ARABIC LETTER SEEN, COMBINING CIRCUMFLEX ACCENT +← (‎ سٛ ‎) 0633 065B ARABIC LETTER SEEN, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ݾ ‎) 077E ARABIC LETTER SEEN WITH INVERTED V # →‎سٛ‎→ + +# سج ﰜ ﲭ ﴴ + (‎ سج ‎) 0633 062C ARABIC LETTER SEEN, ARABIC LETTER JEEM +← (‎ ﰜ ‎) FC1C ARABIC LIGATURE SEEN WITH JEEM ISOLATED FORM +← (‎ ﲭ ‎) FCAD ARABIC LIGATURE SEEN WITH JEEM INITIAL FORM +← (‎ ﴴ ‎) FD34 ARABIC LIGATURE SEEN WITH JEEM MEDIAL FORM + +# سجح ﵝ + (‎ سجح ‎) 0633 062C 062D ARABIC LETTER SEEN, ARABIC LETTER JEEM, ARABIC LETTER HAH +← (‎ ﵝ ‎) FD5D ARABIC LIGATURE SEEN WITH JEEM WITH HAH INITIAL FORM + +# سجى ﵞ + (‎ سجى ‎) 0633 062C 0649 ARABIC LETTER SEEN, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ ﵞ ‎) FD5E ARABIC LIGATURE SEEN WITH JEEM WITH ALEF MAKSURA FINAL FORM + +# سح ﰝ ﲮ ﴵ + (‎ سح ‎) 0633 062D ARABIC LETTER SEEN, ARABIC LETTER HAH +← (‎ ﰝ ‎) FC1D ARABIC LIGATURE SEEN WITH HAH ISOLATED FORM +← (‎ ﲮ ‎) FCAE ARABIC LIGATURE SEEN WITH HAH INITIAL FORM +← (‎ ﴵ ‎) FD35 ARABIC LIGATURE SEEN WITH HAH MEDIAL FORM + +# سحج ﵜ + (‎ سحج ‎) 0633 062D 062C ARABIC LETTER SEEN, ARABIC LETTER HAH, ARABIC LETTER JEEM +← (‎ ﵜ ‎) FD5C ARABIC LIGATURE SEEN WITH HAH WITH JEEM INITIAL FORM + +# سخ ﰞ ﲯ ﴶ + (‎ سخ ‎) 0633 062E ARABIC LETTER SEEN, ARABIC LETTER KHAH +← (‎ ﰞ ‎) FC1E ARABIC LIGATURE SEEN WITH KHAH ISOLATED FORM +← (‎ ﲯ ‎) FCAF ARABIC LIGATURE SEEN WITH KHAH INITIAL FORM +← (‎ ﴶ ‎) FD36 ARABIC LIGATURE SEEN WITH KHAH MEDIAL FORM + +# سخى سخي ﶨ ﷆ + (‎ سخى ‎) 0633 062E 0649 ARABIC LETTER SEEN, ARABIC LETTER KHAH, ARABIC LETTER ALEF MAKSURA +← (‎ سخي ‎) 0633 062E 064A ARABIC LETTER SEEN, ARABIC LETTER KHAH, ARABIC LETTER YEH +← (‎ ﶨ ‎) FDA8 ARABIC LIGATURE SEEN WITH KHAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﷆ ‎) FDC6 ARABIC LIGATURE SEEN WITH KHAH WITH YEH FINAL FORM # →‎سخي‎→ + +# سر ﴎ ﴪ + (‎ سر ‎) 0633 0631 ARABIC LETTER SEEN, ARABIC LETTER REH +← (‎ ﴎ ‎) FD0E ARABIC LIGATURE SEEN WITH REH ISOLATED FORM +← (‎ ﴪ ‎) FD2A ARABIC LIGATURE SEEN WITH REH FINAL FORM + +# سم ﰟ ﲰ ﳧ + (‎ سم ‎) 0633 0645 ARABIC LETTER SEEN, ARABIC LETTER MEEM +← (‎ ﰟ ‎) FC1F ARABIC LIGATURE SEEN WITH MEEM ISOLATED FORM +← (‎ ﲰ ‎) FCB0 ARABIC LIGATURE SEEN WITH MEEM INITIAL FORM +← (‎ ﳧ ‎) FCE7 ARABIC LIGATURE SEEN WITH MEEM MEDIAL FORM + +# سمج ﵡ + (‎ سمج ‎) 0633 0645 062C ARABIC LETTER SEEN, ARABIC LETTER MEEM, ARABIC LETTER JEEM +← (‎ ﵡ ‎) FD61 ARABIC LIGATURE SEEN WITH MEEM WITH JEEM INITIAL FORM + +# سمح ﵟ ﵠ + (‎ سمح ‎) 0633 0645 062D ARABIC LETTER SEEN, ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﵟ ‎) FD5F ARABIC LIGATURE SEEN WITH MEEM WITH HAH FINAL FORM +← (‎ ﵠ ‎) FD60 ARABIC LIGATURE SEEN WITH MEEM WITH HAH INITIAL FORM + +# سمم ﵢ ﵣ + (‎ سمم ‎) 0633 0645 0645 ARABIC LETTER SEEN, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵢ ‎) FD62 ARABIC LIGATURE SEEN WITH MEEM WITH MEEM FINAL FORM +← (‎ ﵣ ‎) FD63 ARABIC LIGATURE SEEN WITH MEEM WITH MEEM INITIAL FORM + +# سى سي ﳻ ﳼ ﴗ ﴘ + (‎ سى ‎) 0633 0649 ARABIC LETTER SEEN, ARABIC LETTER ALEF MAKSURA +← (‎ سي ‎) 0633 064A ARABIC LETTER SEEN, ARABIC LETTER YEH +← (‎ ﳻ ‎) FCFB ARABIC LIGATURE SEEN WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﳼ ‎) FCFC ARABIC LIGATURE SEEN WITH YEH ISOLATED FORM # →‎سي‎→ +← (‎ ﴗ ‎) FD17 ARABIC LIGATURE SEEN WITH ALEF MAKSURA FINAL FORM +← (‎ ﴘ ‎) FD18 ARABIC LIGATURE SEEN WITH YEH FINAL FORM # →‎سي‎→ + +# سۛ ش 𞸔 𞸴 𞹔 𞹴 𞺔 𞺴 ﺵ ﺶ ﺷ ﺸ + (‎ سۛ ‎) 0633 06DB ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS +← (‎ ش ‎) 0634 ARABIC LETTER SHEEN +← (‎ 𞸔 ‎) 1EE14 ARABIC MATHEMATICAL SHEEN # →‎ش‎→ +← (‎ 𞸴 ‎) 1EE34 ARABIC MATHEMATICAL INITIAL SHEEN # →‎ش‎→ +← (‎ 𞹔 ‎) 1EE54 ARABIC MATHEMATICAL TAILED SHEEN # →‎ش‎→ +← (‎ 𞹴 ‎) 1EE74 ARABIC MATHEMATICAL STRETCHED SHEEN # →‎ش‎→ +← (‎ 𞺔 ‎) 1EE94 ARABIC MATHEMATICAL LOOPED SHEEN # →‎ش‎→ +← (‎ 𞺴 ‎) 1EEB4 ARABIC MATHEMATICAL DOUBLE-STRUCK SHEEN # →‎ش‎→ +← (‎ ﺵ ‎) FEB5 ARABIC LETTER SHEEN ISOLATED FORM # →‎ش‎→ +← (‎ ﺶ ‎) FEB6 ARABIC LETTER SHEEN FINAL FORM # →‎ش‎→ +← (‎ ﺷ ‎) FEB7 ARABIC LETTER SHEEN INITIAL FORM # →‎ش‎→ +← (‎ ﺸ ‎) FEB8 ARABIC LETTER SHEEN MEDIAL FORM # →‎ش‎→ + +# سۛo سۛه شه ﳪ ﴲ + (‎ سۛo ‎) 0633 06DB 006F ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, LATIN SMALL LETTER O +← (‎ سۛه ‎) 0633 06DB 0647 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER HEH # →‎شه‎→ +← (‎ شه ‎) 0634 0647 ARABIC LETTER SHEEN, ARABIC LETTER HEH +← (‎ ﳪ ‎) FCEA ARABIC LIGATURE SHEEN WITH HEH MEDIAL FORM # →‎شه‎→ +← (‎ ﴲ ‎) FD32 ARABIC LIGATURE SHEEN WITH HEH INITIAL FORM # →‎شه‎→ + +# سۛج شج ﴉ ﴥ ﴭ ﴷ + (‎ سۛج ‎) 0633 06DB 062C ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER JEEM +← (‎ شج ‎) 0634 062C ARABIC LETTER SHEEN, ARABIC LETTER JEEM +← (‎ ﴉ ‎) FD09 ARABIC LIGATURE SHEEN WITH JEEM ISOLATED FORM # →‎شج‎→ +← (‎ ﴥ ‎) FD25 ARABIC LIGATURE SHEEN WITH JEEM FINAL FORM # →‎شج‎→ +← (‎ ﴭ ‎) FD2D ARABIC LIGATURE SHEEN WITH JEEM INITIAL FORM # →‎شج‎→ +← (‎ ﴷ ‎) FD37 ARABIC LIGATURE SHEEN WITH JEEM MEDIAL FORM # →‎شج‎→ + +# سۛجى شجي ﵩ + (‎ سۛجى ‎) 0633 06DB 062C 0649 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ شجي ‎) 0634 062C 064A ARABIC LETTER SHEEN, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﵩ ‎) FD69 ARABIC LIGATURE SHEEN WITH JEEM WITH YEH FINAL FORM # →‎شجي‎→ + +# سۛح شح ﴊ ﴦ ﴮ ﴸ + (‎ سۛح ‎) 0633 06DB 062D ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER HAH +← (‎ شح ‎) 0634 062D ARABIC LETTER SHEEN, ARABIC LETTER HAH +← (‎ ﴊ ‎) FD0A ARABIC LIGATURE SHEEN WITH HAH ISOLATED FORM # →‎شح‎→ +← (‎ ﴦ ‎) FD26 ARABIC LIGATURE SHEEN WITH HAH FINAL FORM # →‎شح‎→ +← (‎ ﴮ ‎) FD2E ARABIC LIGATURE SHEEN WITH HAH INITIAL FORM # →‎شح‎→ +← (‎ ﴸ ‎) FD38 ARABIC LIGATURE SHEEN WITH HAH MEDIAL FORM # →‎شح‎→ + +# سۛحم شحم ﵧ ﵨ + (‎ سۛحم ‎) 0633 06DB 062D 0645 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ شحم ‎) 0634 062D 0645 ARABIC LETTER SHEEN, ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ ﵧ ‎) FD67 ARABIC LIGATURE SHEEN WITH HAH WITH MEEM FINAL FORM # →‎شحم‎→ +← (‎ ﵨ ‎) FD68 ARABIC LIGATURE SHEEN WITH HAH WITH MEEM INITIAL FORM # →‎شحم‎→ + +# سۛحى شحي ﶪ + (‎ سۛحى ‎) 0633 06DB 062D 0649 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ شحي ‎) 0634 062D 064A ARABIC LETTER SHEEN, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶪ ‎) FDAA ARABIC LIGATURE SHEEN WITH HAH WITH YEH FINAL FORM # →‎شحي‎→ + +# سۛخ شخ ﴋ ﴧ ﴯ ﴹ + (‎ سۛخ ‎) 0633 06DB 062E ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER KHAH +← (‎ شخ ‎) 0634 062E ARABIC LETTER SHEEN, ARABIC LETTER KHAH +← (‎ ﴋ ‎) FD0B ARABIC LIGATURE SHEEN WITH KHAH ISOLATED FORM # →‎شخ‎→ +← (‎ ﴧ ‎) FD27 ARABIC LIGATURE SHEEN WITH KHAH FINAL FORM # →‎شخ‎→ +← (‎ ﴯ ‎) FD2F ARABIC LIGATURE SHEEN WITH KHAH INITIAL FORM # →‎شخ‎→ +← (‎ ﴹ ‎) FD39 ARABIC LIGATURE SHEEN WITH KHAH MEDIAL FORM # →‎شخ‎→ + +# سۛر شر ﴍ ﴩ + (‎ سۛر ‎) 0633 06DB 0631 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER REH +← (‎ شر ‎) 0634 0631 ARABIC LETTER SHEEN, ARABIC LETTER REH +← (‎ ﴍ ‎) FD0D ARABIC LIGATURE SHEEN WITH REH ISOLATED FORM # →‎شر‎→ +← (‎ ﴩ ‎) FD29 ARABIC LIGATURE SHEEN WITH REH FINAL FORM # →‎شر‎→ + +# سۛم شم ﳩ ﴌ ﴨ ﴰ + (‎ سۛم ‎) 0633 06DB 0645 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER MEEM +← (‎ شم ‎) 0634 0645 ARABIC LETTER SHEEN, ARABIC LETTER MEEM +← (‎ ﳩ ‎) FCE9 ARABIC LIGATURE SHEEN WITH MEEM MEDIAL FORM # →‎شم‎→ +← (‎ ﴌ ‎) FD0C ARABIC LIGATURE SHEEN WITH MEEM ISOLATED FORM # →‎شم‎→ +← (‎ ﴨ ‎) FD28 ARABIC LIGATURE SHEEN WITH MEEM FINAL FORM # →‎شم‎→ +← (‎ ﴰ ‎) FD30 ARABIC LIGATURE SHEEN WITH MEEM INITIAL FORM # →‎شم‎→ + +# سۛمخ شمخ ﵪ ﵫ + (‎ سۛمخ ‎) 0633 06DB 0645 062E ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER MEEM, ARABIC LETTER KHAH +← (‎ شمخ ‎) 0634 0645 062E ARABIC LETTER SHEEN, ARABIC LETTER MEEM, ARABIC LETTER KHAH +← (‎ ﵪ ‎) FD6A ARABIC LIGATURE SHEEN WITH MEEM WITH KHAH FINAL FORM # →‎شمخ‎→ +← (‎ ﵫ ‎) FD6B ARABIC LIGATURE SHEEN WITH MEEM WITH KHAH INITIAL FORM # →‎شمخ‎→ + +# سۛمم شمم ﵬ ﵭ + (‎ سۛمم ‎) 0633 06DB 0645 0645 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ شمم ‎) 0634 0645 0645 ARABIC LETTER SHEEN, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵬ ‎) FD6C ARABIC LIGATURE SHEEN WITH MEEM WITH MEEM FINAL FORM # →‎شمم‎→ +← (‎ ﵭ ‎) FD6D ARABIC LIGATURE SHEEN WITH MEEM WITH MEEM INITIAL FORM # →‎شمم‎→ + +# سۛى شى شي ﳽ ﳾ ﴙ ﴚ + (‎ سۛى ‎) 0633 06DB 0649 ARABIC LETTER SEEN, ARABIC SMALL HIGH THREE DOTS, ARABIC LETTER ALEF MAKSURA +← (‎ شى ‎) 0634 0649 ARABIC LETTER SHEEN, ARABIC LETTER ALEF MAKSURA +← (‎ شي ‎) 0634 064A ARABIC LETTER SHEEN, ARABIC LETTER YEH +← (‎ ﳽ ‎) FCFD ARABIC LIGATURE SHEEN WITH ALEF MAKSURA ISOLATED FORM # →‎شى‎→ +← (‎ ﳾ ‎) FCFE ARABIC LIGATURE SHEEN WITH YEH ISOLATED FORM # →‎شي‎→ +← (‎ ﴙ ‎) FD19 ARABIC LIGATURE SHEEN WITH ALEF MAKSURA FINAL FORM # →‎شى‎→ +← (‎ ﴚ ‎) FD1A ARABIC LIGATURE SHEEN WITH YEH FINAL FORM # →‎شي‎→ + +# ص 𐋲 𞸑 𞸱 𞹑 𞹱 𞺑 𞺱 ﺹ ﺺ ﺻ ﺼ + (‎ ص ‎) 0635 ARABIC LETTER SAD +← (‎ 𐋲 ‎) 102F2 COPTIC EPACT NUMBER NINETY +← (‎ 𞸑 ‎) 1EE11 ARABIC MATHEMATICAL SAD +← (‎ 𞸱 ‎) 1EE31 ARABIC MATHEMATICAL INITIAL SAD +← (‎ 𞹑 ‎) 1EE51 ARABIC MATHEMATICAL TAILED SAD +← (‎ 𞹱 ‎) 1EE71 ARABIC MATHEMATICAL STRETCHED SAD +← (‎ 𞺑 ‎) 1EE91 ARABIC MATHEMATICAL LOOPED SAD +← (‎ 𞺱 ‎) 1EEB1 ARABIC MATHEMATICAL DOUBLE-STRUCK SAD +← (‎ ﺹ ‎) FEB9 ARABIC LETTER SAD ISOLATED FORM +← (‎ ﺺ ‎) FEBA ARABIC LETTER SAD FINAL FORM +← (‎ ﺻ ‎) FEBB ARABIC LETTER SAD INITIAL FORM +← (‎ ﺼ ‎) FEBC ARABIC LETTER SAD MEDIAL FORM + +# ص̤̣ ص࣮࣭ ࢯ + (‎ ص̤̣ ‎) 0635 0324 0323 ARABIC LETTER SAD, COMBINING DIAERESIS BELOW, COMBINING DOT BELOW +← (‎ ص࣮࣭ ‎) 0635 08EE 08ED ARABIC LETTER SAD, ARABIC TONE TWO DOTS BELOW, ARABIC TONE ONE DOT BELOW +← (‎ ࢯ ‎) 08AF ARABIC LETTER SAD WITH THREE DOTS BELOW # →‎ص࣮࣭‎→ + +# صح ﰠ ﲱ + (‎ صح ‎) 0635 062D ARABIC LETTER SAD, ARABIC LETTER HAH +← (‎ ﰠ ‎) FC20 ARABIC LIGATURE SAD WITH HAH ISOLATED FORM +← (‎ ﲱ ‎) FCB1 ARABIC LIGATURE SAD WITH HAH INITIAL FORM + +# صحح ﵤ ﵥ + (‎ صحح ‎) 0635 062D 062D ARABIC LETTER SAD, ARABIC LETTER HAH, ARABIC LETTER HAH +← (‎ ﵤ ‎) FD64 ARABIC LIGATURE SAD WITH HAH WITH HAH FINAL FORM +← (‎ ﵥ ‎) FD65 ARABIC LIGATURE SAD WITH HAH WITH HAH INITIAL FORM + +# صحى صحي ﶩ + (‎ صحى ‎) 0635 062D 0649 ARABIC LETTER SAD, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ صحي ‎) 0635 062D 064A ARABIC LETTER SAD, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶩ ‎) FDA9 ARABIC LIGATURE SAD WITH HAH WITH YEH FINAL FORM # →‎صحي‎→ + +# صخ ﲲ + (‎ صخ ‎) 0635 062E ARABIC LETTER SAD, ARABIC LETTER KHAH +← (‎ ﲲ ‎) FCB2 ARABIC LIGATURE SAD WITH KHAH INITIAL FORM + +# صر ﴏ ﴫ + (‎ صر ‎) 0635 0631 ARABIC LETTER SAD, ARABIC LETTER REH +← (‎ ﴏ ‎) FD0F ARABIC LIGATURE SAD WITH REH ISOLATED FORM +← (‎ ﴫ ‎) FD2B ARABIC LIGATURE SAD WITH REH FINAL FORM + +# صلعم ﷵ + (‎ صلعم ‎) 0635 0644 0639 0645 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER AIN, ARABIC LETTER MEEM +← (‎ ﷵ ‎) FDF5 ARABIC LIGATURE SALAM ISOLATED FORM + +# صلى صلے ﷰ ﷹ + (‎ صلى ‎) 0635 0644 0649 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA +← (‎ صلے ‎) 0635 0644 06D2 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER YEH BARREE +← (‎ ﷰ ‎) FDF0 ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM # →‎صلے‎→ +← (‎ ﷹ ‎) FDF9 ARABIC LIGATURE SALLA ISOLATED FORM + +# صلى lللo علىo وسلم صلى lلله علىه وسلم صلى 1لله علىه وسلم صلى الله عليه وسلم ﷺ + (‎ صلى 1لله علىه وسلم ‎) 0635 0644 0649 0020 0031 0644 0644 0647 0020 0639 0644 0649 0647 0020 0648 0633 0644 0645 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, SPACE, DIGIT ONE, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC LETTER HEH, SPACE, ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HEH, SPACE, ARABIC LETTER WAW, ARABIC LETTER SEEN, ARABIC LETTER LAM, ARABIC LETTER MEEM +← (‎ صلى lللo علىo وسلم ‎) 0635 0644 0649 0020 006C 0644 0644 006F 0020 0639 0644 0649 006F 0020 0648 0633 0644 0645 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, SPACE, LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, LATIN SMALL LETTER O, SPACE, ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, LATIN SMALL LETTER O, SPACE, ARABIC LETTER WAW, ARABIC LETTER SEEN, ARABIC LETTER LAM, ARABIC LETTER MEEM # →‎صلى الله عليه وسلم‎→ +← (‎ صلى lلله علىه وسلم ‎) 0635 0644 0649 0020 006C 0644 0644 0647 0020 0639 0644 0649 0647 0020 0648 0633 0644 0645 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, SPACE, LATIN SMALL LETTER L, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC LETTER HEH, SPACE, ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HEH, SPACE, ARABIC LETTER WAW, ARABIC LETTER SEEN, ARABIC LETTER LAM, ARABIC LETTER MEEM +← (‎ صلى الله عليه وسلم ‎) 0635 0644 0649 0020 0627 0644 0644 0647 0020 0639 0644 064A 0647 0020 0648 0633 0644 0645 ARABIC LETTER SAD, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, SPACE, ARABIC LETTER ALEF, ARABIC LETTER LAM, ARABIC LETTER LAM, ARABIC LETTER HEH, SPACE, ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER YEH, ARABIC LETTER HEH, SPACE, ARABIC LETTER WAW, ARABIC LETTER SEEN, ARABIC LETTER LAM, ARABIC LETTER MEEM +← (‎ ﷺ ‎) FDFA ARABIC LIGATURE SALLALLAHOU ALAYHE WASALLAM # →‎صلى الله عليه وسلم‎→ + +# صم ﰡ ﲳ + (‎ صم ‎) 0635 0645 ARABIC LETTER SAD, ARABIC LETTER MEEM +← (‎ ﰡ ‎) FC21 ARABIC LIGATURE SAD WITH MEEM ISOLATED FORM +← (‎ ﲳ ‎) FCB3 ARABIC LIGATURE SAD WITH MEEM INITIAL FORM + +# صمم ﵦ ﷅ + (‎ صمم ‎) 0635 0645 0645 ARABIC LETTER SAD, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵦ ‎) FD66 ARABIC LIGATURE SAD WITH MEEM WITH MEEM FINAL FORM +← (‎ ﷅ ‎) FDC5 ARABIC LIGATURE SAD WITH MEEM WITH MEEM INITIAL FORM + +# صى صي ﴅ ﴆ ﴡ ﴢ + (‎ صى ‎) 0635 0649 ARABIC LETTER SAD, ARABIC LETTER ALEF MAKSURA +← (‎ صي ‎) 0635 064A ARABIC LETTER SAD, ARABIC LETTER YEH +← (‎ ﴅ ‎) FD05 ARABIC LIGATURE SAD WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﴆ ‎) FD06 ARABIC LIGATURE SAD WITH YEH ISOLATED FORM # →‎صي‎→ +← (‎ ﴡ ‎) FD21 ARABIC LIGATURE SAD WITH ALEF MAKSURA FINAL FORM +← (‎ ﴢ ‎) FD22 ARABIC LIGATURE SAD WITH YEH FINAL FORM # →‎صي‎→ + +# صۛ ڞ + (‎ صۛ ‎) 0635 06DB ARABIC LETTER SAD, ARABIC SMALL HIGH THREE DOTS +← (‎ ڞ ‎) 069E ARABIC LETTER SAD WITH THREE DOTS ABOVE + +# ض 𞸙 𞸹 𞹙 𞹹 𞺙 𞺹 ﺽ ﺾ ﺿ ﻀ + (‎ ض ‎) 0636 ARABIC LETTER DAD +← (‎ 𞸙 ‎) 1EE19 ARABIC MATHEMATICAL DAD +← (‎ 𞸹 ‎) 1EE39 ARABIC MATHEMATICAL INITIAL DAD +← (‎ 𞹙 ‎) 1EE59 ARABIC MATHEMATICAL TAILED DAD +← (‎ 𞹹 ‎) 1EE79 ARABIC MATHEMATICAL STRETCHED DAD +← (‎ 𞺙 ‎) 1EE99 ARABIC MATHEMATICAL LOOPED DAD +← (‎ 𞺹 ‎) 1EEB9 ARABIC MATHEMATICAL DOUBLE-STRUCK DAD +← (‎ ﺽ ‎) FEBD ARABIC LETTER DAD ISOLATED FORM +← (‎ ﺾ ‎) FEBE ARABIC LETTER DAD FINAL FORM +← (‎ ﺿ ‎) FEBF ARABIC LETTER DAD INITIAL FORM +← (‎ ﻀ ‎) FEC0 ARABIC LETTER DAD MEDIAL FORM + +# ضج ﰢ ﲴ + (‎ ضج ‎) 0636 062C ARABIC LETTER DAD, ARABIC LETTER JEEM +← (‎ ﰢ ‎) FC22 ARABIC LIGATURE DAD WITH JEEM ISOLATED FORM +← (‎ ﲴ ‎) FCB4 ARABIC LIGATURE DAD WITH JEEM INITIAL FORM + +# ضح ﰣ ﲵ + (‎ ضح ‎) 0636 062D ARABIC LETTER DAD, ARABIC LETTER HAH +← (‎ ﰣ ‎) FC23 ARABIC LIGATURE DAD WITH HAH ISOLATED FORM +← (‎ ﲵ ‎) FCB5 ARABIC LIGATURE DAD WITH HAH INITIAL FORM + +# ضحى ضحي ﵮ ﶫ + (‎ ضحى ‎) 0636 062D 0649 ARABIC LETTER DAD, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ ضحي ‎) 0636 062D 064A ARABIC LETTER DAD, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﵮ ‎) FD6E ARABIC LIGATURE DAD WITH HAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﶫ ‎) FDAB ARABIC LIGATURE DAD WITH HAH WITH YEH FINAL FORM # →‎ضحي‎→ + +# ضخ ﰤ ﲶ + (‎ ضخ ‎) 0636 062E ARABIC LETTER DAD, ARABIC LETTER KHAH +← (‎ ﰤ ‎) FC24 ARABIC LIGATURE DAD WITH KHAH ISOLATED FORM +← (‎ ﲶ ‎) FCB6 ARABIC LIGATURE DAD WITH KHAH INITIAL FORM + +# ضخم ﵯ ﵰ + (‎ ضخم ‎) 0636 062E 0645 ARABIC LETTER DAD, ARABIC LETTER KHAH, ARABIC LETTER MEEM +← (‎ ﵯ ‎) FD6F ARABIC LIGATURE DAD WITH KHAH WITH MEEM FINAL FORM +← (‎ ﵰ ‎) FD70 ARABIC LIGATURE DAD WITH KHAH WITH MEEM INITIAL FORM + +# ضر ﴐ ﴬ + (‎ ضر ‎) 0636 0631 ARABIC LETTER DAD, ARABIC LETTER REH +← (‎ ﴐ ‎) FD10 ARABIC LIGATURE DAD WITH REH ISOLATED FORM +← (‎ ﴬ ‎) FD2C ARABIC LIGATURE DAD WITH REH FINAL FORM + +# ضم ﰥ ﲷ + (‎ ضم ‎) 0636 0645 ARABIC LETTER DAD, ARABIC LETTER MEEM +← (‎ ﰥ ‎) FC25 ARABIC LIGATURE DAD WITH MEEM ISOLATED FORM +← (‎ ﲷ ‎) FCB7 ARABIC LIGATURE DAD WITH MEEM INITIAL FORM + +# ضى ضي ﴇ ﴈ ﴣ ﴤ + (‎ ضى ‎) 0636 0649 ARABIC LETTER DAD, ARABIC LETTER ALEF MAKSURA +← (‎ ضي ‎) 0636 064A ARABIC LETTER DAD, ARABIC LETTER YEH +← (‎ ﴇ ‎) FD07 ARABIC LIGATURE DAD WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﴈ ‎) FD08 ARABIC LIGATURE DAD WITH YEH ISOLATED FORM # →‎ضي‎→ +← (‎ ﴣ ‎) FD23 ARABIC LIGATURE DAD WITH ALEF MAKSURA FINAL FORM +← (‎ ﴤ ‎) FD24 ARABIC LIGATURE DAD WITH YEH FINAL FORM # →‎ضي‎→ + +# ط 𐋨 𞸈 𞹨 𞺈 𞺨 ﻁ ﻂ ﻃ ﻄ + (‎ ط ‎) 0637 ARABIC LETTER TAH +← (‎ 𐋨 ‎) 102E8 COPTIC EPACT DIGIT EIGHT +← (‎ 𞸈 ‎) 1EE08 ARABIC MATHEMATICAL TAH +← (‎ 𞹨 ‎) 1EE68 ARABIC MATHEMATICAL STRETCHED TAH +← (‎ 𞺈 ‎) 1EE88 ARABIC MATHEMATICAL LOOPED TAH +← (‎ 𞺨 ‎) 1EEA8 ARABIC MATHEMATICAL DOUBLE-STRUCK TAH +← (‎ ﻁ ‎) FEC1 ARABIC LETTER TAH ISOLATED FORM +← (‎ ﻂ ‎) FEC2 ARABIC LETTER TAH FINAL FORM +← (‎ ﻃ ‎) FEC3 ARABIC LETTER TAH INITIAL FORM +← (‎ ﻄ ‎) FEC4 ARABIC LETTER TAH MEDIAL FORM + +# طح ﰦ ﲸ + (‎ طح ‎) 0637 062D ARABIC LETTER TAH, ARABIC LETTER HAH +← (‎ ﰦ ‎) FC26 ARABIC LIGATURE TAH WITH HAH ISOLATED FORM +← (‎ ﲸ ‎) FCB8 ARABIC LIGATURE TAH WITH HAH INITIAL FORM + +# طم ﰧ ﴳ ﴺ + (‎ طم ‎) 0637 0645 ARABIC LETTER TAH, ARABIC LETTER MEEM +← (‎ ﰧ ‎) FC27 ARABIC LIGATURE TAH WITH MEEM ISOLATED FORM +← (‎ ﴳ ‎) FD33 ARABIC LIGATURE TAH WITH MEEM INITIAL FORM +← (‎ ﴺ ‎) FD3A ARABIC LIGATURE TAH WITH MEEM MEDIAL FORM + +# طمح ﵱ ﵲ + (‎ طمح ‎) 0637 0645 062D ARABIC LETTER TAH, ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﵱ ‎) FD71 ARABIC LIGATURE TAH WITH MEEM WITH HAH FINAL FORM +← (‎ ﵲ ‎) FD72 ARABIC LIGATURE TAH WITH MEEM WITH HAH INITIAL FORM + +# طمم ﵳ + (‎ طمم ‎) 0637 0645 0645 ARABIC LETTER TAH, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵳ ‎) FD73 ARABIC LIGATURE TAH WITH MEEM WITH MEEM INITIAL FORM + +# طمى طمي ﵴ + (‎ طمى ‎) 0637 0645 0649 ARABIC LETTER TAH, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ طمي ‎) 0637 0645 064A ARABIC LETTER TAH, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﵴ ‎) FD74 ARABIC LIGATURE TAH WITH MEEM WITH YEH FINAL FORM # →‎طمي‎→ + +# طى طي ﳵ ﳶ ﴑ ﴒ + (‎ طى ‎) 0637 0649 ARABIC LETTER TAH, ARABIC LETTER ALEF MAKSURA +← (‎ طي ‎) 0637 064A ARABIC LETTER TAH, ARABIC LETTER YEH +← (‎ ﳵ ‎) FCF5 ARABIC LIGATURE TAH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﳶ ‎) FCF6 ARABIC LIGATURE TAH WITH YEH ISOLATED FORM # →‎طي‎→ +← (‎ ﴑ ‎) FD11 ARABIC LIGATURE TAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﴒ ‎) FD12 ARABIC LIGATURE TAH WITH YEH FINAL FORM # →‎طي‎→ + +# طۛ ڟ + (‎ طۛ ‎) 0637 06DB ARABIC LETTER TAH, ARABIC SMALL HIGH THREE DOTS +← (‎ ڟ ‎) 069F ARABIC LETTER TAH WITH THREE DOTS ABOVE + +# ظ 𞸚 𞹺 𞺚 𞺺 ﻅ ﻆ ﻇ ﻈ + (‎ ظ ‎) 0638 ARABIC LETTER ZAH +← (‎ 𞸚 ‎) 1EE1A ARABIC MATHEMATICAL ZAH +← (‎ 𞹺 ‎) 1EE7A ARABIC MATHEMATICAL STRETCHED ZAH +← (‎ 𞺚 ‎) 1EE9A ARABIC MATHEMATICAL LOOPED ZAH +← (‎ 𞺺 ‎) 1EEBA ARABIC MATHEMATICAL DOUBLE-STRUCK ZAH +← (‎ ﻅ ‎) FEC5 ARABIC LETTER ZAH ISOLATED FORM +← (‎ ﻆ ‎) FEC6 ARABIC LETTER ZAH FINAL FORM +← (‎ ﻇ ‎) FEC7 ARABIC LETTER ZAH INITIAL FORM +← (‎ ﻈ ‎) FEC8 ARABIC LETTER ZAH MEDIAL FORM + +# ظم ﰨ ﲹ ﴻ + (‎ ظم ‎) 0638 0645 ARABIC LETTER ZAH, ARABIC LETTER MEEM +← (‎ ﰨ ‎) FC28 ARABIC LIGATURE ZAH WITH MEEM ISOLATED FORM +← (‎ ﲹ ‎) FCB9 ARABIC LIGATURE ZAH WITH MEEM INITIAL FORM +← (‎ ﴻ ‎) FD3B ARABIC LIGATURE ZAH WITH MEEM MEDIAL FORM + +# عج ﰩ ﲺ + (‎ عج ‎) 0639 062C ARABIC LETTER AIN, ARABIC LETTER JEEM +← (‎ ﰩ ‎) FC29 ARABIC LIGATURE AIN WITH JEEM ISOLATED FORM +← (‎ ﲺ ‎) FCBA ARABIC LIGATURE AIN WITH JEEM INITIAL FORM + +# عجم ﵵ ﷄ + (‎ عجم ‎) 0639 062C 0645 ARABIC LETTER AIN, ARABIC LETTER JEEM, ARABIC LETTER MEEM +← (‎ ﵵ ‎) FD75 ARABIC LIGATURE AIN WITH JEEM WITH MEEM FINAL FORM +← (‎ ﷄ ‎) FDC4 ARABIC LIGATURE AIN WITH JEEM WITH MEEM INITIAL FORM + +# علىo علىه عليه ﷷ + (‎ علىo ‎) 0639 0644 0649 006F ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, LATIN SMALL LETTER O +← (‎ علىه ‎) 0639 0644 0649 0647 ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HEH # →‎عليه‎→ +← (‎ عليه ‎) 0639 0644 064A 0647 ARABIC LETTER AIN, ARABIC LETTER LAM, ARABIC LETTER YEH, ARABIC LETTER HEH +← (‎ ﷷ ‎) FDF7 ARABIC LIGATURE ALAYHE ISOLATED FORM # →‎عليه‎→ + +# عم ﰪ ﲻ + (‎ عم ‎) 0639 0645 ARABIC LETTER AIN, ARABIC LETTER MEEM +← (‎ ﰪ ‎) FC2A ARABIC LIGATURE AIN WITH MEEM ISOLATED FORM +← (‎ ﲻ ‎) FCBB ARABIC LIGATURE AIN WITH MEEM INITIAL FORM + +# عمم ﵶ ﵷ + (‎ عمم ‎) 0639 0645 0645 ARABIC LETTER AIN, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵶ ‎) FD76 ARABIC LIGATURE AIN WITH MEEM WITH MEEM FINAL FORM +← (‎ ﵷ ‎) FD77 ARABIC LIGATURE AIN WITH MEEM WITH MEEM INITIAL FORM + +# عمى عمي ﵸ ﶶ + (‎ عمى ‎) 0639 0645 0649 ARABIC LETTER AIN, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ عمي ‎) 0639 0645 064A ARABIC LETTER AIN, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﵸ ‎) FD78 ARABIC LIGATURE AIN WITH MEEM WITH ALEF MAKSURA FINAL FORM +← (‎ ﶶ ‎) FDB6 ARABIC LIGATURE AIN WITH MEEM WITH YEH FINAL FORM # →‎عمي‎→ + +# عى عي ﳷ ﳸ ﴓ ﴔ + (‎ عى ‎) 0639 0649 ARABIC LETTER AIN, ARABIC LETTER ALEF MAKSURA +← (‎ عي ‎) 0639 064A ARABIC LETTER AIN, ARABIC LETTER YEH +← (‎ ﳷ ‎) FCF7 ARABIC LIGATURE AIN WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﳸ ‎) FCF8 ARABIC LIGATURE AIN WITH YEH ISOLATED FORM # →‎عي‎→ +← (‎ ﴓ ‎) FD13 ARABIC LIGATURE AIN WITH ALEF MAKSURA FINAL FORM +← (‎ ﴔ ‎) FD14 ARABIC LIGATURE AIN WITH YEH FINAL FORM # →‎عي‎→ + +# غ 𞸛 𞸻 𞹛 𞹻 𞺛 𞺻 ﻍ ﻎ ﻏ ﻐ + (‎ غ ‎) 063A ARABIC LETTER GHAIN +← (‎ 𞸛 ‎) 1EE1B ARABIC MATHEMATICAL GHAIN +← (‎ 𞸻 ‎) 1EE3B ARABIC MATHEMATICAL INITIAL GHAIN +← (‎ 𞹛 ‎) 1EE5B ARABIC MATHEMATICAL TAILED GHAIN +← (‎ 𞹻 ‎) 1EE7B ARABIC MATHEMATICAL STRETCHED GHAIN +← (‎ 𞺛 ‎) 1EE9B ARABIC MATHEMATICAL LOOPED GHAIN +← (‎ 𞺻 ‎) 1EEBB ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN +← (‎ ﻍ ‎) FECD ARABIC LETTER GHAIN ISOLATED FORM +← (‎ ﻎ ‎) FECE ARABIC LETTER GHAIN FINAL FORM +← (‎ ﻏ ‎) FECF ARABIC LETTER GHAIN INITIAL FORM +← (‎ ﻐ ‎) FED0 ARABIC LETTER GHAIN MEDIAL FORM + +# غج ﰫ ﲼ + (‎ غج ‎) 063A 062C ARABIC LETTER GHAIN, ARABIC LETTER JEEM +← (‎ ﰫ ‎) FC2B ARABIC LIGATURE GHAIN WITH JEEM ISOLATED FORM +← (‎ ﲼ ‎) FCBC ARABIC LIGATURE GHAIN WITH JEEM INITIAL FORM + +# غم ﰬ ﲽ + (‎ غم ‎) 063A 0645 ARABIC LETTER GHAIN, ARABIC LETTER MEEM +← (‎ ﰬ ‎) FC2C ARABIC LIGATURE GHAIN WITH MEEM ISOLATED FORM +← (‎ ﲽ ‎) FCBD ARABIC LIGATURE GHAIN WITH MEEM INITIAL FORM + +# غمم ﵹ + (‎ غمم ‎) 063A 0645 0645 ARABIC LETTER GHAIN, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵹ ‎) FD79 ARABIC LIGATURE GHAIN WITH MEEM WITH MEEM FINAL FORM + +# غمى غمي ﵺ ﵻ + (‎ غمى ‎) 063A 0645 0649 ARABIC LETTER GHAIN, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ غمي ‎) 063A 0645 064A ARABIC LETTER GHAIN, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﵺ ‎) FD7A ARABIC LIGATURE GHAIN WITH MEEM WITH YEH FINAL FORM # →‎غمي‎→ +← (‎ ﵻ ‎) FD7B ARABIC LIGATURE GHAIN WITH MEEM WITH ALEF MAKSURA FINAL FORM + +# غى غي ﳹ ﳺ ﴕ ﴖ + (‎ غى ‎) 063A 0649 ARABIC LETTER GHAIN, ARABIC LETTER ALEF MAKSURA +← (‎ غي ‎) 063A 064A ARABIC LETTER GHAIN, ARABIC LETTER YEH +← (‎ ﳹ ‎) FCF9 ARABIC LIGATURE GHAIN WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﳺ ‎) FCFA ARABIC LIGATURE GHAIN WITH YEH ISOLATED FORM # →‎غي‎→ +← (‎ ﴕ ‎) FD15 ARABIC LIGATURE GHAIN WITH ALEF MAKSURA FINAL FORM +← (‎ ﴖ ‎) FD16 ARABIC LIGATURE GHAIN WITH YEH FINAL FORM # →‎غي‎→ + +# ى̂ یٛ ؽ + (‎ ؽ ‎) 063D ARABIC LETTER FARSI YEH WITH INVERTED V +← (‎ ى̂ ‎) 0649 0302 ARABIC LETTER ALEF MAKSURA, COMBINING CIRCUMFLEX ACCENT # →‎یٛ‎→ +← (‎ یٛ ‎) 06CC 065B ARABIC LETTER FARSI YEH, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE + +# ف ڧ 𞸐 𞸰 𞹰 𞺐 𞺰 ﻑ ﻒ ﻓ ﻔ + (‎ ف ‎) 0641 ARABIC LETTER FEH +← (‎ ڧ ‎) 06A7 ARABIC LETTER QAF WITH DOT ABOVE +← (‎ 𞸐 ‎) 1EE10 ARABIC MATHEMATICAL FEH +← (‎ 𞸰 ‎) 1EE30 ARABIC MATHEMATICAL INITIAL FEH +← (‎ 𞹰 ‎) 1EE70 ARABIC MATHEMATICAL STRETCHED FEH +← (‎ 𞺐 ‎) 1EE90 ARABIC MATHEMATICAL LOOPED FEH +← (‎ 𞺰 ‎) 1EEB0 ARABIC MATHEMATICAL DOUBLE-STRUCK FEH +← (‎ ﻑ ‎) FED1 ARABIC LETTER FEH ISOLATED FORM +← (‎ ﻒ ‎) FED2 ARABIC LETTER FEH FINAL FORM +← (‎ ﻓ ‎) FED3 ARABIC LETTER FEH INITIAL FORM +← (‎ ﻔ ‎) FED4 ARABIC LETTER FEH MEDIAL FORM + +# فج ﰭ ﲾ + (‎ فج ‎) 0641 062C ARABIC LETTER FEH, ARABIC LETTER JEEM +← (‎ ﰭ ‎) FC2D ARABIC LIGATURE FEH WITH JEEM ISOLATED FORM +← (‎ ﲾ ‎) FCBE ARABIC LIGATURE FEH WITH JEEM INITIAL FORM + +# فح ﰮ ﲿ + (‎ فح ‎) 0641 062D ARABIC LETTER FEH, ARABIC LETTER HAH +← (‎ ﰮ ‎) FC2E ARABIC LIGATURE FEH WITH HAH ISOLATED FORM +← (‎ ﲿ ‎) FCBF ARABIC LIGATURE FEH WITH HAH INITIAL FORM + +# فخ ﰯ ﳀ + (‎ فخ ‎) 0641 062E ARABIC LETTER FEH, ARABIC LETTER KHAH +← (‎ ﰯ ‎) FC2F ARABIC LIGATURE FEH WITH KHAH ISOLATED FORM +← (‎ ﳀ ‎) FCC0 ARABIC LIGATURE FEH WITH KHAH INITIAL FORM + +# فخم ﵼ ﵽ + (‎ فخم ‎) 0641 062E 0645 ARABIC LETTER FEH, ARABIC LETTER KHAH, ARABIC LETTER MEEM +← (‎ ﵼ ‎) FD7C ARABIC LIGATURE FEH WITH KHAH WITH MEEM FINAL FORM +← (‎ ﵽ ‎) FD7D ARABIC LIGATURE FEH WITH KHAH WITH MEEM INITIAL FORM + +# فم ﰰ ﳁ + (‎ فم ‎) 0641 0645 ARABIC LETTER FEH, ARABIC LETTER MEEM +← (‎ ﰰ ‎) FC30 ARABIC LIGATURE FEH WITH MEEM ISOLATED FORM +← (‎ ﳁ ‎) FCC1 ARABIC LIGATURE FEH WITH MEEM INITIAL FORM + +# فمى فمي ﷁ + (‎ فمى ‎) 0641 0645 0649 ARABIC LETTER FEH, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ فمي ‎) 0641 0645 064A ARABIC LETTER FEH, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﷁ ‎) FDC1 ARABIC LIGATURE FEH WITH MEEM WITH YEH FINAL FORM # →‎فمي‎→ + +# فى في ﰱ ﰲ ﱼ ﱽ + (‎ فى ‎) 0641 0649 ARABIC LETTER FEH, ARABIC LETTER ALEF MAKSURA +← (‎ في ‎) 0641 064A ARABIC LETTER FEH, ARABIC LETTER YEH +← (‎ ﰱ ‎) FC31 ARABIC LIGATURE FEH WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰲ ‎) FC32 ARABIC LIGATURE FEH WITH YEH ISOLATED FORM # →‎في‎→ +← (‎ ﱼ ‎) FC7C ARABIC LIGATURE FEH WITH ALEF MAKSURA FINAL FORM +← (‎ ﱽ ‎) FC7D ARABIC LIGATURE FEH WITH YEH FINAL FORM # →‎في‎→ + +# ق 𞸒 𞸲 𞹒 𞹲 𞺒 𞺲 ﻕ ﻖ ﻗ ﻘ + (‎ ق ‎) 0642 ARABIC LETTER QAF +← (‎ 𞸒 ‎) 1EE12 ARABIC MATHEMATICAL QAF +← (‎ 𞸲 ‎) 1EE32 ARABIC MATHEMATICAL INITIAL QAF +← (‎ 𞹒 ‎) 1EE52 ARABIC MATHEMATICAL TAILED QAF +← (‎ 𞹲 ‎) 1EE72 ARABIC MATHEMATICAL STRETCHED QAF +← (‎ 𞺒 ‎) 1EE92 ARABIC MATHEMATICAL LOOPED QAF +← (‎ 𞺲 ‎) 1EEB2 ARABIC MATHEMATICAL DOUBLE-STRUCK QAF +← (‎ ﻕ ‎) FED5 ARABIC LETTER QAF ISOLATED FORM +← (‎ ﻖ ‎) FED6 ARABIC LETTER QAF FINAL FORM +← (‎ ﻗ ‎) FED7 ARABIC LETTER QAF INITIAL FORM +← (‎ ﻘ ‎) FED8 ARABIC LETTER QAF MEDIAL FORM + +# قح ﰳ ﳂ + (‎ قح ‎) 0642 062D ARABIC LETTER QAF, ARABIC LETTER HAH +← (‎ ﰳ ‎) FC33 ARABIC LIGATURE QAF WITH HAH ISOLATED FORM +← (‎ ﳂ ‎) FCC2 ARABIC LIGATURE QAF WITH HAH INITIAL FORM + +# قلى قلے ﷱ + (‎ قلى ‎) 0642 0644 0649 ARABIC LETTER QAF, ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA +← (‎ قلے ‎) 0642 0644 06D2 ARABIC LETTER QAF, ARABIC LETTER LAM, ARABIC LETTER YEH BARREE +← (‎ ﷱ ‎) FDF1 ARABIC LIGATURE QALA USED AS KORANIC STOP SIGN ISOLATED FORM # →‎قلے‎→ + +# قم ﰴ ﳃ + (‎ قم ‎) 0642 0645 ARABIC LETTER QAF, ARABIC LETTER MEEM +← (‎ ﰴ ‎) FC34 ARABIC LIGATURE QAF WITH MEEM ISOLATED FORM +← (‎ ﳃ ‎) FCC3 ARABIC LIGATURE QAF WITH MEEM INITIAL FORM + +# قمح ﵾ ﶴ + (‎ قمح ‎) 0642 0645 062D ARABIC LETTER QAF, ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﵾ ‎) FD7E ARABIC LIGATURE QAF WITH MEEM WITH HAH FINAL FORM +← (‎ ﶴ ‎) FDB4 ARABIC LIGATURE QAF WITH MEEM WITH HAH INITIAL FORM + +# قمم ﵿ + (‎ قمم ‎) 0642 0645 0645 ARABIC LETTER QAF, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﵿ ‎) FD7F ARABIC LIGATURE QAF WITH MEEM WITH MEEM FINAL FORM + +# قمى قمي ﶲ + (‎ قمى ‎) 0642 0645 0649 ARABIC LETTER QAF, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ قمي ‎) 0642 0645 064A ARABIC LETTER QAF, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶲ ‎) FDB2 ARABIC LIGATURE QAF WITH MEEM WITH YEH FINAL FORM # →‎قمي‎→ + +# قى قي ﰵ ﰶ ﱾ ﱿ + (‎ قى ‎) 0642 0649 ARABIC LETTER QAF, ARABIC LETTER ALEF MAKSURA +← (‎ قي ‎) 0642 064A ARABIC LETTER QAF, ARABIC LETTER YEH +← (‎ ﰵ ‎) FC35 ARABIC LIGATURE QAF WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰶ ‎) FC36 ARABIC LIGATURE QAF WITH YEH ISOLATED FORM # →‎قي‎→ +← (‎ ﱾ ‎) FC7E ARABIC LIGATURE QAF WITH ALEF MAKSURA FINAL FORM +← (‎ ﱿ ‎) FC7F ARABIC LIGATURE QAF WITH YEH FINAL FORM # →‎قي‎→ + +# ك ک ڪ 𞸊 𞸪 𞹪 ﮎ ﮏ ﮐ ﮑ ﻙ ﻚ ﻛ ﻜ + (‎ ك ‎) 0643 ARABIC LETTER KAF +← (‎ ک ‎) 06A9 ARABIC LETTER KEHEH +← (‎ ڪ ‎) 06AA ARABIC LETTER SWASH KAF +← (‎ 𞸊 ‎) 1EE0A ARABIC MATHEMATICAL KAF +← (‎ 𞸪 ‎) 1EE2A ARABIC MATHEMATICAL INITIAL KAF +← (‎ 𞹪 ‎) 1EE6A ARABIC MATHEMATICAL STRETCHED KAF +← (‎ ﮎ ‎) FB8E ARABIC LETTER KEHEH ISOLATED FORM # →‎ک‎→ +← (‎ ﮏ ‎) FB8F ARABIC LETTER KEHEH FINAL FORM # →‎ک‎→ +← (‎ ﮐ ‎) FB90 ARABIC LETTER KEHEH INITIAL FORM # →‎ک‎→ +← (‎ ﮑ ‎) FB91 ARABIC LETTER KEHEH MEDIAL FORM # →‎ک‎→ +← (‎ ﻙ ‎) FED9 ARABIC LETTER KAF ISOLATED FORM +← (‎ ﻚ ‎) FEDA ARABIC LETTER KAF FINAL FORM +← (‎ ﻛ ‎) FEDB ARABIC LETTER KAF INITIAL FORM +← (‎ ﻜ ‎) FEDC ARABIC LETTER KAF MEDIAL FORM + +# كl ك1 كا ﰷ ﲀ + (‎ ك1 ‎) 0643 0031 ARABIC LETTER KAF, DIGIT ONE +← (‎ كl ‎) 0643 006C ARABIC LETTER KAF, LATIN SMALL LETTER L # →‎كا‎→ +← (‎ كا ‎) 0643 0627 ARABIC LETTER KAF, ARABIC LETTER ALEF +← (‎ ﰷ ‎) FC37 ARABIC LIGATURE KAF WITH ALEF ISOLATED FORM # →‎كا‎→ +← (‎ ﲀ ‎) FC80 ARABIC LIGATURE KAF WITH ALEF FINAL FORM # →‎كا‎→ + +# كج ﰸ ﳄ + (‎ كج ‎) 0643 062C ARABIC LETTER KAF, ARABIC LETTER JEEM +← (‎ ﰸ ‎) FC38 ARABIC LIGATURE KAF WITH JEEM ISOLATED FORM +← (‎ ﳄ ‎) FCC4 ARABIC LIGATURE KAF WITH JEEM INITIAL FORM + +# كح ﰹ ﳅ + (‎ كح ‎) 0643 062D ARABIC LETTER KAF, ARABIC LETTER HAH +← (‎ ﰹ ‎) FC39 ARABIC LIGATURE KAF WITH HAH ISOLATED FORM +← (‎ ﳅ ‎) FCC5 ARABIC LIGATURE KAF WITH HAH INITIAL FORM + +# كخ ﰺ ﳆ + (‎ كخ ‎) 0643 062E ARABIC LETTER KAF, ARABIC LETTER KHAH +← (‎ ﰺ ‎) FC3A ARABIC LIGATURE KAF WITH KHAH ISOLATED FORM +← (‎ ﳆ ‎) FCC6 ARABIC LIGATURE KAF WITH KHAH INITIAL FORM + +# كل ﰻ ﲁ ﳇ ﳫ + (‎ كل ‎) 0643 0644 ARABIC LETTER KAF, ARABIC LETTER LAM +← (‎ ﰻ ‎) FC3B ARABIC LIGATURE KAF WITH LAM ISOLATED FORM +← (‎ ﲁ ‎) FC81 ARABIC LIGATURE KAF WITH LAM FINAL FORM +← (‎ ﳇ ‎) FCC7 ARABIC LIGATURE KAF WITH LAM INITIAL FORM +← (‎ ﳫ ‎) FCEB ARABIC LIGATURE KAF WITH LAM MEDIAL FORM + +# كم ﰼ ﲂ ﳈ ﳬ + (‎ كم ‎) 0643 0645 ARABIC LETTER KAF, ARABIC LETTER MEEM +← (‎ ﰼ ‎) FC3C ARABIC LIGATURE KAF WITH MEEM ISOLATED FORM +← (‎ ﲂ ‎) FC82 ARABIC LIGATURE KAF WITH MEEM FINAL FORM +← (‎ ﳈ ‎) FCC8 ARABIC LIGATURE KAF WITH MEEM INITIAL FORM +← (‎ ﳬ ‎) FCEC ARABIC LIGATURE KAF WITH MEEM MEDIAL FORM + +# كمم ﶻ ﷃ + (‎ كمم ‎) 0643 0645 0645 ARABIC LETTER KAF, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﶻ ‎) FDBB ARABIC LIGATURE KAF WITH MEEM WITH MEEM FINAL FORM +← (‎ ﷃ ‎) FDC3 ARABIC LIGATURE KAF WITH MEEM WITH MEEM INITIAL FORM + +# كمى كمي ﶷ + (‎ كمى ‎) 0643 0645 0649 ARABIC LETTER KAF, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ كمي ‎) 0643 0645 064A ARABIC LETTER KAF, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶷ ‎) FDB7 ARABIC LIGATURE KAF WITH MEEM WITH YEH FINAL FORM # →‎كمي‎→ + +# كى كي ﰽ ﰾ ﲃ ﲄ + (‎ كى ‎) 0643 0649 ARABIC LETTER KAF, ARABIC LETTER ALEF MAKSURA +← (‎ كي ‎) 0643 064A ARABIC LETTER KAF, ARABIC LETTER YEH +← (‎ ﰽ ‎) FC3D ARABIC LIGATURE KAF WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﰾ ‎) FC3E ARABIC LIGATURE KAF WITH YEH ISOLATED FORM # →‎كي‎→ +← (‎ ﲃ ‎) FC83 ARABIC LIGATURE KAF WITH ALEF MAKSURA FINAL FORM +← (‎ ﲄ ‎) FC84 ARABIC LIGATURE KAF WITH YEH FINAL FORM # →‎كي‎→ + +# كۛ کۛ ڭ ݣ ﯓ ﯔ ﯕ ﯖ + (‎ كۛ ‎) 0643 06DB ARABIC LETTER KAF, ARABIC SMALL HIGH THREE DOTS +← (‎ کۛ ‎) 06A9 06DB ARABIC LETTER KEHEH, ARABIC SMALL HIGH THREE DOTS # →‎ݣ‎→→‎ڭ‎→ +← (‎ ڭ ‎) 06AD ARABIC LETTER NG +← (‎ ݣ ‎) 0763 ARABIC LETTER KEHEH WITH THREE DOTS ABOVE # →‎ڭ‎→ +← (‎ ﯓ ‎) FBD3 ARABIC LETTER NG ISOLATED FORM # →‎ڭ‎→ +← (‎ ﯔ ‎) FBD4 ARABIC LETTER NG FINAL FORM # →‎ڭ‎→ +← (‎ ﯕ ‎) FBD5 ARABIC LETTER NG INITIAL FORM # →‎ڭ‎→ +← (‎ ﯖ ‎) FBD6 ARABIC LETTER NG MEDIAL FORM # →‎ڭ‎→ + +# ل 𞸋 𞸫 𞹋 𞺋 𞺫 ﻝ ﻞ ﻟ ﻠ + (‎ ل ‎) 0644 ARABIC LETTER LAM +← (‎ 𞸋 ‎) 1EE0B ARABIC MATHEMATICAL LAM +← (‎ 𞸫 ‎) 1EE2B ARABIC MATHEMATICAL INITIAL LAM +← (‎ 𞹋 ‎) 1EE4B ARABIC MATHEMATICAL TAILED LAM +← (‎ 𞺋 ‎) 1EE8B ARABIC MATHEMATICAL LOOPED LAM +← (‎ 𞺫 ‎) 1EEAB ARABIC MATHEMATICAL DOUBLE-STRUCK LAM +← (‎ ﻝ ‎) FEDD ARABIC LETTER LAM ISOLATED FORM +← (‎ ﻞ ‎) FEDE ARABIC LETTER LAM FINAL FORM +← (‎ ﻟ ‎) FEDF ARABIC LETTER LAM INITIAL FORM +← (‎ ﻠ ‎) FEE0 ARABIC LETTER LAM MEDIAL FORM + +# لl ل1 لا ﻻ ﻼ + (‎ ل1 ‎) 0644 0031 ARABIC LETTER LAM, DIGIT ONE +← (‎ لl ‎) 0644 006C ARABIC LETTER LAM, LATIN SMALL LETTER L # →‎لا‎→ +← (‎ لا ‎) 0644 0627 ARABIC LETTER LAM, ARABIC LETTER ALEF +← (‎ ﻻ ‎) FEFB ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM # →‎لا‎→ +← (‎ ﻼ ‎) FEFC ARABIC LIGATURE LAM WITH ALEF FINAL FORM # →‎لا‎→ + +# لlٕ لٳ لإ ﻹ ﻺ + (‎ لlٕ ‎) 0644 006C 0655 ARABIC LETTER LAM, LATIN SMALL LETTER L, ARABIC HAMZA BELOW +← (‎ لٳ ‎) 0644 0673 ARABIC LETTER LAM, ARABIC LETTER ALEF WITH WAVY HAMZA BELOW # →‎لإ‎→ +← (‎ لإ ‎) 0644 0625 ARABIC LETTER LAM, ARABIC LETTER ALEF WITH HAMZA BELOW +← (‎ ﻹ ‎) FEF9 ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW ISOLATED FORM # →‎لإ‎→ +← (‎ ﻺ ‎) FEFA ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW FINAL FORM # →‎لإ‎→ + +# لlٴ لٲ لأ ﻷ ﻸ + (‎ لlٴ ‎) 0644 006C 0674 ARABIC LETTER LAM, LATIN SMALL LETTER L, ARABIC LETTER HIGH HAMZA +← (‎ لٲ ‎) 0644 0672 ARABIC LETTER LAM, ARABIC LETTER ALEF WITH WAVY HAMZA ABOVE # →‎لأ‎→ +← (‎ لأ ‎) 0644 0623 ARABIC LETTER LAM, ARABIC LETTER ALEF WITH HAMZA ABOVE +← (‎ ﻷ ‎) FEF7 ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE ISOLATED FORM # →‎لأ‎→ +← (‎ ﻸ ‎) FEF8 ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE FINAL FORM # →‎لأ‎→ + +# لo له ﳍ + (‎ لo ‎) 0644 006F ARABIC LETTER LAM, LATIN SMALL LETTER O +← (‎ له ‎) 0644 0647 ARABIC LETTER LAM, ARABIC LETTER HEH +← (‎ ﳍ ‎) FCCD ARABIC LIGATURE LAM WITH HEH INITIAL FORM # →‎له‎→ + +# ل̆ لٚ ڵ + (‎ ل̆ ‎) 0644 0306 ARABIC LETTER LAM, COMBINING BREVE +← (‎ لٚ ‎) 0644 065A ARABIC LETTER LAM, ARABIC VOWEL SIGN SMALL V ABOVE +← (‎ ڵ ‎) 06B5 ARABIC LETTER LAM WITH SMALL V # →‎لٚ‎→ + +# لآ ﻵ ﻶ + (‎ لآ ‎) 0644 0622 ARABIC LETTER LAM, ARABIC LETTER ALEF WITH MADDA ABOVE +← (‎ ﻵ ‎) FEF5 ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE ISOLATED FORM +← (‎ ﻶ ‎) FEF6 ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE FINAL FORM + +# لج ﰿ ﳉ + (‎ لج ‎) 0644 062C ARABIC LETTER LAM, ARABIC LETTER JEEM +← (‎ ﰿ ‎) FC3F ARABIC LIGATURE LAM WITH JEEM ISOLATED FORM +← (‎ ﳉ ‎) FCC9 ARABIC LIGATURE LAM WITH JEEM INITIAL FORM + +# لجج ﶃ ﶄ + (‎ لجج ‎) 0644 062C 062C ARABIC LETTER LAM, ARABIC LETTER JEEM, ARABIC LETTER JEEM +← (‎ ﶃ ‎) FD83 ARABIC LIGATURE LAM WITH JEEM WITH JEEM INITIAL FORM +← (‎ ﶄ ‎) FD84 ARABIC LIGATURE LAM WITH JEEM WITH JEEM FINAL FORM + +# لجم ﶺ ﶼ + (‎ لجم ‎) 0644 062C 0645 ARABIC LETTER LAM, ARABIC LETTER JEEM, ARABIC LETTER MEEM +← (‎ ﶺ ‎) FDBA ARABIC LIGATURE LAM WITH JEEM WITH MEEM INITIAL FORM +← (‎ ﶼ ‎) FDBC ARABIC LIGATURE LAM WITH JEEM WITH MEEM FINAL FORM + +# لجى لجي ﶬ + (‎ لجى ‎) 0644 062C 0649 ARABIC LETTER LAM, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ لجي ‎) 0644 062C 064A ARABIC LETTER LAM, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﶬ ‎) FDAC ARABIC LIGATURE LAM WITH JEEM WITH YEH FINAL FORM # →‎لجي‎→ + +# لح ﱀ ﳊ + (‎ لح ‎) 0644 062D ARABIC LETTER LAM, ARABIC LETTER HAH +← (‎ ﱀ ‎) FC40 ARABIC LIGATURE LAM WITH HAH ISOLATED FORM +← (‎ ﳊ ‎) FCCA ARABIC LIGATURE LAM WITH HAH INITIAL FORM + +# لحم ﶀ ﶵ + (‎ لحم ‎) 0644 062D 0645 ARABIC LETTER LAM, ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ ﶀ ‎) FD80 ARABIC LIGATURE LAM WITH HAH WITH MEEM FINAL FORM +← (‎ ﶵ ‎) FDB5 ARABIC LIGATURE LAM WITH HAH WITH MEEM INITIAL FORM + +# لحى لحي ﶁ ﶂ + (‎ لحى ‎) 0644 062D 0649 ARABIC LETTER LAM, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ لحي ‎) 0644 062D 064A ARABIC LETTER LAM, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶁ ‎) FD81 ARABIC LIGATURE LAM WITH HAH WITH YEH FINAL FORM # →‎لحي‎→ +← (‎ ﶂ ‎) FD82 ARABIC LIGATURE LAM WITH HAH WITH ALEF MAKSURA FINAL FORM + +# لخ ﱁ ﳋ + (‎ لخ ‎) 0644 062E ARABIC LETTER LAM, ARABIC LETTER KHAH +← (‎ ﱁ ‎) FC41 ARABIC LIGATURE LAM WITH KHAH ISOLATED FORM +← (‎ ﳋ ‎) FCCB ARABIC LIGATURE LAM WITH KHAH INITIAL FORM + +# لخم ﶅ ﶆ + (‎ لخم ‎) 0644 062E 0645 ARABIC LETTER LAM, ARABIC LETTER KHAH, ARABIC LETTER MEEM +← (‎ ﶅ ‎) FD85 ARABIC LIGATURE LAM WITH KHAH WITH MEEM FINAL FORM +← (‎ ﶆ ‎) FD86 ARABIC LIGATURE LAM WITH KHAH WITH MEEM INITIAL FORM + +# لم ﱂ ﲅ ﳌ ﳭ + (‎ لم ‎) 0644 0645 ARABIC LETTER LAM, ARABIC LETTER MEEM +← (‎ ﱂ ‎) FC42 ARABIC LIGATURE LAM WITH MEEM ISOLATED FORM +← (‎ ﲅ ‎) FC85 ARABIC LIGATURE LAM WITH MEEM FINAL FORM +← (‎ ﳌ ‎) FCCC ARABIC LIGATURE LAM WITH MEEM INITIAL FORM +← (‎ ﳭ ‎) FCED ARABIC LIGATURE LAM WITH MEEM MEDIAL FORM + +# لمح ﶇ ﶈ + (‎ لمح ‎) 0644 0645 062D ARABIC LETTER LAM, ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﶇ ‎) FD87 ARABIC LIGATURE LAM WITH MEEM WITH HAH FINAL FORM +← (‎ ﶈ ‎) FD88 ARABIC LIGATURE LAM WITH MEEM WITH HAH INITIAL FORM + +# لمى لمي ﶭ + (‎ لمى ‎) 0644 0645 0649 ARABIC LETTER LAM, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ لمي ‎) 0644 0645 064A ARABIC LETTER LAM, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶭ ‎) FDAD ARABIC LIGATURE LAM WITH MEEM WITH YEH FINAL FORM # →‎لمي‎→ + +# لى لي ﱃ ﱄ ﲆ ﲇ + (‎ لى ‎) 0644 0649 ARABIC LETTER LAM, ARABIC LETTER ALEF MAKSURA +← (‎ لي ‎) 0644 064A ARABIC LETTER LAM, ARABIC LETTER YEH +← (‎ ﱃ ‎) FC43 ARABIC LIGATURE LAM WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﱄ ‎) FC44 ARABIC LIGATURE LAM WITH YEH ISOLATED FORM # →‎لي‎→ +← (‎ ﲆ ‎) FC86 ARABIC LIGATURE LAM WITH ALEF MAKSURA FINAL FORM +← (‎ ﲇ ‎) FC87 ARABIC LIGATURE LAM WITH YEH FINAL FORM # →‎لي‎→ + +# لۛ ڷ + (‎ لۛ ‎) 0644 06DB ARABIC LETTER LAM, ARABIC SMALL HIGH THREE DOTS +← (‎ ڷ ‎) 06B7 ARABIC LETTER LAM WITH THREE DOTS ABOVE + +# م 𞸌 𞸬 𞹬 𞺌 𞺬 ﻡ ﻢ ﻣ ﻤ + (‎ م ‎) 0645 ARABIC LETTER MEEM +← (‎ 𞸌 ‎) 1EE0C ARABIC MATHEMATICAL MEEM +← (‎ 𞸬 ‎) 1EE2C ARABIC MATHEMATICAL INITIAL MEEM +← (‎ 𞹬 ‎) 1EE6C ARABIC MATHEMATICAL STRETCHED MEEM +← (‎ 𞺌 ‎) 1EE8C ARABIC MATHEMATICAL LOOPED MEEM +← (‎ 𞺬 ‎) 1EEAC ARABIC MATHEMATICAL DOUBLE-STRUCK MEEM +← (‎ ﻡ ‎) FEE1 ARABIC LETTER MEEM ISOLATED FORM +← (‎ ﻢ ‎) FEE2 ARABIC LETTER MEEM FINAL FORM +← (‎ ﻣ ‎) FEE3 ARABIC LETTER MEEM INITIAL FORM +← (‎ ﻤ ‎) FEE4 ARABIC LETTER MEEM MEDIAL FORM + +# مl م1 ما ﲈ + (‎ م1 ‎) 0645 0031 ARABIC LETTER MEEM, DIGIT ONE +← (‎ مl ‎) 0645 006C ARABIC LETTER MEEM, LATIN SMALL LETTER L # →‎ما‎→ +← (‎ ما ‎) 0645 0627 ARABIC LETTER MEEM, ARABIC LETTER ALEF +← (‎ ﲈ ‎) FC88 ARABIC LIGATURE MEEM WITH ALEF FINAL FORM # →‎ما‎→ + +# م͈ ۾ + (‎ م͈ ‎) 0645 0348 ARABIC LETTER MEEM, COMBINING DOUBLE VERTICAL LINE BELOW +← (‎ ۾ ‎) 06FE ARABIC SIGN SINDHI POSTPOSITION MEN + +# مج ﱅ ﳎ + (‎ مج ‎) 0645 062C ARABIC LETTER MEEM, ARABIC LETTER JEEM +← (‎ ﱅ ‎) FC45 ARABIC LIGATURE MEEM WITH JEEM ISOLATED FORM +← (‎ ﳎ ‎) FCCE ARABIC LIGATURE MEEM WITH JEEM INITIAL FORM + +# مجح ﶌ + (‎ مجح ‎) 0645 062C 062D ARABIC LETTER MEEM, ARABIC LETTER JEEM, ARABIC LETTER HAH +← (‎ ﶌ ‎) FD8C ARABIC LIGATURE MEEM WITH JEEM WITH HAH INITIAL FORM + +# مجخ ﶒ + (‎ مجخ ‎) 0645 062C 062E ARABIC LETTER MEEM, ARABIC LETTER JEEM, ARABIC LETTER KHAH +← (‎ ﶒ ‎) FD92 ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM + +# مجم ﶍ + (‎ مجم ‎) 0645 062C 0645 ARABIC LETTER MEEM, ARABIC LETTER JEEM, ARABIC LETTER MEEM +← (‎ ﶍ ‎) FD8D ARABIC LIGATURE MEEM WITH JEEM WITH MEEM INITIAL FORM + +# مجى مجي ﷀ + (‎ مجى ‎) 0645 062C 0649 ARABIC LETTER MEEM, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ مجي ‎) 0645 062C 064A ARABIC LETTER MEEM, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﷀ ‎) FDC0 ARABIC LIGATURE MEEM WITH JEEM WITH YEH FINAL FORM # →‎مجي‎→ + +# مح ﱆ ﳏ + (‎ مح ‎) 0645 062D ARABIC LETTER MEEM, ARABIC LETTER HAH +← (‎ ﱆ ‎) FC46 ARABIC LIGATURE MEEM WITH HAH ISOLATED FORM +← (‎ ﳏ ‎) FCCF ARABIC LIGATURE MEEM WITH HAH INITIAL FORM + +# محج ﶉ + (‎ محج ‎) 0645 062D 062C ARABIC LETTER MEEM, ARABIC LETTER HAH, ARABIC LETTER JEEM +← (‎ ﶉ ‎) FD89 ARABIC LIGATURE MEEM WITH HAH WITH JEEM INITIAL FORM + +# محم ﶊ + (‎ محم ‎) 0645 062D 0645 ARABIC LETTER MEEM, ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ ﶊ ‎) FD8A ARABIC LIGATURE MEEM WITH HAH WITH MEEM INITIAL FORM + +# محمد ﷴ + (‎ محمد ‎) 0645 062D 0645 062F ARABIC LETTER MEEM, ARABIC LETTER HAH, ARABIC LETTER MEEM, ARABIC LETTER DAL +← (‎ ﷴ ‎) FDF4 ARABIC LIGATURE MOHAMMAD ISOLATED FORM + +# محى محي ﶋ + (‎ محى ‎) 0645 062D 0649 ARABIC LETTER MEEM, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ محي ‎) 0645 062D 064A ARABIC LETTER MEEM, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶋ ‎) FD8B ARABIC LIGATURE MEEM WITH HAH WITH YEH FINAL FORM # →‎محي‎→ + +# مخ ﱇ ﳐ + (‎ مخ ‎) 0645 062E ARABIC LETTER MEEM, ARABIC LETTER KHAH +← (‎ ﱇ ‎) FC47 ARABIC LIGATURE MEEM WITH KHAH ISOLATED FORM +← (‎ ﳐ ‎) FCD0 ARABIC LIGATURE MEEM WITH KHAH INITIAL FORM + +# مخج ﶎ + (‎ مخج ‎) 0645 062E 062C ARABIC LETTER MEEM, ARABIC LETTER KHAH, ARABIC LETTER JEEM +← (‎ ﶎ ‎) FD8E ARABIC LIGATURE MEEM WITH KHAH WITH JEEM INITIAL FORM + +# مخم ﶏ + (‎ مخم ‎) 0645 062E 0645 ARABIC LETTER MEEM, ARABIC LETTER KHAH, ARABIC LETTER MEEM +← (‎ ﶏ ‎) FD8F ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM + +# مخى مخي ﶹ + (‎ مخى ‎) 0645 062E 0649 ARABIC LETTER MEEM, ARABIC LETTER KHAH, ARABIC LETTER ALEF MAKSURA +← (‎ مخي ‎) 0645 062E 064A ARABIC LETTER MEEM, ARABIC LETTER KHAH, ARABIC LETTER YEH +← (‎ ﶹ ‎) FDB9 ARABIC LIGATURE MEEM WITH KHAH WITH YEH FINAL FORM # →‎مخي‎→ + +# مم ﱈ ﲉ ﳑ + (‎ مم ‎) 0645 0645 ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﱈ ‎) FC48 ARABIC LIGATURE MEEM WITH MEEM ISOLATED FORM +← (‎ ﲉ ‎) FC89 ARABIC LIGATURE MEEM WITH MEEM FINAL FORM +← (‎ ﳑ ‎) FCD1 ARABIC LIGATURE MEEM WITH MEEM INITIAL FORM + +# ممى ممي ﶱ + (‎ ممى ‎) 0645 0645 0649 ARABIC LETTER MEEM, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ ممي ‎) 0645 0645 064A ARABIC LETTER MEEM, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶱ ‎) FDB1 ARABIC LIGATURE MEEM WITH MEEM WITH YEH FINAL FORM # →‎ممي‎→ + +# مى مي ﱉ ﱊ + (‎ مى ‎) 0645 0649 ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ مي ‎) 0645 064A ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﱉ ‎) FC49 ARABIC LIGATURE MEEM WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﱊ ‎) FC4A ARABIC LIGATURE MEEM WITH YEH ISOLATED FORM # →‎مي‎→ + +# مۛ ࢧ + (‎ مۛ ‎) 0645 06DB ARABIC LETTER MEEM, ARABIC SMALL HIGH THREE DOTS +← (‎ ࢧ ‎) 08A7 ARABIC LETTER MEEM WITH THREE DOTS ABOVE + +# ن 𞸍 𞸭 𞹍 𞹭 𞺍 𞺭 ﻥ ﻦ ﻧ ﻨ + (‎ ن ‎) 0646 ARABIC LETTER NOON +← (‎ 𞸍 ‎) 1EE0D ARABIC MATHEMATICAL NOON +← (‎ 𞸭 ‎) 1EE2D ARABIC MATHEMATICAL INITIAL NOON +← (‎ 𞹍 ‎) 1EE4D ARABIC MATHEMATICAL TAILED NOON +← (‎ 𞹭 ‎) 1EE6D ARABIC MATHEMATICAL STRETCHED NOON +← (‎ 𞺍 ‎) 1EE8D ARABIC MATHEMATICAL LOOPED NOON +← (‎ 𞺭 ‎) 1EEAD ARABIC MATHEMATICAL DOUBLE-STRUCK NOON +← (‎ ﻥ ‎) FEE5 ARABIC LETTER NOON ISOLATED FORM +← (‎ ﻦ ‎) FEE6 ARABIC LETTER NOON FINAL FORM +← (‎ ﻧ ‎) FEE7 ARABIC LETTER NOON INITIAL FORM +← (‎ ﻨ ‎) FEE8 ARABIC LETTER NOON MEDIAL FORM + +# نo نه ﳖ ﳯ + (‎ نo ‎) 0646 006F ARABIC LETTER NOON, LATIN SMALL LETTER O +← (‎ نه ‎) 0646 0647 ARABIC LETTER NOON, ARABIC LETTER HEH +← (‎ ﳖ ‎) FCD6 ARABIC LIGATURE NOON WITH HEH INITIAL FORM # →‎نه‎→ +← (‎ ﳯ ‎) FCEF ARABIC LIGATURE NOON WITH HEH MEDIAL FORM # →‎نه‎→ + +# ن̆ نٚ ݩ + (‎ ن̆ ‎) 0646 0306 ARABIC LETTER NOON, COMBINING BREVE +← (‎ نٚ ‎) 0646 065A ARABIC LETTER NOON, ARABIC VOWEL SIGN SMALL V ABOVE +← (‎ ݩ ‎) 0769 ARABIC LETTER NOON WITH SMALL V # →‎نٚ‎→ + +# نؕ ݨ + (‎ نؕ ‎) 0646 0615 ARABIC LETTER NOON, ARABIC SMALL HIGH TAH +← (‎ ݨ ‎) 0768 ARABIC LETTER NOON WITH SMALL TAH + +# نجح ﶸ ﶽ + (‎ نجح ‎) 0646 062C 062D ARABIC LETTER NOON, ARABIC LETTER JEEM, ARABIC LETTER HAH +← (‎ ﶸ ‎) FDB8 ARABIC LIGATURE NOON WITH JEEM WITH HAH INITIAL FORM +← (‎ ﶽ ‎) FDBD ARABIC LIGATURE NOON WITH JEEM WITH HAH FINAL FORM + +# نجم ﶗ ﶘ + (‎ نجم ‎) 0646 062C 0645 ARABIC LETTER NOON, ARABIC LETTER JEEM, ARABIC LETTER MEEM +← (‎ ﶗ ‎) FD97 ARABIC LIGATURE NOON WITH JEEM WITH MEEM FINAL FORM +← (‎ ﶘ ‎) FD98 ARABIC LIGATURE NOON WITH JEEM WITH MEEM INITIAL FORM + +# نجى نجي ﶙ ﷇ + (‎ نجى ‎) 0646 062C 0649 ARABIC LETTER NOON, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ نجي ‎) 0646 062C 064A ARABIC LETTER NOON, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﶙ ‎) FD99 ARABIC LIGATURE NOON WITH JEEM WITH ALEF MAKSURA FINAL FORM +← (‎ ﷇ ‎) FDC7 ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM # →‎نجي‎→ + +# نح ﱌ ﳓ + (‎ نح ‎) 0646 062D ARABIC LETTER NOON, ARABIC LETTER HAH +← (‎ ﱌ ‎) FC4C ARABIC LIGATURE NOON WITH HAH ISOLATED FORM +← (‎ ﳓ ‎) FCD3 ARABIC LIGATURE NOON WITH HAH INITIAL FORM + +# نحم ﶕ + (‎ نحم ‎) 0646 062D 0645 ARABIC LETTER NOON, ARABIC LETTER HAH, ARABIC LETTER MEEM +← (‎ ﶕ ‎) FD95 ARABIC LIGATURE NOON WITH HAH WITH MEEM INITIAL FORM + +# نحى نحي ﶖ ﶳ + (‎ نحى ‎) 0646 062D 0649 ARABIC LETTER NOON, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ نحي ‎) 0646 062D 064A ARABIC LETTER NOON, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶖ ‎) FD96 ARABIC LIGATURE NOON WITH HAH WITH ALEF MAKSURA FINAL FORM +← (‎ ﶳ ‎) FDB3 ARABIC LIGATURE NOON WITH HAH WITH YEH FINAL FORM # →‎نحي‎→ + +# نخ ﱍ ﳔ + (‎ نخ ‎) 0646 062E ARABIC LETTER NOON, ARABIC LETTER KHAH +← (‎ ﱍ ‎) FC4D ARABIC LIGATURE NOON WITH KHAH ISOLATED FORM +← (‎ ﳔ ‎) FCD4 ARABIC LIGATURE NOON WITH KHAH INITIAL FORM + +# نر ﲊ + (‎ نر ‎) 0646 0631 ARABIC LETTER NOON, ARABIC LETTER REH +← (‎ ﲊ ‎) FC8A ARABIC LIGATURE NOON WITH REH FINAL FORM + +# نز ﲋ + (‎ نز ‎) 0646 0632 ARABIC LETTER NOON, ARABIC LETTER ZAIN +← (‎ ﲋ ‎) FC8B ARABIC LIGATURE NOON WITH ZAIN FINAL FORM + +# نم ﱎ ﲌ ﳕ ﳮ + (‎ نم ‎) 0646 0645 ARABIC LETTER NOON, ARABIC LETTER MEEM +← (‎ ﱎ ‎) FC4E ARABIC LIGATURE NOON WITH MEEM ISOLATED FORM +← (‎ ﲌ ‎) FC8C ARABIC LIGATURE NOON WITH MEEM FINAL FORM +← (‎ ﳕ ‎) FCD5 ARABIC LIGATURE NOON WITH MEEM INITIAL FORM +← (‎ ﳮ ‎) FCEE ARABIC LIGATURE NOON WITH MEEM MEDIAL FORM + +# نمى نمي ﶚ ﶛ + (‎ نمى ‎) 0646 0645 0649 ARABIC LETTER NOON, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ نمي ‎) 0646 0645 064A ARABIC LETTER NOON, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶚ ‎) FD9A ARABIC LIGATURE NOON WITH MEEM WITH YEH FINAL FORM # →‎نمي‎→ +← (‎ ﶛ ‎) FD9B ARABIC LIGATURE NOON WITH MEEM WITH ALEF MAKSURA FINAL FORM + +# نن ﲍ + (‎ نن ‎) 0646 0646 ARABIC LETTER NOON, ARABIC LETTER NOON +← (‎ ﲍ ‎) FC8D ARABIC LIGATURE NOON WITH NOON FINAL FORM + +# نى ني ﱏ ﱐ ﲎ ﲏ + (‎ نى ‎) 0646 0649 ARABIC LETTER NOON, ARABIC LETTER ALEF MAKSURA +← (‎ ني ‎) 0646 064A ARABIC LETTER NOON, ARABIC LETTER YEH +← (‎ ﱏ ‎) FC4F ARABIC LIGATURE NOON WITH ALEF MAKSURA ISOLATED FORM +← (‎ ﱐ ‎) FC50 ARABIC LIGATURE NOON WITH YEH ISOLATED FORM # →‎ني‎→ +← (‎ ﲎ ‎) FC8E ARABIC LIGATURE NOON WITH ALEF MAKSURA FINAL FORM +← (‎ ﲏ ‎) FC8F ARABIC LIGATURE NOON WITH YEH FINAL FORM # →‎ني‎→ + +# و ࢱ 𐋤 𞸅 𞺅 𞺥 ﻭ ﻮ + (‎ و ‎) 0648 ARABIC LETTER WAW +← (‎ ࢱ ‎) 08B1 ARABIC LETTER STRAIGHT WAW +← (‎ 𐋤 ‎) 102E4 COPTIC EPACT DIGIT FOUR +← (‎ 𞸅 ‎) 1EE05 ARABIC MATHEMATICAL WAW +← (‎ 𞺅 ‎) 1EE85 ARABIC MATHEMATICAL LOOPED WAW +← (‎ 𞺥 ‎) 1EEA5 ARABIC MATHEMATICAL DOUBLE-STRUCK WAW +← (‎ ﻭ ‎) FEED ARABIC LETTER WAW ISOLATED FORM +← (‎ ﻮ ‎) FEEE ARABIC LETTER WAW FINAL FORM + +# و̂ وٛ ۉ ﯢ ﯣ + (‎ و̂ ‎) 0648 0302 ARABIC LETTER WAW, COMBINING CIRCUMFLEX ACCENT +← (‎ وٛ ‎) 0648 065B ARABIC LETTER WAW, ARABIC VOWEL SIGN INVERTED SMALL V ABOVE +← (‎ ۉ ‎) 06C9 ARABIC LETTER KIRGHIZ YU # →‎وٛ‎→ +← (‎ ﯢ ‎) FBE2 ARABIC LETTER KIRGHIZ YU ISOLATED FORM # →‎ۉ‎→→‎وٛ‎→ +← (‎ ﯣ ‎) FBE3 ARABIC LETTER KIRGHIZ YU FINAL FORM # →‎ۉ‎→→‎وٛ‎→ + +# و̆ وٚ ۆ ﯙ ﯚ + (‎ و̆ ‎) 0648 0306 ARABIC LETTER WAW, COMBINING BREVE +← (‎ وٚ ‎) 0648 065A ARABIC LETTER WAW, ARABIC VOWEL SIGN SMALL V ABOVE +← (‎ ۆ ‎) 06C6 ARABIC LETTER OE # →‎وٚ‎→ +← (‎ ﯙ ‎) FBD9 ARABIC LETTER OE ISOLATED FORM # →‎ۆ‎→→‎وٚ‎→ +← (‎ ﯚ ‎) FBDA ARABIC LETTER OE FINAL FORM # →‎ۆ‎→→‎وٚ‎→ + +# و̓ وُ ۇ ﯗ ﯘ + (‎ و̓ ‎) 0648 0313 ARABIC LETTER WAW, COMBINING COMMA ABOVE +← (‎ وُ ‎) 0648 064F ARABIC LETTER WAW, ARABIC DAMMA +← (‎ ۇ ‎) 06C7 ARABIC LETTER U # →‎وُ‎→ +← (‎ ﯗ ‎) FBD7 ARABIC LETTER U ISOLATED FORM # →‎ۇ‎→→‎وُ‎→ +← (‎ ﯘ ‎) FBD8 ARABIC LETTER U FINAL FORM # →‎ۇ‎→→‎وُ‎→ + +# و̓ٴ ٴو̓ ٴۇ ۇٴ ٷ ﯝ + (‎ و̓ٴ ‎) 0648 0313 0674 ARABIC LETTER WAW, COMBINING COMMA ABOVE, ARABIC LETTER HIGH HAMZA +← (‎ ٴو̓ ‎) 0674 0648 0313 ARABIC LETTER HIGH HAMZA, ARABIC LETTER WAW, COMBINING COMMA ABOVE # →‎ٴۇ‎→→‎ٷ‎→→‎ۇٴ‎→ +← (‎ ٴۇ ‎) 0674 06C7 ARABIC LETTER HIGH HAMZA, ARABIC LETTER U # →‎ٷ‎→→‎ۇٴ‎→ +← (‎ ۇٴ ‎) 06C7 0674 ARABIC LETTER U, ARABIC LETTER HIGH HAMZA +← (‎ ٷ ‎) 0677 ARABIC LETTER U WITH HAMZA ABOVE # →‎ۇٴ‎→ +← (‎ ﯝ ‎) FBDD ARABIC LETTER U WITH HAMZA ABOVE ISOLATED FORM # →‎ۇٴ‎→ + +# وسلم ﷸ + (‎ وسلم ‎) 0648 0633 0644 0645 ARABIC LETTER WAW, ARABIC LETTER SEEN, ARABIC LETTER LAM, ARABIC LETTER MEEM +← (‎ ﷸ ‎) FDF8 ARABIC LIGATURE WASALLAM ISOLATED FORM + +# وٰ ۈ ﯛ ﯜ + (‎ وٰ ‎) 0648 0670 ARABIC LETTER WAW, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ۈ ‎) 06C8 ARABIC LETTER YU +← (‎ ﯛ ‎) FBDB ARABIC LETTER YU ISOLATED FORM # →‎ۈ‎→ +← (‎ ﯜ ‎) FBDC ARABIC LETTER YU FINAL FORM # →‎ۈ‎→ + +# وۛ ۋ ﯞ ﯟ + (‎ وۛ ‎) 0648 06DB ARABIC LETTER WAW, ARABIC SMALL HIGH THREE DOTS +← (‎ ۋ ‎) 06CB ARABIC LETTER VE +← (‎ ﯞ ‎) FBDE ARABIC LETTER VE ISOLATED FORM # →‎ۋ‎→ +← (‎ ﯟ ‎) FBDF ARABIC LETTER VE FINAL FORM # →‎ۋ‎→ + +# ى ي ں ی ے ٮ ࢽ 𞸉 𞸝 𞸩 𞹉 𞹝 𞹩 𞺉 𞺩 ﮞ ﮟ ﮮ ﮯ ﯨ ﯩ ﯼ ﯽ ﯾ ﯿ ﻯ ﻰ ﻱ ﻲ ﻳ ﻴ 𞸜 𞹼 + (‎ ى ‎) 0649 ARABIC LETTER ALEF MAKSURA +← (‎ ي ‎) 064A ARABIC LETTER YEH +← (‎ ں ‎) 06BA ARABIC LETTER NOON GHUNNA +← (‎ ی ‎) 06CC ARABIC LETTER FARSI YEH +← (‎ ے ‎) 06D2 ARABIC LETTER YEH BARREE # →‎ي‎→ +← (‎ ٮ ‎) 066E ARABIC LETTER DOTLESS BEH +← (‎ ࢽ ‎) 08BD ARABIC LETTER AFRICAN NOON # →‎ں‎→ +← (‎ 𞸉 ‎) 1EE09 ARABIC MATHEMATICAL YEH # →‎ي‎→ +← (‎ 𞸝 ‎) 1EE1D ARABIC MATHEMATICAL DOTLESS NOON # →‎ں‎→ +← (‎ 𞸩 ‎) 1EE29 ARABIC MATHEMATICAL INITIAL YEH # →‎ي‎→ +← (‎ 𞹉 ‎) 1EE49 ARABIC MATHEMATICAL TAILED YEH # →‎ي‎→ +← (‎ 𞹝 ‎) 1EE5D ARABIC MATHEMATICAL TAILED DOTLESS NOON # →‎ں‎→ +← (‎ 𞹩 ‎) 1EE69 ARABIC MATHEMATICAL STRETCHED YEH # →‎ي‎→ +← (‎ 𞺉 ‎) 1EE89 ARABIC MATHEMATICAL LOOPED YEH # →‎ي‎→ +← (‎ 𞺩 ‎) 1EEA9 ARABIC MATHEMATICAL DOUBLE-STRUCK YEH # →‎ي‎→ +← (‎ ﮞ ‎) FB9E ARABIC LETTER NOON GHUNNA ISOLATED FORM # →‎ں‎→ +← (‎ ﮟ ‎) FB9F ARABIC LETTER NOON GHUNNA FINAL FORM # →‎ں‎→ +← (‎ ﮮ ‎) FBAE ARABIC LETTER YEH BARREE ISOLATED FORM # →‎ے‎→→‎ي‎→ +← (‎ ﮯ ‎) FBAF ARABIC LETTER YEH BARREE FINAL FORM # →‎ے‎→→‎ي‎→ +← (‎ ﯨ ‎) FBE8 ARABIC LETTER UIGHUR KAZAKH KIRGHIZ ALEF MAKSURA INITIAL FORM +← (‎ ﯩ ‎) FBE9 ARABIC LETTER UIGHUR KAZAKH KIRGHIZ ALEF MAKSURA MEDIAL FORM +← (‎ ﯼ ‎) FBFC ARABIC LETTER FARSI YEH ISOLATED FORM +← (‎ ﯽ ‎) FBFD ARABIC LETTER FARSI YEH FINAL FORM # →‎ﻰ‎→ +← (‎ ﯾ ‎) FBFE ARABIC LETTER FARSI YEH INITIAL FORM # →‎ی‎→ +← (‎ ﯿ ‎) FBFF ARABIC LETTER FARSI YEH MEDIAL FORM # →‎ی‎→ +← (‎ ﻯ ‎) FEEF ARABIC LETTER ALEF MAKSURA ISOLATED FORM +← (‎ ﻰ ‎) FEF0 ARABIC LETTER ALEF MAKSURA FINAL FORM +← (‎ ﻱ ‎) FEF1 ARABIC LETTER YEH ISOLATED FORM # →‎ي‎→ +← (‎ ﻲ ‎) FEF2 ARABIC LETTER YEH FINAL FORM # →‎ي‎→ +← (‎ ﻳ ‎) FEF3 ARABIC LETTER YEH INITIAL FORM # →‎ي‎→ +← (‎ ﻴ ‎) FEF4 ARABIC LETTER YEH MEDIAL FORM # →‎ي‎→ +← (‎ 𞸜 ‎) 1EE1C ARABIC MATHEMATICAL DOTLESS BEH # →‎ٮ‎→ +← (‎ 𞹼 ‎) 1EE7C ARABIC MATHEMATICAL STRETCHED DOTLESS BEH # →‎ٮ‎→ + +# ىo ىه يه ﳞ ﳱ + (‎ ىo ‎) 0649 006F ARABIC LETTER ALEF MAKSURA, LATIN SMALL LETTER O +← (‎ ىه ‎) 0649 0647 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HEH # →‎يه‎→ +← (‎ يه ‎) 064A 0647 ARABIC LETTER YEH, ARABIC LETTER HEH +← (‎ ﳞ ‎) FCDE ARABIC LIGATURE YEH WITH HEH INITIAL FORM # →‎يه‎→ +← (‎ ﳱ ‎) FCF1 ARABIC LIGATURE YEH WITH HEH MEDIAL FORM # →‎يه‎→ + +# ى̆ ٮٚ یٚ ێ ݖ + (‎ ى̆ ‎) 0649 0306 ARABIC LETTER ALEF MAKSURA, COMBINING BREVE +← (‎ ٮٚ ‎) 066E 065A ARABIC LETTER DOTLESS BEH, ARABIC VOWEL SIGN SMALL V ABOVE +← (‎ یٚ ‎) 06CC 065A ARABIC LETTER FARSI YEH, ARABIC VOWEL SIGN SMALL V ABOVE +← (‎ ێ ‎) 06CE ARABIC LETTER YEH WITH SMALL V # →‎یٚ‎→ +← (‎ ݖ ‎) 0756 ARABIC LETTER BEH WITH SMALL V # →‎ٮٚ‎→ + +# ى̆̇ يۨ ࢺ + (‎ ى̆̇ ‎) 0649 0306 0307 ARABIC LETTER ALEF MAKSURA, COMBINING BREVE, COMBINING DOT ABOVE +← (‎ يۨ ‎) 064A 06E8 ARABIC LETTER YEH, ARABIC SMALL HIGH NOON +← (‎ ࢺ ‎) 08BA ARABIC LETTER YEH WITH TWO DOTS BELOW AND SMALL NOON ABOVE # →‎يۨ‎→ + +# ىؕ ٮؕ ںؕ ٹ ڻ ﭦ ﭧ ﭨ ﭩ ﮠ ﮡ ﮢ ﮣ + (‎ ىؕ ‎) 0649 0615 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH TAH +← (‎ ٮؕ ‎) 066E 0615 ARABIC LETTER DOTLESS BEH, ARABIC SMALL HIGH TAH +← (‎ ںؕ ‎) 06BA 0615 ARABIC LETTER NOON GHUNNA, ARABIC SMALL HIGH TAH +← (‎ ٹ ‎) 0679 ARABIC LETTER TTEH # →‎ٮؕ‎→ +← (‎ ڻ ‎) 06BB ARABIC LETTER RNOON # →‎ںؕ‎→ +← (‎ ﭦ ‎) FB66 ARABIC LETTER TTEH ISOLATED FORM # →‎ٹ‎→→‎ٮؕ‎→ +← (‎ ﭧ ‎) FB67 ARABIC LETTER TTEH FINAL FORM # →‎ٹ‎→→‎ٮؕ‎→ +← (‎ ﭨ ‎) FB68 ARABIC LETTER TTEH INITIAL FORM # →‎ٹ‎→→‎ٮؕ‎→ +← (‎ ﭩ ‎) FB69 ARABIC LETTER TTEH MEDIAL FORM # →‎ٹ‎→→‎ٮؕ‎→ +← (‎ ﮠ ‎) FBA0 ARABIC LETTER RNOON ISOLATED FORM # →‎ڻ‎→→‎ںؕ‎→ +← (‎ ﮡ ‎) FBA1 ARABIC LETTER RNOON FINAL FORM # →‎ڻ‎→→‎ںؕ‎→ +← (‎ ﮢ ‎) FBA2 ARABIC LETTER RNOON INITIAL FORM # →‎ڻ‎→→‎ںؕ‎→ +← (‎ ﮣ ‎) FBA3 ARABIC LETTER RNOON MEDIAL FORM # →‎ڻ‎→→‎ںؕ‎→ + +# ىج يج ﱕ ﳚ + (‎ ىج ‎) 0649 062C ARABIC LETTER ALEF MAKSURA, ARABIC LETTER JEEM +← (‎ يج ‎) 064A 062C ARABIC LETTER YEH, ARABIC LETTER JEEM +← (‎ ﱕ ‎) FC55 ARABIC LIGATURE YEH WITH JEEM ISOLATED FORM # →‎يج‎→ +← (‎ ﳚ ‎) FCDA ARABIC LIGATURE YEH WITH JEEM INITIAL FORM # →‎يج‎→ + +# ىجى يجي ﶯ + (‎ ىجى ‎) 0649 062C 0649 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER JEEM, ARABIC LETTER ALEF MAKSURA +← (‎ يجي ‎) 064A 062C 064A ARABIC LETTER YEH, ARABIC LETTER JEEM, ARABIC LETTER YEH +← (‎ ﶯ ‎) FDAF ARABIC LIGATURE YEH WITH JEEM WITH YEH FINAL FORM # →‎يجي‎→ + +# ىح يح ﱖ ﳛ + (‎ ىح ‎) 0649 062D ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HAH +← (‎ يح ‎) 064A 062D ARABIC LETTER YEH, ARABIC LETTER HAH +← (‎ ﱖ ‎) FC56 ARABIC LIGATURE YEH WITH HAH ISOLATED FORM # →‎يح‎→ +← (‎ ﳛ ‎) FCDB ARABIC LIGATURE YEH WITH HAH INITIAL FORM # →‎يح‎→ + +# ىحى يحي ﶮ + (‎ ىحى ‎) 0649 062D 0649 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER HAH, ARABIC LETTER ALEF MAKSURA +← (‎ يحي ‎) 064A 062D 064A ARABIC LETTER YEH, ARABIC LETTER HAH, ARABIC LETTER YEH +← (‎ ﶮ ‎) FDAE ARABIC LIGATURE YEH WITH HAH WITH YEH FINAL FORM # →‎يحي‎→ + +# ىخ يخ ﱗ ﳜ + (‎ ىخ ‎) 0649 062E ARABIC LETTER ALEF MAKSURA, ARABIC LETTER KHAH +← (‎ يخ ‎) 064A 062E ARABIC LETTER YEH, ARABIC LETTER KHAH +← (‎ ﱗ ‎) FC57 ARABIC LIGATURE YEH WITH KHAH ISOLATED FORM # →‎يخ‎→ +← (‎ ﳜ ‎) FCDC ARABIC LIGATURE YEH WITH KHAH INITIAL FORM # →‎يخ‎→ + +# ىر ير ﲑ + (‎ ىر ‎) 0649 0631 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER REH +← (‎ ير ‎) 064A 0631 ARABIC LETTER YEH, ARABIC LETTER REH +← (‎ ﲑ ‎) FC91 ARABIC LIGATURE YEH WITH REH FINAL FORM # →‎ير‎→ + +# ىز يز ﲒ + (‎ ىز ‎) 0649 0632 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER ZAIN +← (‎ يز ‎) 064A 0632 ARABIC LETTER YEH, ARABIC LETTER ZAIN +← (‎ ﲒ ‎) FC92 ARABIC LIGATURE YEH WITH ZAIN FINAL FORM # →‎يز‎→ + +# ىم يم ﱘ ﲓ ﳝ ﳰ + (‎ ىم ‎) 0649 0645 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER MEEM +← (‎ يم ‎) 064A 0645 ARABIC LETTER YEH, ARABIC LETTER MEEM +← (‎ ﱘ ‎) FC58 ARABIC LIGATURE YEH WITH MEEM ISOLATED FORM # →‎يم‎→ +← (‎ ﲓ ‎) FC93 ARABIC LIGATURE YEH WITH MEEM FINAL FORM # →‎يم‎→ +← (‎ ﳝ ‎) FCDD ARABIC LIGATURE YEH WITH MEEM INITIAL FORM # →‎يم‎→ +← (‎ ﳰ ‎) FCF0 ARABIC LIGATURE YEH WITH MEEM MEDIAL FORM # →‎يم‎→ + +# ىمم يمم ﶜ ﶝ + (‎ ىمم ‎) 0649 0645 0645 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ يمم ‎) 064A 0645 0645 ARABIC LETTER YEH, ARABIC LETTER MEEM, ARABIC LETTER MEEM +← (‎ ﶜ ‎) FD9C ARABIC LIGATURE YEH WITH MEEM WITH MEEM FINAL FORM # →‎يمم‎→ +← (‎ ﶝ ‎) FD9D ARABIC LIGATURE YEH WITH MEEM WITH MEEM INITIAL FORM # →‎يمم‎→ + +# ىمى يمي ﶰ + (‎ ىمى ‎) 0649 0645 0649 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER MEEM, ARABIC LETTER ALEF MAKSURA +← (‎ يمي ‎) 064A 0645 064A ARABIC LETTER YEH, ARABIC LETTER MEEM, ARABIC LETTER YEH +← (‎ ﶰ ‎) FDB0 ARABIC LIGATURE YEH WITH MEEM WITH YEH FINAL FORM # →‎يمي‎→ + +# ىن ين ﲔ + (‎ ىن ‎) 0649 0646 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER NOON +← (‎ ين ‎) 064A 0646 ARABIC LETTER YEH, ARABIC LETTER NOON +← (‎ ﲔ ‎) FC94 ARABIC LIGATURE YEH WITH NOON FINAL FORM # →‎ين‎→ + +# ىى يى يي ﱙ ﱚ ﲕ ﲖ + (‎ ىى ‎) 0649 0649 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER ALEF MAKSURA +← (‎ يى ‎) 064A 0649 ARABIC LETTER YEH, ARABIC LETTER ALEF MAKSURA +← (‎ يي ‎) 064A 064A ARABIC LETTER YEH, ARABIC LETTER YEH +← (‎ ﱙ ‎) FC59 ARABIC LIGATURE YEH WITH ALEF MAKSURA ISOLATED FORM # →‎يى‎→ +← (‎ ﱚ ‎) FC5A ARABIC LIGATURE YEH WITH YEH ISOLATED FORM # →‎يي‎→ +← (‎ ﲕ ‎) FC95 ARABIC LIGATURE YEH WITH ALEF MAKSURA FINAL FORM # →‎يى‎→ +← (‎ ﲖ ‎) FC96 ARABIC LIGATURE YEH WITH YEH FINAL FORM # →‎يي‎→ + +# ىٔ ئ ࢨ + (‎ ىٔ ‎) 0649 0654 ARABIC LETTER ALEF MAKSURA, ARABIC HAMZA ABOVE +← (‎ ئ ‎) 064A 0654 ARABIC LETTER YEH, ARABIC HAMZA ABOVE +← (‎ ࢨ ‎) 08A8 ARABIC LETTER YEH WITH TWO DOTS BELOW AND HAMZA ABOVE # →‎ئ‎→ + +# ىٰ ﱝ ﲐ + (‎ ىٰ ‎) 0649 0670 ARABIC LETTER ALEF MAKSURA, ARABIC LETTER SUPERSCRIPT ALEF +← (‎ ﱝ ‎) FC5D ARABIC LIGATURE ALEF MAKSURA WITH SUPERSCRIPT ALEF ISOLATED FORM +← (‎ ﲐ ‎) FC90 ARABIC LIGATURE ALEF MAKSURA WITH SUPERSCRIPT ALEF FINAL FORM + +# ىۛۢ پۢ ࢷ + (‎ ىۛۢ ‎) 0649 06DB 06E2 ARABIC LETTER ALEF MAKSURA, ARABIC SMALL HIGH THREE DOTS, ARABIC SMALL HIGH MEEM ISOLATED FORM +← (‎ پۢ ‎) 067E 06E2 ARABIC LETTER PEH, ARABIC SMALL HIGH MEEM ISOLATED FORM +← (‎ ࢷ ‎) 08B7 ARABIC LETTER PEH WITH SMALL MEEM ABOVE # →‎پۢ‎→ + +# ٌ ࣥ ࣨ ࣱ + (‎ ٌ ‎) 064C ARABIC DAMMATAN +← (‎ ࣥ ‎) 08E5 ARABIC CURLY DAMMA +← (‎ ࣨ ‎) 08E8 ARABIC CURLY DAMMATAN +← (‎ ࣱ ‎) 08F1 ARABIC OPEN DAMMATAN + +# ٍ ࣲ + (‎ ٍ ‎) 064D ARABIC KASRATAN +← (‎ ࣲ ‎) 08F2 ARABIC OPEN KASRATAN + +# ٕ ٟ + (‎ ٕ ‎) 0655 ARABIC HAMZA BELOW +← (‎ ٟ ‎) 065F ARABIC WAVY HAMZA BELOW + +# ٢ ۲ ꧏ + (‎ ٢ ‎) 0662 ARABIC-INDIC DIGIT TWO +← (‎ ۲ ‎) 06F2 EXTENDED ARABIC-INDIC DIGIT TWO +← (‎ ꧏ ‎) A9CF JAVANESE PANGRANGKEP + +# ٣ ۳ 𞣉 + (‎ ٣ ‎) 0663 ARABIC-INDIC DIGIT THREE +← (‎ ۳ ‎) 06F3 EXTENDED ARABIC-INDIC DIGIT THREE +← (‎ 𞣉 ‎) 1E8C9 MENDE KIKAKUI DIGIT THREE + +# ٤ ۴ + (‎ ٤ ‎) 0664 ARABIC-INDIC DIGIT FOUR +← (‎ ۴ ‎) 06F4 EXTENDED ARABIC-INDIC DIGIT FOUR + +# ٦ ۶ + (‎ ٦ ‎) 0666 ARABIC-INDIC DIGIT SIX +← (‎ ۶ ‎) 06F6 EXTENDED ARABIC-INDIC DIGIT SIX + +# ٩ ۹ १ 𑣤 + (‎ ٩ ‎) 0669 ARABIC-INDIC DIGIT NINE +← (‎ ۹ ‎) 06F9 EXTENDED ARABIC-INDIC DIGIT NINE +← (‎ १ ‎) 0967 DEVANAGARI DIGIT ONE +← (‎ 𑣤 ‎) 118E4 WARANG CITI DIGIT FOUR + +# ڡ ٯ ࢻ ࢼ 𞸞 𞸟 𞹟 𞹾 + (‎ ٯ ‎) 066F ARABIC LETTER DOTLESS QAF +← (‎ ڡ ‎) 06A1 ARABIC LETTER DOTLESS FEH +← (‎ ࢻ ‎) 08BB ARABIC LETTER AFRICAN FEH # →‎ڡ‎→ +← (‎ ࢼ ‎) 08BC ARABIC LETTER AFRICAN QAF +← (‎ 𞸞 ‎) 1EE1E ARABIC MATHEMATICAL DOTLESS FEH # →‎ڡ‎→ +← (‎ 𞸟 ‎) 1EE1F ARABIC MATHEMATICAL DOTLESS QAF +← (‎ 𞹟 ‎) 1EE5F ARABIC MATHEMATICAL TAILED DOTLESS QAF +← (‎ 𞹾 ‎) 1EE7E ARABIC MATHEMATICAL STRETCHED DOTLESS FEH # →‎ڡ‎→ + +# ڡۛ ٯۛ ڤ ڨ ﭪ ﭫ ﭬ ﭭ + (‎ ٯۛ ‎) 066F 06DB ARABIC LETTER DOTLESS QAF, ARABIC SMALL HIGH THREE DOTS +← (‎ ڡۛ ‎) 06A1 06DB ARABIC LETTER DOTLESS FEH, ARABIC SMALL HIGH THREE DOTS # →‎ڤ‎→→‎ڨ‎→ +← (‎ ڤ ‎) 06A4 ARABIC LETTER VEH # →‎ڨ‎→ +← (‎ ڨ ‎) 06A8 ARABIC LETTER QAF WITH THREE DOTS ABOVE +← (‎ ﭪ ‎) FB6A ARABIC LETTER VEH ISOLATED FORM # →‎ڤ‎→→‎ڨ‎→ +← (‎ ﭫ ‎) FB6B ARABIC LETTER VEH FINAL FORM # →‎ڤ‎→→‎ڨ‎→ +← (‎ ﭬ ‎) FB6C ARABIC LETTER VEH INITIAL FORM # →‎ڤ‎→→‎ڨ‎→ +← (‎ ﭭ ‎) FB6D ARABIC LETTER VEH MEDIAL FORM # →‎ڤ‎→→‎ڨ‎→ + +# ٱ ﭐ ﭑ + (‎ ٱ ‎) 0671 ARABIC LETTER ALEF WASLA +← (‎ ﭐ ‎) FB50 ARABIC LETTER ALEF WASLA ISOLATED FORM +← (‎ ﭑ ‎) FB51 ARABIC LETTER ALEF WASLA FINAL FORM + +# ٺ ﭞ ﭟ ﭠ ﭡ + (‎ ٺ ‎) 067A ARABIC LETTER TTEHEH +← (‎ ﭞ ‎) FB5E ARABIC LETTER TTEHEH ISOLATED FORM +← (‎ ﭟ ‎) FB5F ARABIC LETTER TTEHEH FINAL FORM +← (‎ ﭠ ‎) FB60 ARABIC LETTER TTEHEH INITIAL FORM +← (‎ ﭡ ‎) FB61 ARABIC LETTER TTEHEH MEDIAL FORM + +# ٻ ې ﭒ ﭓ ﭔ ﭕ ﯤ ﯥ ﯦ ﯧ + (‎ ٻ ‎) 067B ARABIC LETTER BEEH +← (‎ ې ‎) 06D0 ARABIC LETTER E +← (‎ ﭒ ‎) FB52 ARABIC LETTER BEEH ISOLATED FORM +← (‎ ﭓ ‎) FB53 ARABIC LETTER BEEH FINAL FORM +← (‎ ﭔ ‎) FB54 ARABIC LETTER BEEH INITIAL FORM +← (‎ ﭕ ‎) FB55 ARABIC LETTER BEEH MEDIAL FORM +← (‎ ﯤ ‎) FBE4 ARABIC LETTER E ISOLATED FORM # →‎ې‎→ +← (‎ ﯥ ‎) FBE5 ARABIC LETTER E FINAL FORM # →‎ې‎→ +← (‎ ﯦ ‎) FBE6 ARABIC LETTER E INITIAL FORM # →‎ې‎→ +← (‎ ﯧ ‎) FBE7 ARABIC LETTER E MEDIAL FORM # →‎ې‎→ + +# ٿ ﭢ ﭣ ﭤ ﭥ + (‎ ٿ ‎) 067F ARABIC LETTER TEHEH +← (‎ ﭢ ‎) FB62 ARABIC LETTER TEHEH ISOLATED FORM +← (‎ ﭣ ‎) FB63 ARABIC LETTER TEHEH FINAL FORM +← (‎ ﭤ ‎) FB64 ARABIC LETTER TEHEH INITIAL FORM +← (‎ ﭥ ‎) FB65 ARABIC LETTER TEHEH MEDIAL FORM + +# ڀ ﭚ ﭛ ﭜ ﭝ + (‎ ڀ ‎) 0680 ARABIC LETTER BEHEH +← (‎ ﭚ ‎) FB5A ARABIC LETTER BEHEH ISOLATED FORM +← (‎ ﭛ ‎) FB5B ARABIC LETTER BEHEH FINAL FORM +← (‎ ﭜ ‎) FB5C ARABIC LETTER BEHEH INITIAL FORM +← (‎ ﭝ ‎) FB5D ARABIC LETTER BEHEH MEDIAL FORM + +# ڃ ﭶ ﭷ ﭸ ﭹ + (‎ ڃ ‎) 0683 ARABIC LETTER NYEH +← (‎ ﭶ ‎) FB76 ARABIC LETTER NYEH ISOLATED FORM +← (‎ ﭷ ‎) FB77 ARABIC LETTER NYEH FINAL FORM +← (‎ ﭸ ‎) FB78 ARABIC LETTER NYEH INITIAL FORM +← (‎ ﭹ ‎) FB79 ARABIC LETTER NYEH MEDIAL FORM + +# ڄ ﭲ ﭳ ﭴ ﭵ + (‎ ڄ ‎) 0684 ARABIC LETTER DYEH +← (‎ ﭲ ‎) FB72 ARABIC LETTER DYEH ISOLATED FORM +← (‎ ﭳ ‎) FB73 ARABIC LETTER DYEH FINAL FORM +← (‎ ﭴ ‎) FB74 ARABIC LETTER DYEH INITIAL FORM +← (‎ ﭵ ‎) FB75 ARABIC LETTER DYEH MEDIAL FORM + +# چ ﭺ ﭻ ﭼ ﭽ + (‎ چ ‎) 0686 ARABIC LETTER TCHEH +← (‎ ﭺ ‎) FB7A ARABIC LETTER TCHEH ISOLATED FORM +← (‎ ﭻ ‎) FB7B ARABIC LETTER TCHEH FINAL FORM +← (‎ ﭼ ‎) FB7C ARABIC LETTER TCHEH INITIAL FORM +← (‎ ﭽ ‎) FB7D ARABIC LETTER TCHEH MEDIAL FORM + +# ڇ ﭾ ﭿ ﮀ ﮁ + (‎ ڇ ‎) 0687 ARABIC LETTER TCHEHEH +← (‎ ﭾ ‎) FB7E ARABIC LETTER TCHEHEH ISOLATED FORM +← (‎ ﭿ ‎) FB7F ARABIC LETTER TCHEHEH FINAL FORM +← (‎ ﮀ ‎) FB80 ARABIC LETTER TCHEHEH INITIAL FORM +← (‎ ﮁ ‎) FB81 ARABIC LETTER TCHEHEH MEDIAL FORM + +# ڊؕ ڋ + (‎ ڊؕ ‎) 068A 0615 ARABIC LETTER DAL WITH DOT BELOW, ARABIC SMALL HIGH TAH +← (‎ ڋ ‎) 068B ARABIC LETTER DAL WITH DOT BELOW AND SMALL TAH + +# ڌ ﮄ ﮅ + (‎ ڌ ‎) 068C ARABIC LETTER DAHAL +← (‎ ﮄ ‎) FB84 ARABIC LETTER DAHAL ISOLATED FORM +← (‎ ﮅ ‎) FB85 ARABIC LETTER DAHAL FINAL FORM + +# ڍ ﮂ ﮃ + (‎ ڍ ‎) 068D ARABIC LETTER DDAHAL +← (‎ ﮂ ‎) FB82 ARABIC LETTER DDAHAL ISOLATED FORM +← (‎ ﮃ ‎) FB83 ARABIC LETTER DDAHAL FINAL FORM + +# ڗؕ ݱ + (‎ ڗؕ ‎) 0697 0615 ARABIC LETTER REH WITH TWO DOTS ABOVE, ARABIC SMALL HIGH TAH +← (‎ ݱ ‎) 0771 ARABIC LETTER REH WITH SMALL ARABIC LETTER TAH AND TWO DOTS + +# ڢۛ ࢤ + (‎ ڢۛ ‎) 06A2 06DB ARABIC LETTER FEH WITH DOT MOVED BELOW, ARABIC SMALL HIGH THREE DOTS +← (‎ ࢤ ‎) 08A4 ARABIC LETTER FEH WITH DOT BELOW AND THREE DOTS ABOVE + +# ڦ ﭮ ﭯ ﭰ ﭱ + (‎ ڦ ‎) 06A6 ARABIC LETTER PEHEH +← (‎ ﭮ ‎) FB6E ARABIC LETTER PEHEH ISOLATED FORM +← (‎ ﭯ ‎) FB6F ARABIC LETTER PEHEH FINAL FORM +← (‎ ﭰ ‎) FB70 ARABIC LETTER PEHEH INITIAL FORM +← (‎ ﭱ ‎) FB71 ARABIC LETTER PEHEH MEDIAL FORM + +# ڬ ݢ + (‎ ڬ ‎) 06AC ARABIC LETTER KAF WITH DOT ABOVE +← (‎ ݢ ‎) 0762 ARABIC LETTER KEHEH WITH DOT ABOVE + +# گ ࢰ ﮒ ﮓ ﮔ ﮕ + (‎ گ ‎) 06AF ARABIC LETTER GAF +← (‎ ࢰ ‎) 08B0 ARABIC LETTER GAF WITH INVERTED STROKE +← (‎ ﮒ ‎) FB92 ARABIC LETTER GAF ISOLATED FORM +← (‎ ﮓ ‎) FB93 ARABIC LETTER GAF FINAL FORM +← (‎ ﮔ ‎) FB94 ARABIC LETTER GAF INITIAL FORM +← (‎ ﮕ ‎) FB95 ARABIC LETTER GAF MEDIAL FORM + +# گۛ ڴ + (‎ گۛ ‎) 06AF 06DB ARABIC LETTER GAF, ARABIC SMALL HIGH THREE DOTS +← (‎ ڴ ‎) 06B4 ARABIC LETTER GAF WITH THREE DOTS ABOVE + +# ڱ ﮚ ﮛ ﮜ ﮝ + (‎ ڱ ‎) 06B1 ARABIC LETTER NGOEH +← (‎ ﮚ ‎) FB9A ARABIC LETTER NGOEH ISOLATED FORM +← (‎ ﮛ ‎) FB9B ARABIC LETTER NGOEH FINAL FORM +← (‎ ﮜ ‎) FB9C ARABIC LETTER NGOEH INITIAL FORM +← (‎ ﮝ ‎) FB9D ARABIC LETTER NGOEH MEDIAL FORM + +# ڳ ﮖ ﮗ ﮘ ﮙ + (‎ ڳ ‎) 06B3 ARABIC LETTER GUEH +← (‎ ﮖ ‎) FB96 ARABIC LETTER GUEH ISOLATED FORM +← (‎ ﮗ ‎) FB97 ARABIC LETTER GUEH FINAL FORM +← (‎ ﮘ ‎) FB98 ARABIC LETTER GUEH INITIAL FORM +← (‎ ﮙ ‎) FB99 ARABIC LETTER GUEH MEDIAL FORM + +# ۀ ۂ ﮤ ﮥ + (‎ ۀ ‎) 06C0 ARABIC LETTER HEH WITH YEH ABOVE +← (‎ ۂ ‎) 06C2 ARABIC LETTER HEH GOAL WITH HAMZA ABOVE # →‎ﮤ‎→ +← (‎ ﮤ ‎) FBA4 ARABIC LETTER HEH WITH YEH ABOVE ISOLATED FORM +← (‎ ﮥ ‎) FBA5 ARABIC LETTER HEH WITH YEH ABOVE FINAL FORM + +# ۅ ﯠ ﯡ + (‎ ۅ ‎) 06C5 ARABIC LETTER KIRGHIZ OE +← (‎ ﯠ ‎) FBE0 ARABIC LETTER KIRGHIZ OE ISOLATED FORM +← (‎ ﯡ ‎) FBE1 ARABIC LETTER KIRGHIZ OE FINAL FORM + +# ۓ ﮰ ﮱ + (‎ ۓ ‎) 06D3 ARABIC LETTER YEH BARREE WITH HAMZA ABOVE +← (‎ ﮰ ‎) FBB0 ARABIC LETTER YEH BARREE WITH HAMZA ABOVE ISOLATED FORM +← (‎ ﮱ ‎) FBB1 ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM + +# ۛ ⃛ ᪴ + (‎ ۛ ‎) 06DB ARABIC SMALL HIGH THREE DOTS +← (‎ ⃛ ‎) 20DB COMBINING THREE DOTS ABOVE # →᪴→ +← (‎ ᪴ ‎) 1AB4 COMBINING TRIPLE DOT + +# ܼ ݂ + (‎ ܼ ‎) 073C SYRIAC HBASA-ESASA DOTTED +← (‎ ݂ ‎) 0742 SYRIAC RUKKAKHA + +# ݔ ݧ ࢩ + (‎ ݔ ‎) 0754 ARABIC LETTER BEH WITH TWO DOTS BELOW AND DOT ABOVE +← (‎ ݧ ‎) 0767 ARABIC LETTER NOON WITH TWO DOTS BELOW +← (‎ ࢩ ‎) 08A9 ARABIC LETTER YEH WITH TWO DOTS BELOW AND DOT ABOVE + +# अॆ ऄ + (‎ ऄ ‎) 0904 DEVANAGARI LETTER SHORT A +← (‎ अॆ ‎) 0905 0946 DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN SHORT E + +# अा आ + (‎ अा ‎) 0905 093E DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN AA +← (‎ आ ‎) 0906 DEVANAGARI LETTER AA + +# अाॆ अॊ आॆ ऒ + (‎ अाॆ ‎) 0905 093E 0946 DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN AA, DEVANAGARI VOWEL SIGN SHORT E +← (‎ अॊ ‎) 0905 094A DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN SHORT O # →आॆ→ +← (‎ आॆ ‎) 0906 0946 DEVANAGARI LETTER AA, DEVANAGARI VOWEL SIGN SHORT E +← (‎ ऒ ‎) 0912 DEVANAGARI LETTER SHORT O # →अॊ→→आॆ→ + +# अाे अो आे ओ + (‎ अाे ‎) 0905 093E 0947 DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN AA, DEVANAGARI VOWEL SIGN E +← (‎ अो ‎) 0905 094B DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN O # →आे→ +← (‎ आे ‎) 0906 0947 DEVANAGARI LETTER AA, DEVANAGARI VOWEL SIGN E +← (‎ ओ ‎) 0913 DEVANAGARI LETTER O # →अो→→आे→ + +# अाै अौ आै औ + (‎ अाै ‎) 0905 093E 0948 DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN AA, DEVANAGARI VOWEL SIGN AI +← (‎ अौ ‎) 0905 094C DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN AU # →आै→ +← (‎ आै ‎) 0906 0948 DEVANAGARI LETTER AA, DEVANAGARI VOWEL SIGN AI +← (‎ औ ‎) 0914 DEVANAGARI LETTER AU # →अौ→→आै→ + +# अॉ ऑ + (‎ अॉ ‎) 0905 0949 DEVANAGARI LETTER A, DEVANAGARI VOWEL SIGN CANDRA O +← (‎ ऑ ‎) 0911 DEVANAGARI LETTER CANDRA O + +# र्इ ई + (‎ ई ‎) 0908 DEVANAGARI LETTER II +← (‎ र्इ ‎) 0930 094D 0907 DEVANAGARI LETTER RA, DEVANAGARI SIGN VIRAMA, DEVANAGARI LETTER I + +# एॅ ऍ + (‎ ऍ ‎) 090D DEVANAGARI LETTER CANDRA E +← (‎ एॅ ‎) 090F 0945 DEVANAGARI LETTER E, DEVANAGARI VOWEL SIGN CANDRA E + +# एॆ ऎ + (‎ ऎ ‎) 090E DEVANAGARI LETTER SHORT E +← (‎ एॆ ‎) 090F 0946 DEVANAGARI LETTER E, DEVANAGARI VOWEL SIGN SHORT E + +# एे ऐ + (‎ एे ‎) 090F 0947 DEVANAGARI LETTER E, DEVANAGARI VOWEL SIGN E +← (‎ ऐ ‎) 0910 DEVANAGARI LETTER AI + +# ऺ 𑇋 + (‎ ऺ ‎) 093A DEVANAGARI VOWEL SIGN OE +← (‎ 𑇋 ‎) 111CB SHARADA VOWEL MODIFIER MARK + +# ऽ ઽ + (‎ ऽ ‎) 093D DEVANAGARI SIGN AVAGRAHA +← (‎ ઽ ‎) 0ABD GUJARATI SIGN AVAGRAHA + +# ु ુ + (‎ ु ‎) 0941 DEVANAGARI VOWEL SIGN U +← (‎ ુ ‎) 0AC1 GUJARATI VOWEL SIGN U + +# ू ૂ + (‎ ू ‎) 0942 DEVANAGARI VOWEL SIGN UU +← (‎ ૂ ‎) 0AC2 GUJARATI VOWEL SIGN UU + +# ॆ ੋ + (‎ ॆ ‎) 0946 DEVANAGARI VOWEL SIGN SHORT E +← (‎ ੋ ‎) 0A4B GURMUKHI VOWEL SIGN OO + +# ् ੍ ્ + (‎ ् ‎) 094D DEVANAGARI SIGN VIRAMA +← (‎ ੍ ‎) 0A4D GURMUKHI SIGN VIRAMA +← (‎ ્ ‎) 0ACD GUJARATI SIGN VIRAMA + +# । ꠰ + (‎ । ‎) 0964 DEVANAGARI DANDA +← (‎ ꠰ ‎) A830 NORTH INDIC FRACTION ONE QUARTER + +# ।। ॥ + (‎ ।। ‎) 0964 0964 DEVANAGARI DANDA, DEVANAGARI DANDA +← (‎ ॥ ‎) 0965 DEVANAGARI DOUBLE DANDA + +# २ ૨ + (‎ २ ‎) 0968 DEVANAGARI DIGIT TWO +← (‎ ૨ ‎) 0AE8 GUJARATI DIGIT TWO + +# ३ ૩ + (‎ ३ ‎) 0969 DEVANAGARI DIGIT THREE +← (‎ ૩ ‎) 0AE9 GUJARATI DIGIT THREE + +# ४ ૪ + (‎ ४ ‎) 096A DEVANAGARI DIGIT FOUR +← (‎ ૪ ‎) 0AEA GUJARATI DIGIT FOUR + +# ८ ૮ + (‎ ८ ‎) 096E DEVANAGARI DIGIT EIGHT +← (‎ ૮ ‎) 0AEE GUJARATI DIGIT EIGHT + +# ॰ ૰ ⚬ 𑂻 𑇇 + (‎ ॰ ‎) 0970 DEVANAGARI ABBREVIATION SIGN +← (‎ ૰ ‎) 0AF0 GUJARATI ABBREVIATION SIGN +← (‎ ⚬ ‎) 26AC MEDIUM SMALL WHITE CIRCLE +← (‎ 𑂻 ‎) 110BB KAITHI ABBREVIATION SIGN +← (‎ 𑇇 ‎) 111C7 SHARADA ABBREVIATION SIGN + +# ঃ ਃ ః ಃ ഃ ඃ း 𑓁 + (‎ ঃ ‎) 0983 BENGALI SIGN VISARGA +← (‎ ਃ ‎) 0A03 GURMUKHI SIGN VISARGA +← (‎ ః ‎) 0C03 TELUGU SIGN VISARGA # →ਃ→ +← (‎ ಃ ‎) 0C83 KANNADA SIGN VISARGA # →ః→→ਃ→ +← (‎ ഃ ‎) 0D03 MALAYALAM SIGN VISARGA # →ಃ→→ః→→ਃ→ +← (‎ ඃ ‎) 0D83 SINHALA SIGN VISARGAYA # →ഃ→→ಃ→→ః→→ਃ→ +← (‎ း ‎) 1038 MYANMAR SIGN VISARGA # →ඃ→→ഃ→→ಃ→→ః→→ਃ→ +← (‎ 𑓁 ‎) 114C1 TIRHUTA SIGN VISARGA + +# অা আ + (‎ অা ‎) 0985 09BE BENGALI LETTER A, BENGALI VOWEL SIGN AA +← (‎ আ ‎) 0986 BENGALI LETTER AA + +# ঋৃ ঌৢ ৠ ৡ + (‎ ঋৃ ‎) 098B 09C3 BENGALI LETTER VOCALIC R, BENGALI VOWEL SIGN VOCALIC R +← (‎ ঌৢ ‎) 098C 09E2 BENGALI LETTER VOCALIC L, BENGALI VOWEL SIGN VOCALIC L # →ৠ→ +← (‎ ৠ ‎) 09E0 BENGALI LETTER VOCALIC RR +← (‎ ৡ ‎) 09E1 BENGALI LETTER VOCALIC LL # →ঌৢ→→ৠ→ + +# ঘ 𑒒 + (‎ ঘ ‎) 0998 BENGALI LETTER GHA +← (‎ 𑒒 ‎) 11492 TIRHUTA LETTER GHA + +# চ 𑒔 + (‎ চ ‎) 099A BENGALI LETTER CA +← (‎ 𑒔 ‎) 11494 TIRHUTA LETTER CA + +# জ 𑒖 + (‎ জ ‎) 099C BENGALI LETTER JA +← (‎ 𑒖 ‎) 11496 TIRHUTA LETTER JA + +# ঞ 𑒘 + (‎ ঞ ‎) 099E BENGALI LETTER NYA +← (‎ 𑒘 ‎) 11498 TIRHUTA LETTER NYA + +# ট 𑒙 + (‎ ট ‎) 099F BENGALI LETTER TTA +← (‎ 𑒙 ‎) 11499 TIRHUTA LETTER TTA + +# ড 𑒛 + (‎ ড ‎) 09A1 BENGALI LETTER DDA +← (‎ 𑒛 ‎) 1149B TIRHUTA LETTER DDA + +# ণ 𑒪 + (‎ ণ ‎) 09A3 BENGALI LETTER NNA +← (‎ 𑒪 ‎) 114AA TIRHUTA LETTER LA + +# ত 𑒞 + (‎ ত ‎) 09A4 BENGALI LETTER TA +← (‎ 𑒞 ‎) 1149E TIRHUTA LETTER TA + +# থ 𑒟 + (‎ থ ‎) 09A5 BENGALI LETTER THA +← (‎ 𑒟 ‎) 1149F TIRHUTA LETTER THA + +# দ 𑒠 + (‎ দ ‎) 09A6 BENGALI LETTER DA +← (‎ 𑒠 ‎) 114A0 TIRHUTA LETTER DA + +# ধ 𑒡 + (‎ ধ ‎) 09A7 BENGALI LETTER DHA +← (‎ 𑒡 ‎) 114A1 TIRHUTA LETTER DHA + +# ন 𑒢 + (‎ ন ‎) 09A8 BENGALI LETTER NA +← (‎ 𑒢 ‎) 114A2 TIRHUTA LETTER NA + +# প 𑒣 + (‎ প ‎) 09AA BENGALI LETTER PA +← (‎ 𑒣 ‎) 114A3 TIRHUTA LETTER PA + +# ব 𑒩 + (‎ ব ‎) 09AC BENGALI LETTER BA +← (‎ 𑒩 ‎) 114A9 TIRHUTA LETTER RA + +# ম 𑒧 + (‎ ম ‎) 09AE BENGALI LETTER MA +← (‎ 𑒧 ‎) 114A7 TIRHUTA LETTER MA + +# য 𑒨 + (‎ য ‎) 09AF BENGALI LETTER YA +← (‎ 𑒨 ‎) 114A8 TIRHUTA LETTER YA + +# র 𑒫 + (‎ র ‎) 09B0 BENGALI LETTER RA +← (‎ 𑒫 ‎) 114AB TIRHUTA LETTER VA + +# ল 𑒝 + (‎ ল ‎) 09B2 BENGALI LETTER LA +← (‎ 𑒝 ‎) 1149D TIRHUTA LETTER NNA + +# ষ 𑒭 + (‎ ষ ‎) 09B7 BENGALI LETTER SSA +← (‎ 𑒭 ‎) 114AD TIRHUTA LETTER SSA + +# স 𑒮 + (‎ স ‎) 09B8 BENGALI LETTER SA +← (‎ 𑒮 ‎) 114AE TIRHUTA LETTER SA + +# ঽ 𑓄 + (‎ ঽ ‎) 09BD BENGALI SIGN AVAGRAHA +← (‎ 𑓄 ‎) 114C4 TIRHUTA SIGN AVAGRAHA + +# া 𑒰 + (‎ া ‎) 09BE BENGALI VOWEL SIGN AA +← (‎ 𑒰 ‎) 114B0 TIRHUTA VOWEL SIGN AA + +# ি 𑒱 + (‎ ি ‎) 09BF BENGALI VOWEL SIGN I +← (‎ 𑒱 ‎) 114B1 TIRHUTA VOWEL SIGN I + +# ে 𑒹 + (‎ ে ‎) 09C7 BENGALI VOWEL SIGN E +← (‎ 𑒹 ‎) 114B9 TIRHUTA VOWEL SIGN E + +# ো 𑒼 + (‎ ো ‎) 09CB BENGALI VOWEL SIGN O +← (‎ 𑒼 ‎) 114BC TIRHUTA VOWEL SIGN O + +# ৌ 𑒾 + (‎ ৌ ‎) 09CC BENGALI VOWEL SIGN AU +← (‎ 𑒾 ‎) 114BE TIRHUTA VOWEL SIGN AU + +# ্ 𑓂 + (‎ ্ ‎) 09CD BENGALI SIGN VIRAMA +← (‎ 𑓂 ‎) 114C2 TIRHUTA SIGN VIRAMA + +# ৗ 𑒽 + (‎ ৗ ‎) 09D7 BENGALI AU LENGTH MARK +← (‎ 𑒽 ‎) 114BD TIRHUTA VOWEL SIGN SHORT O + +# ১ 𑓑 + (‎ ১ ‎) 09E7 BENGALI DIGIT ONE +← (‎ 𑓑 ‎) 114D1 TIRHUTA DIGIT ONE + +# ২ 𑓒 + (‎ ২ ‎) 09E8 BENGALI DIGIT TWO +← (‎ 𑓒 ‎) 114D2 TIRHUTA DIGIT TWO + +# ৬ 𑓖 + (‎ ৬ ‎) 09EC BENGALI DIGIT SIX +← (‎ 𑓖 ‎) 114D6 TIRHUTA DIGIT SIX + +# ਅਾ ਆ + (‎ ਅਾ ‎) 0A05 0A3E GURMUKHI LETTER A, GURMUKHI VOWEL SIGN AA +← (‎ ਆ ‎) 0A06 GURMUKHI LETTER AA + +# ਅੈ ਐ + (‎ ਅੈ ‎) 0A05 0A48 GURMUKHI LETTER A, GURMUKHI VOWEL SIGN AI +← (‎ ਐ ‎) 0A10 GURMUKHI LETTER AI + +# ਅੌ ਔ + (‎ ਅੌ ‎) 0A05 0A4C GURMUKHI LETTER A, GURMUKHI VOWEL SIGN AU +← (‎ ਔ ‎) 0A14 GURMUKHI LETTER AU + +# ੲਿ ਇ + (‎ ਇ ‎) 0A07 GURMUKHI LETTER I +← (‎ ੲਿ ‎) 0A72 0A3F GURMUKHI IRI, GURMUKHI VOWEL SIGN I + +# ੲੀ ਈ + (‎ ਈ ‎) 0A08 GURMUKHI LETTER II +← (‎ ੲੀ ‎) 0A72 0A40 GURMUKHI IRI, GURMUKHI VOWEL SIGN II + +# ੳੁ ਉ + (‎ ਉ ‎) 0A09 GURMUKHI LETTER U +← (‎ ੳੁ ‎) 0A73 0A41 GURMUKHI URA, GURMUKHI VOWEL SIGN U + +# ੳੂ ਊ + (‎ ਊ ‎) 0A0A GURMUKHI LETTER UU +← (‎ ੳੂ ‎) 0A73 0A42 GURMUKHI URA, GURMUKHI VOWEL SIGN UU + +# ੲੇ ਏ + (‎ ਏ ‎) 0A0F GURMUKHI LETTER EE +← (‎ ੲੇ ‎) 0A72 0A47 GURMUKHI IRI, GURMUKHI VOWEL SIGN EE + +# અા આ + (‎ અા ‎) 0A85 0ABE GUJARATI LETTER A, GUJARATI VOWEL SIGN AA +← (‎ આ ‎) 0A86 GUJARATI LETTER AA + +# અાૅ અૉ આૅ ઑ + (‎ અાૅ ‎) 0A85 0ABE 0AC5 GUJARATI LETTER A, GUJARATI VOWEL SIGN AA, GUJARATI VOWEL SIGN CANDRA E +← (‎ અૉ ‎) 0A85 0AC9 GUJARATI LETTER A, GUJARATI VOWEL SIGN CANDRA O # →આૅ→ +← (‎ આૅ ‎) 0A86 0AC5 GUJARATI LETTER AA, GUJARATI VOWEL SIGN CANDRA E +← (‎ ઑ ‎) 0A91 GUJARATI VOWEL CANDRA O # →અૉ→→આૅ→ + +# અાે અો આે ઓ + (‎ અાે ‎) 0A85 0ABE 0AC7 GUJARATI LETTER A, GUJARATI VOWEL SIGN AA, GUJARATI VOWEL SIGN E +← (‎ અો ‎) 0A85 0ACB GUJARATI LETTER A, GUJARATI VOWEL SIGN O # →આે→ +← (‎ આે ‎) 0A86 0AC7 GUJARATI LETTER AA, GUJARATI VOWEL SIGN E +← (‎ ઓ ‎) 0A93 GUJARATI LETTER O # →અો→→આે→ + +# અાૈ અૌ આૈ ઔ + (‎ અાૈ ‎) 0A85 0ABE 0AC8 GUJARATI LETTER A, GUJARATI VOWEL SIGN AA, GUJARATI VOWEL SIGN AI +← (‎ અૌ ‎) 0A85 0ACC GUJARATI LETTER A, GUJARATI VOWEL SIGN AU # →આૈ→ +← (‎ આૈ ‎) 0A86 0AC8 GUJARATI LETTER AA, GUJARATI VOWEL SIGN AI +← (‎ ઔ ‎) 0A94 GUJARATI LETTER AU # →અૌ→→આૈ→ + +# અૅ ઍ + (‎ અૅ ‎) 0A85 0AC5 GUJARATI LETTER A, GUJARATI VOWEL SIGN CANDRA E +← (‎ ઍ ‎) 0A8D GUJARATI VOWEL CANDRA E + +# અે એ + (‎ અે ‎) 0A85 0AC7 GUJARATI LETTER A, GUJARATI VOWEL SIGN E +← (‎ એ ‎) 0A8F GUJARATI LETTER E + +# અૈ ઐ + (‎ અૈ ‎) 0A85 0AC8 GUJARATI LETTER A, GUJARATI VOWEL SIGN AI +← (‎ ઐ ‎) 0A90 GUJARATI LETTER AI + +# ଅା ଆ + (‎ ଅା ‎) 0B05 0B3E ORIYA LETTER A, ORIYA VOWEL SIGN AA +← (‎ ଆ ‎) 0B06 ORIYA LETTER AA + +# அ ௮ + (‎ அ ‎) 0B85 TAMIL LETTER A +← (‎ ௮ ‎) 0BEE TAMIL DIGIT EIGHT + +# ஈ ர ா + (‎ ஈ ‎) 0B88 TAMIL LETTER II +← (‎ ர ‎) 0BB0 TAMIL LETTER RA # →ா→ +← (‎ ா ‎) 0BBE TAMIL VOWEL SIGN AA + +# ஈு ரு ௫ + (‎ ஈு ‎) 0B88 0BC1 TAMIL LETTER II, TAMIL VOWEL SIGN U +← (‎ ரு ‎) 0BB0 0BC1 TAMIL LETTER RA, TAMIL VOWEL SIGN U +← (‎ ௫ ‎) 0BEB TAMIL DIGIT FIVE # →ரு→ + +# உ ௨ ഉ + (‎ உ ‎) 0B89 TAMIL LETTER U +← (‎ ௨ ‎) 0BE8 TAMIL DIGIT TWO +← (‎ ഉ ‎) 0D09 MALAYALAM LETTER U + +# உள ஊ + (‎ உள ‎) 0B89 0BB3 TAMIL LETTER U, TAMIL LETTER LLA +← (‎ ஊ ‎) 0B8A TAMIL LETTER UU + +# உൗ ഉൗ ഊ + (‎ உൗ ‎) 0B89 0D57 TAMIL LETTER U, MALAYALAM AU LENGTH MARK +← (‎ ഉൗ ‎) 0D09 0D57 MALAYALAM LETTER U, MALAYALAM AU LENGTH MARK +← (‎ ഊ ‎) 0D0A MALAYALAM LETTER UU # →ഉൗ→ + +# எ ௭ + (‎ எ ‎) 0B8E TAMIL LETTER E +← (‎ ௭ ‎) 0BED TAMIL DIGIT SEVEN + +# எவ எ௳ ௷ + (‎ எவ ‎) 0B8E 0BB5 TAMIL LETTER E, TAMIL LETTER VA +← (‎ எ௳ ‎) 0B8E 0BF3 TAMIL LETTER E, TAMIL DAY SIGN # →௷→ +← (‎ ௷ ‎) 0BF7 TAMIL CREDIT SIGN + +# ஐ ஜ ജ + (‎ ஐ ‎) 0B90 TAMIL LETTER AI +← (‎ ஜ ‎) 0B9C TAMIL LETTER JA +← (‎ ജ ‎) 0D1C MALAYALAM LETTER JA # →ஜ→ + +# க ௧ + (‎ க ‎) 0B95 TAMIL LETTER KA +← (‎ ௧ ‎) 0BE7 TAMIL DIGIT ONE + +# ச ௪ + (‎ ச ‎) 0B9A TAMIL LETTER CA +← (‎ ௪ ‎) 0BEA TAMIL DIGIT FOUR + +# சு ௬ + (‎ சு ‎) 0B9A 0BC1 TAMIL LETTER CA, TAMIL VOWEL SIGN U +← (‎ ௬ ‎) 0BEC TAMIL DIGIT SIX + +# சூ ௲ + (‎ சூ ‎) 0B9A 0BC2 TAMIL LETTER CA, TAMIL VOWEL SIGN UU +← (‎ ௲ ‎) 0BF2 TAMIL NUMBER ONE THOUSAND + +# டி ഺ + (‎ டி ‎) 0B9F 0BBF TAMIL LETTER TTA, TAMIL VOWEL SIGN I +← (‎ ഺ ‎) 0D3A MALAYALAM LETTER TTTA + +# ண ണ + (‎ ண ‎) 0BA3 TAMIL LETTER NNA +← (‎ ണ ‎) 0D23 MALAYALAM LETTER NNA + +# நீ ௺ + (‎ நீ ‎) 0BA8 0BC0 TAMIL LETTER NA, TAMIL VOWEL SIGN II +← (‎ ௺ ‎) 0BFA TAMIL NUMBER SIGN + +# ன ை + (‎ ன ‎) 0BA9 TAMIL LETTER NNNA +← (‎ ை ‎) 0BC8 TAMIL VOWEL SIGN AI + +# மீ ௴ + (‎ மீ ‎) 0BAE 0BC0 TAMIL LETTER MA, TAMIL VOWEL SIGN II +← (‎ ௴ ‎) 0BF4 TAMIL MONTH SIGN + +# ய ௰ + (‎ ய ‎) 0BAF TAMIL LETTER YA +← (‎ ௰ ‎) 0BF0 TAMIL NUMBER TEN + +# ள ௗ + (‎ ள ‎) 0BB3 TAMIL LETTER LLA +← (‎ ௗ ‎) 0BD7 TAMIL AU LENGTH MARK + +# ழ ഴ + (‎ ழ ‎) 0BB4 TAMIL LETTER LLLA +← (‎ ഴ ‎) 0D34 MALAYALAM LETTER LLLA + +# ஶ ശ + (‎ ஶ ‎) 0BB6 TAMIL LETTER SHA +← (‎ ശ ‎) 0D36 MALAYALAM LETTER SHA + +# ஷ ௸ + (‎ ஷ ‎) 0BB7 TAMIL LETTER SSA +← (‎ ௸ ‎) 0BF8 TAMIL AS ABOVE SIGN + +# ி ി ീ + (‎ ி ‎) 0BBF TAMIL VOWEL SIGN I +← (‎ ി ‎) 0D3F MALAYALAM VOWEL SIGN I +← (‎ ീ ‎) 0D40 MALAYALAM VOWEL SIGN II + +# ெஈ ெர ொ + (‎ ெஈ ‎) 0BC6 0B88 TAMIL VOWEL SIGN E, TAMIL LETTER II +← (‎ ெர ‎) 0BC6 0BB0 TAMIL VOWEL SIGN E, TAMIL LETTER RA +← (‎ ொ ‎) 0BCA TAMIL VOWEL SIGN O # →ெர→ + +# ெள ௌ + (‎ ெள ‎) 0BC6 0BB3 TAMIL VOWEL SIGN E, TAMIL LETTER LLA +← (‎ ௌ ‎) 0BCC TAMIL VOWEL SIGN AU + +# ேஈ ேர ோ + (‎ ேஈ ‎) 0BC7 0B88 TAMIL VOWEL SIGN EE, TAMIL LETTER II +← (‎ ேர ‎) 0BC7 0BB0 TAMIL VOWEL SIGN EE, TAMIL LETTER RA +← (‎ ோ ‎) 0BCB TAMIL VOWEL SIGN OO # →ேர→ + +# ௳ ௵ + (‎ ௳ ‎) 0BF3 TAMIL DAY SIGN +← (‎ ௵ ‎) 0BF5 TAMIL YEAR SIGN + +# అ ಅ + (‎ అ ‎) 0C05 TELUGU LETTER A +← (‎ ಅ ‎) 0C85 KANNADA LETTER A + +# ఆ ಆ + (‎ ఆ ‎) 0C06 TELUGU LETTER AA +← (‎ ಆ ‎) 0C86 KANNADA LETTER AA + +# ఇ ಇ + (‎ ఇ ‎) 0C07 TELUGU LETTER I +← (‎ ಇ ‎) 0C87 KANNADA LETTER I + +# ఋా ౠ + (‎ ఋా ‎) 0C0B 0C3E TELUGU LETTER VOCALIC R, TELUGU VOWEL SIGN AA +← (‎ ౠ ‎) 0C60 TELUGU LETTER VOCALIC RR + +# ఌా ౡ + (‎ ఌా ‎) 0C0C 0C3E TELUGU LETTER VOCALIC L, TELUGU VOWEL SIGN AA +← (‎ ౡ ‎) 0C61 TELUGU LETTER VOCALIC LL + +# ఒ ಒ + (‎ ఒ ‎) 0C12 TELUGU LETTER O +← (‎ ಒ ‎) 0C92 KANNADA LETTER O + +# ఒౌ ఒೌ ಒೌ ఔ ಔ + (‎ ఒౌ ‎) 0C12 0C4C TELUGU LETTER O, TELUGU VOWEL SIGN AU +← (‎ ఒೌ ‎) 0C12 0CCC TELUGU LETTER O, KANNADA VOWEL SIGN AU # →ಒೌ→→ಔ→→ఔ→ +← (‎ ಒೌ ‎) 0C92 0CCC KANNADA LETTER O, KANNADA VOWEL SIGN AU # →ಔ→→ఔ→ +← (‎ ఔ ‎) 0C14 TELUGU LETTER AU +← (‎ ಔ ‎) 0C94 KANNADA LETTER AU # →ఔ→ + +# ఒౕ ఓ ಓ + (‎ ఒౕ ‎) 0C12 0C55 TELUGU LETTER O, TELUGU LENGTH MARK +← (‎ ఓ ‎) 0C13 TELUGU LETTER OO +← (‎ ಓ ‎) 0C93 KANNADA LETTER OO # →ఓ→ + +# జ ಜ + (‎ జ ‎) 0C1C TELUGU LETTER JA +← (‎ ಜ ‎) 0C9C KANNADA LETTER JA + +# ఞ ಞ + (‎ ఞ ‎) 0C1E TELUGU LETTER NYA +← (‎ ಞ ‎) 0C9E KANNADA LETTER NYA + +# రּ ఠ + (‎ ఠ ‎) 0C20 TELUGU LETTER TTHA +← (‎ రּ ‎) 0C30 05BC TELUGU LETTER RA, HEBREW POINT DAGESH OR MAPIQ + +# డ̣ ఢ + (‎ డ̣ ‎) 0C21 0323 TELUGU LETTER DDA, COMBINING DOT BELOW +← (‎ ఢ ‎) 0C22 TELUGU LETTER DDHA + +# ణ ಣ + (‎ ణ ‎) 0C23 TELUGU LETTER NNA +← (‎ ಣ ‎) 0CA3 KANNADA LETTER NNA + +# ధּ థ + (‎ థ ‎) 0C25 TELUGU LETTER THA +← (‎ ధּ ‎) 0C27 05BC TELUGU LETTER DHA, HEBREW POINT DAGESH OR MAPIQ + +# బ̣ భ + (‎ బ̣ ‎) 0C2C 0323 TELUGU LETTER BA, COMBINING DOT BELOW +← (‎ భ ‎) 0C2D TELUGU LETTER BHA + +# వు మ + (‎ మ ‎) 0C2E TELUGU LETTER MA +← (‎ వు ‎) 0C35 0C41 TELUGU LETTER VA, TELUGU VOWEL SIGN U + +# య ಯ + (‎ య ‎) 0C2F TELUGU LETTER YA +← (‎ ಯ ‎) 0CAF KANNADA LETTER YA + +# ఱ ಱ + (‎ ఱ ‎) 0C31 TELUGU LETTER RRA +← (‎ ಱ ‎) 0CB1 KANNADA LETTER RRA + +# ల ಲ + (‎ ల ‎) 0C32 TELUGU LETTER LA +← (‎ ಲ ‎) 0CB2 KANNADA LETTER LA + +# వ̣ ష + (‎ వ̣ ‎) 0C35 0323 TELUGU LETTER VA, COMBINING DOT BELOW +← (‎ ష ‎) 0C37 TELUGU LETTER SSA + +# వా హ + (‎ వా ‎) 0C35 0C3E TELUGU LETTER VA, TELUGU VOWEL SIGN AA +← (‎ హ ‎) 0C39 TELUGU LETTER HA + +# ుా ూ + (‎ ుా ‎) 0C41 0C3E TELUGU VOWEL SIGN U, TELUGU VOWEL SIGN AA +← (‎ ూ ‎) 0C42 TELUGU VOWEL SIGN UU + +# ృా ౄ + (‎ ృా ‎) 0C43 0C3E TELUGU VOWEL SIGN VOCALIC R, TELUGU VOWEL SIGN AA +← (‎ ౄ ‎) 0C44 TELUGU VOWEL SIGN VOCALIC RR + +# ౧ ೧ + (‎ ౧ ‎) 0C67 TELUGU DIGIT ONE +← (‎ ೧ ‎) 0CE7 KANNADA DIGIT ONE + +# ౨ ೨ + (‎ ౨ ‎) 0C68 TELUGU DIGIT TWO +← (‎ ೨ ‎) 0CE8 KANNADA DIGIT TWO + +# ౯ ೯ + (‎ ౯ ‎) 0C6F TELUGU DIGIT NINE +← (‎ ೯ ‎) 0CEF KANNADA DIGIT NINE + +# ಌಾ ೡ + (‎ ಌಾ ‎) 0C8C 0CBE KANNADA LETTER VOCALIC L, KANNADA VOWEL SIGN AA +← (‎ ೡ ‎) 0CE1 KANNADA LETTER VOCALIC LL + +# ഇൗ ഈ + (‎ ഇൗ ‎) 0D07 0D57 MALAYALAM LETTER I, MALAYALAM AU LENGTH MARK +← (‎ ഈ ‎) 0D08 MALAYALAM LETTER II + +# നു ഌ ങ ൹ + (‎ ഌ ‎) 0D0C MALAYALAM LETTER VOCALIC L +← (‎ നു ‎) 0D28 0D41 MALAYALAM LETTER NA, MALAYALAM VOWEL SIGN U +← (‎ ങ ‎) 0D19 MALAYALAM LETTER NGA +← (‎ ൹ ‎) 0D79 MALAYALAM DATE MARK # →നു→ + +# എെ െഎ ഐ + (‎ എെ ‎) 0D0E 0D46 MALAYALAM LETTER E, MALAYALAM VOWEL SIGN E +← (‎ െഎ ‎) 0D46 0D0E MALAYALAM VOWEL SIGN E, MALAYALAM LETTER E # →ഐ→ +← (‎ ഐ ‎) 0D10 MALAYALAM LETTER AI + +# ഒാ ഓ + (‎ ഒാ ‎) 0D12 0D3E MALAYALAM LETTER O, MALAYALAM VOWEL SIGN AA +← (‎ ഓ ‎) 0D13 MALAYALAM LETTER OO + +# ഒൗ ഔ + (‎ ഒൗ ‎) 0D12 0D57 MALAYALAM LETTER O, MALAYALAM AU LENGTH MARK +← (‎ ഔ ‎) 0D14 MALAYALAM LETTER AU + +# ഞ ൡ + (‎ ഞ ‎) 0D1E MALAYALAM LETTER NYA +← (‎ ൡ ‎) 0D61 MALAYALAM LETTER VOCALIC LL + +# ദ്ര ൫ + (‎ ദ്ര ‎) 0D26 0D4D 0D30 MALAYALAM LETTER DA, MALAYALAM SIGN VIRAMA, MALAYALAM LETTER RA +← (‎ ൫ ‎) 0D6B MALAYALAM DIGIT FIVE + +# ന് ൯ ൻ + (‎ ന് ‎) 0D28 0D4D MALAYALAM LETTER NA, MALAYALAM SIGN VIRAMA +← (‎ ൯ ‎) 0D6F MALAYALAM DIGIT NINE +← (‎ ൻ ‎) 0D7B MALAYALAM LETTER CHILLU N # →൯→ + +# ന്ന ൬ + (‎ ന്ന ‎) 0D28 0D4D 0D28 MALAYALAM LETTER NA, MALAYALAM SIGN VIRAMA, MALAYALAM LETTER NA +← (‎ ൬ ‎) 0D6C MALAYALAM DIGIT SIX + +# ന്മ ൚ + (‎ ന്മ ‎) 0D28 0D4D 0D2E MALAYALAM LETTER NA, MALAYALAM SIGN VIRAMA, MALAYALAM LETTER MA +← (‎ ൚ ‎) 0D5A MALAYALAM FRACTION THREE EIGHTIETHS + +# ര റ + (‎ ര ‎) 0D30 MALAYALAM LETTER RA +← (‎ റ ‎) 0D31 MALAYALAM LETTER RRA + +# ര് ൪ ർ + (‎ ര് ‎) 0D30 0D4D MALAYALAM LETTER RA, MALAYALAM SIGN VIRAMA +← (‎ ൪ ‎) 0D6A MALAYALAM DIGIT FOUR +← (‎ ർ ‎) 0D7C MALAYALAM LETTER CHILLU RR # →൪→ + +# വ്ര വ് ൮ + (‎ വ് ‎) 0D35 0D4D MALAYALAM LETTER VA, MALAYALAM SIGN VIRAMA +← (‎ വ്ര ‎) 0D35 0D4D 0D30 MALAYALAM LETTER VA, MALAYALAM SIGN VIRAMA, MALAYALAM LETTER RA # →൮→ +← (‎ ൮ ‎) 0D6E MALAYALAM DIGIT EIGHT + +# ഹ്മ ൶ + (‎ ഹ്മ ‎) 0D39 0D4D 0D2E MALAYALAM LETTER HA, MALAYALAM SIGN VIRAMA, MALAYALAM LETTER MA +← (‎ ൶ ‎) 0D76 MALAYALAM FRACTION ONE SIXTEENTH + +# ു ൂ ൃ + (‎ ു ‎) 0D41 MALAYALAM VOWEL SIGN U +← (‎ ൂ ‎) 0D42 MALAYALAM VOWEL SIGN UU +← (‎ ൃ ‎) 0D43 MALAYALAM VOWEL SIGN VOCALIC R # →ൂ→ + +# െെ ൈ + (‎ െെ ‎) 0D46 0D46 MALAYALAM VOWEL SIGN E, MALAYALAM VOWEL SIGN E +← (‎ ൈ ‎) 0D48 MALAYALAM VOWEL SIGN AI + +# ජ ෪ + (‎ ජ ‎) 0DA2 SINHALA LETTER ALPAPRAANA JAYANNA +← (‎ ෪ ‎) 0DEA SINHALA LITH DIGIT FOUR + +# ද ෫ + (‎ ද ‎) 0DAF SINHALA LETTER ALPAPRAANA DAYANNA +← (‎ ෫ ‎) 0DEB SINHALA LITH DIGIT FIVE + +# ෨ා ෩ + (‎ ෨ා ‎) 0DE8 0DCF SINHALA LITH DIGIT TWO, SINHALA VOWEL SIGN AELA-PILLA +← (‎ ෩ ‎) 0DE9 SINHALA LITH DIGIT THREE + +# ෨ී ෯ + (‎ ෨ී ‎) 0DE8 0DD3 SINHALA LITH DIGIT TWO, SINHALA VOWEL SIGN DIGA IS-PILLA +← (‎ ෯ ‎) 0DEF SINHALA LITH DIGIT NINE + +# ข ฃ + (‎ ข ‎) 0E02 THAI CHARACTER KHO KHAI +← (‎ ฃ ‎) 0E03 THAI CHARACTER KHO KHUAT + +# ค ด ต + (‎ ค ‎) 0E04 THAI CHARACTER KHO KHWAI +← (‎ ด ‎) 0E14 THAI CHARACTER DO DEK +← (‎ ต ‎) 0E15 THAI CHARACTER TO TAO # →ด→ + +# ฆ ม + (‎ ฆ ‎) 0E06 THAI CHARACTER KHO RAKHANG +← (‎ ม ‎) 0E21 THAI CHARACTER MO MA + +# จ ຈ + (‎ จ ‎) 0E08 THAI CHARACTER CHO CHAN +← (‎ ຈ ‎) 0E88 LAO LETTER CO + +# ช ซ + (‎ ช ‎) 0E0A THAI CHARACTER CHO CHANG +← (‎ ซ ‎) 0E0B THAI CHARACTER SO SO + +# ฎ ฏ + (‎ ฎ ‎) 0E0E THAI CHARACTER DO CHADA +← (‎ ฏ ‎) 0E0F THAI CHARACTER TO PATAK + +# ฑ ท + (‎ ฑ ‎) 0E11 THAI CHARACTER THO NANGMONTHO +← (‎ ท ‎) 0E17 THAI CHARACTER THO THAHAN + +# บ ບ + (‎ บ ‎) 0E1A THAI CHARACTER BO BAIMAI +← (‎ ບ ‎) 0E9A LAO LETTER BO + +# ป ປ + (‎ ป ‎) 0E1B THAI CHARACTER PO PLA +← (‎ ປ ‎) 0E9B LAO LETTER PO + +# ฝ ຝ + (‎ ฝ ‎) 0E1D THAI CHARACTER FO FA +← (‎ ຝ ‎) 0E9D LAO LETTER FO TAM + +# พ ພ + (‎ พ ‎) 0E1E THAI CHARACTER PHO PHAN +← (‎ ພ ‎) 0E9E LAO LETTER PHO TAM + +# ฟ ຟ + (‎ ฟ ‎) 0E1F THAI CHARACTER FO FAN +← (‎ ຟ ‎) 0E9F LAO LETTER FO SUNG + +# ภ ฦ + (‎ ภ ‎) 0E20 THAI CHARACTER PHO SAMPHAO +← (‎ ฦ ‎) 0E26 THAI CHARACTER LU + +# ย ຍ + (‎ ย ‎) 0E22 THAI CHARACTER YO YAK +← (‎ ຍ ‎) 0E8D LAO LETTER NYO + +# ฯ ។ + (‎ ฯ ‎) 0E2F THAI CHARACTER PAIYANNOI +← (‎ ។ ‎) 17D4 KHMER SIGN KHAN + +# า ๅ + (‎ า ‎) 0E32 THAI CHARACTER SARA AA +← (‎ ๅ ‎) 0E45 THAI CHARACTER LAKKHANGYAO + +# ิ ិ + (‎ ิ ‎) 0E34 THAI CHARACTER SARA I +← (‎ ិ ‎) 17B7 KHMER VOWEL SIGN I + +# ี ី + (‎ ี ‎) 0E35 THAI CHARACTER SARA II +← (‎ ី ‎) 17B8 KHMER VOWEL SIGN II + +# ึ ឹ + (‎ ึ ‎) 0E36 THAI CHARACTER SARA UE +← (‎ ឹ ‎) 17B9 KHMER VOWEL SIGN Y + +# ื ឺ + (‎ ื ‎) 0E37 THAI CHARACTER SARA UEE +← (‎ ឺ ‎) 17BA KHMER VOWEL SIGN YY + +# ุ ຸ + (‎ ุ ‎) 0E38 THAI CHARACTER SARA U +← (‎ ຸ ‎) 0EB8 LAO VOWEL SIGN U + +# ู ູ + (‎ ู ‎) 0E39 THAI CHARACTER SARA UU +← (‎ ູ ‎) 0EB9 LAO VOWEL SIGN UU + +# เเ แ + (‎ เเ ‎) 0E40 0E40 THAI CHARACTER SARA E, THAI CHARACTER SARA E +← (‎ แ ‎) 0E41 THAI CHARACTER SARA AE + +# ่ ່ ់ + (‎ ่ ‎) 0E48 THAI CHARACTER MAI EK +← (‎ ່ ‎) 0EC8 LAO TONE MAI EK +← (‎ ់ ‎) 17CB KHMER SIGN BANTOC + +# ้ ້ + (‎ ้ ‎) 0E49 THAI CHARACTER MAI THO +← (‎ ້ ‎) 0EC9 LAO TONE MAI THO + +# ๊ ໊ + (‎ ๊ ‎) 0E4A THAI CHARACTER MAI TRI +← (‎ ໊ ‎) 0ECA LAO TONE MAI TI + +# ๋ ໋ + (‎ ๋ ‎) 0E4B THAI CHARACTER MAI CHATTAWA +← (‎ ໋ ‎) 0ECB LAO TONE MAI CATAWA + +# ๏ ៙ + (‎ ๏ ‎) 0E4F THAI CHARACTER FONGMAN +← (‎ ៙ ‎) 17D9 KHMER SIGN PHNAEK MUAN + +# ๚ ៕ + (‎ ๚ ‎) 0E5A THAI CHARACTER ANGKHANKHU +← (‎ ៕ ‎) 17D5 KHMER SIGN BARIYOOSAN + +# ๛ ៚ + (‎ ๛ ‎) 0E5B THAI CHARACTER KHOMUT +← (‎ ៚ ‎) 17DA KHMER SIGN KOOMUUT + +# ຫນ ໜ + (‎ ຫນ ‎) 0EAB 0E99 LAO LETTER HO SUNG, LAO LETTER NO +← (‎ ໜ ‎) 0EDC LAO HO NO + +# ຫມ ໝ + (‎ ຫມ ‎) 0EAB 0EA1 LAO LETTER HO SUNG, LAO LETTER MO +← (‎ ໝ ‎) 0EDD LAO HO MO + +# ཨོཾ ༀ + (‎ ༀ ‎) 0F00 TIBETAN SYLLABLE OM +← (‎ ཨོཾ ‎) 0F68 0F7C 0F7E TIBETAN LETTER A, TIBETAN VOWEL SIGN O, TIBETAN SIGN RJES SU NGA RO + +# འུྂཿ ༂ + (‎ ༂ ‎) 0F02 TIBETAN MARK GTER YIG MGO -UM RNAM BCAD MA +← (‎ འུྂཿ ‎) 0F60 0F74 0F82 0F7F TIBETAN LETTER -A, TIBETAN VOWEL SIGN U, TIBETAN SIGN NYI ZLA NAA DA, TIBETAN SIGN RNAM BCAD + +# འུྂ༔ ༃ + (‎ ༃ ‎) 0F03 TIBETAN MARK GTER YIG MGO -UM GTER TSHEG MA +← (‎ འུྂ༔ ‎) 0F60 0F74 0F82 0F14 TIBETAN LETTER -A, TIBETAN VOWEL SIGN U, TIBETAN SIGN NYI ZLA NAA DA, TIBETAN MARK GTER TSHEG + +# ་ ༌ + (‎ ་ ‎) 0F0B TIBETAN MARK INTERSYLLABIC TSHEG +← (‎ ༌ ‎) 0F0C TIBETAN MARK DELIMITER TSHEG BSTAR + +# །། ༎ + (‎ །། ‎) 0F0D 0F0D TIBETAN MARK SHAD, TIBETAN MARK SHAD +← (‎ ༎ ‎) 0F0E TIBETAN MARK NYIS SHAD + +# ༚༚ ༛ + (‎ ༚༚ ‎) 0F1A 0F1A TIBETAN SIGN RDEL DKAR GCIG, TIBETAN SIGN RDEL DKAR GCIG +← (‎ ༛ ‎) 0F1B TIBETAN SIGN RDEL DKAR GNYIS + +# ༚༝ ༟ + (‎ ༚༝ ‎) 0F1A 0F1D TIBETAN SIGN RDEL DKAR GCIG, TIBETAN SIGN RDEL NAG GCIG +← (‎ ༟ ‎) 0F1F TIBETAN SIGN RDEL DKAR RDEL NAG + +# ༝༚ ࿎ + (‎ ༝༚ ‎) 0F1D 0F1A TIBETAN SIGN RDEL NAG GCIG, TIBETAN SIGN RDEL DKAR GCIG +← (‎ ࿎ ‎) 0FCE TIBETAN SIGN RDEL NAG RDEL DKAR + +# ༝༝ ༞ + (‎ ༝༝ ‎) 0F1D 0F1D TIBETAN SIGN RDEL NAG GCIG, TIBETAN SIGN RDEL NAG GCIG +← (‎ ༞ ‎) 0F1E TIBETAN SIGN RDEL NAG GNYIS + +# ར ཪ + (‎ ར ‎) 0F62 TIBETAN LETTER RA +← (‎ ཪ ‎) 0F6A TIBETAN LETTER FIXED-FORM RA + +# ྲཱྀ ཷ + (‎ ཷ ‎) 0F77 TIBETAN VOWEL SIGN VOCALIC RR +← (‎ ྲཱྀ ‎) 0FB2 0F71 0F80 TIBETAN SUBJOINED LETTER RA, TIBETAN VOWEL SIGN AA, TIBETAN VOWEL SIGN REVERSED I + +# ླཱྀ ཹ + (‎ ཹ ‎) 0F79 TIBETAN VOWEL SIGN VOCALIC LL +← (‎ ླཱྀ ‎) 0FB3 0F71 0F80 TIBETAN SUBJOINED LETTER LA, TIBETAN VOWEL SIGN AA, TIBETAN VOWEL SIGN REVERSED I + +# 卐 ࿕ + (‎ ࿕ ‎) 0FD5 RIGHT-FACING SVASTI SIGN +← (‎ 卐 ‎) 5350 CJK UNIFIED IDEOGRAPH-5350 + +# 卍 ࿖ + (‎ ࿖ ‎) 0FD6 LEFT-FACING SVASTI SIGN +← (‎ 卍 ‎) 534D CJK UNIFIED IDEOGRAPH-534D + +# ဂာ က + (‎ က ‎) 1000 MYANMAR LETTER KA +← (‎ ဂာ ‎) 1002 102C MYANMAR LETTER GA, MYANMAR VOWEL SIGN AA + +# ဂှ ႁ + (‎ ဂှ ‎) 1002 103E MYANMAR LETTER GA, MYANMAR CONSONANT SIGN MEDIAL HA +← (‎ ႁ ‎) 1081 MYANMAR LETTER SHAN HA + +# ဃှ ၰ + (‎ ဃှ ‎) 1003 103E MYANMAR LETTER GHA, MYANMAR CONSONANT SIGN MEDIAL HA +← (‎ ၰ ‎) 1070 MYANMAR LETTER EASTERN PWO KAREN GHWA + +# ပာ ဟ + (‎ ပာ ‎) 1015 102C MYANMAR LETTER PA, MYANMAR VOWEL SIGN AA +← (‎ ဟ ‎) 101F MYANMAR LETTER HA + +# ပာှ ဟှ ၯ + (‎ ပာှ ‎) 1015 102C 103E MYANMAR LETTER PA, MYANMAR VOWEL SIGN AA, MYANMAR CONSONANT SIGN MEDIAL HA +← (‎ ဟှ ‎) 101F 103E MYANMAR LETTER HA, MYANMAR CONSONANT SIGN MEDIAL HA +← (‎ ၯ ‎) 106F MYANMAR LETTER EASTERN PWO KAREN YWA # →ဟှ→ + +# ပှ ၦ + (‎ ပှ ‎) 1015 103E MYANMAR LETTER PA, MYANMAR CONSONANT SIGN MEDIAL HA +← (‎ ၦ ‎) 1066 MYANMAR LETTER WESTERN PWO KAREN PWA + +# သြ ဩ + (‎ သြ ‎) 101E 103C MYANMAR LETTER SA, MYANMAR CONSONANT SIGN MEDIAL RA +← (‎ ဩ ‎) 1029 MYANMAR LETTER O + +# သြော် ဩော် ဪ + (‎ သြော် ‎) 101E 103C 1031 102C 103A MYANMAR LETTER SA, MYANMAR CONSONANT SIGN MEDIAL RA, MYANMAR VOWEL SIGN E, MYANMAR VOWEL SIGN AA, MYANMAR SIGN ASAT +← (‎ ဩော် ‎) 1029 1031 102C 103A MYANMAR LETTER O, MYANMAR VOWEL SIGN E, MYANMAR VOWEL SIGN AA, MYANMAR SIGN ASAT +← (‎ ဪ ‎) 102A MYANMAR LETTER AU # →ဩော်→ + +# ၁ ၥ + (‎ ၁ ‎) 1041 MYANMAR DIGIT ONE +← (‎ ၥ ‎) 1065 MYANMAR LETTER WESTERN PWO KAREN THA + +# ၊၊ ။ + (‎ ၊၊ ‎) 104A 104A MYANMAR SIGN LITTLE SECTION, MYANMAR SIGN LITTLE SECTION +← (‎ ။ ‎) 104B MYANMAR SIGN SECTION + +# ၽှ ၾ + (‎ ၽှ ‎) 107D 103E MYANMAR LETTER SHAN PHA, MYANMAR CONSONANT SIGN MEDIAL HA +← (‎ ၾ ‎) 107E MYANMAR LETTER SHAN FA + +# ႃ̊ ႃံ ႞ + (‎ ႃ̊ ‎) 1083 030A MYANMAR VOWEL SIGN SHAN AA, COMBINING RING ABOVE +← (‎ ႃံ ‎) 1083 1036 MYANMAR VOWEL SIGN SHAN AA, MYANMAR SIGN ANUSVARA +← (‎ ႞ ‎) 109E MYANMAR SYMBOL SHAN ONE # →ႃံ→ + +# Ꞇ Ⴀ + (‎ Ⴀ ‎) 10A0 GEORGIAN CAPITAL LETTER AN +← (‎ Ꞇ ‎) A786 LATIN CAPITAL LETTER INSULAR T + +# ᄀ ᆨ ㄱ + (‎ ᄀ ‎) 1100 HANGUL CHOSEONG KIYEOK +← (‎ ᆨ ‎) 11A8 HANGUL JONGSEONG KIYEOK +← (‎ ㄱ ‎) 3131 HANGUL LETTER KIYEOK + +# ᄀᄀ ᆨᆨ ᄁ ᆩ ㄲ + (‎ ᄀᄀ ‎) 1100 1100 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG KIYEOK +← (‎ ᆨᆨ ‎) 11A8 11A8 HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG KIYEOK # →ᆩ→→ᄁ→ +← (‎ ᄁ ‎) 1101 HANGUL CHOSEONG SSANGKIYEOK +← (‎ ᆩ ‎) 11A9 HANGUL JONGSEONG SSANGKIYEOK # →ᄁ→ +← (‎ ㄲ ‎) 3132 HANGUL LETTER SSANGKIYEOK # →ᄁ→ + +# ᄀᄂ ᆨᆫ ᇺ + (‎ ᄀᄂ ‎) 1100 1102 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG NIEUN +← (‎ ᆨᆫ ‎) 11A8 11AB HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG NIEUN +← (‎ ᇺ ‎) 11FA HANGUL JONGSEONG KIYEOK-NIEUN # →ᆨᆫ→ + +# ᄀᄃ ᅚ + (‎ ᄀᄃ ‎) 1100 1103 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG TIKEUT +← (‎ ᅚ ‎) 115A HANGUL CHOSEONG KIYEOK-TIKEUT + +# ᄀᄅ ᆨᆯ ᇃ + (‎ ᄀᄅ ‎) 1100 1105 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG RIEUL +← (‎ ᆨᆯ ‎) 11A8 11AF HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG RIEUL +← (‎ ᇃ ‎) 11C3 HANGUL JONGSEONG KIYEOK-RIEUL # →ᆨᆯ→ + +# ᄀᄇ ᆨᆸ ᇻ + (‎ ᄀᄇ ‎) 1100 1107 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG PIEUP +← (‎ ᆨᆸ ‎) 11A8 11B8 HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG PIEUP +← (‎ ᇻ ‎) 11FB HANGUL JONGSEONG KIYEOK-PIEUP # →ᆨᆸ→ + +# ᄀᄉ ᆨᆺ ᆪ ㄳ + (‎ ᄀᄉ ‎) 1100 1109 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG SIOS +← (‎ ᆨᆺ ‎) 11A8 11BA HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG SIOS +← (‎ ᆪ ‎) 11AA HANGUL JONGSEONG KIYEOK-SIOS # →ᆨᆺ→ +← (‎ ㄳ ‎) 3133 HANGUL LETTER KIYEOK-SIOS # →ᆪ→→ᆨᆺ→ + +# ᄀᄉᄀ ᆨᆺᆨ ᇄ + (‎ ᄀᄉᄀ ‎) 1100 1109 1100 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG KIYEOK +← (‎ ᆨᆺᆨ ‎) 11A8 11BA 11A8 HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG KIYEOK +← (‎ ᇄ ‎) 11C4 HANGUL JONGSEONG KIYEOK-SIOS-KIYEOK # →ᆨᆺᆨ→ + +# ᄀᄎ ᆨᆾ ᇼ + (‎ ᄀᄎ ‎) 1100 110E HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG CHIEUCH +← (‎ ᆨᆾ ‎) 11A8 11BE HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG CHIEUCH +← (‎ ᇼ ‎) 11FC HANGUL JONGSEONG KIYEOK-CHIEUCH # →ᆨᆾ→ + +# ᄀᄏ ᆨᆿ ᇽ + (‎ ᄀᄏ ‎) 1100 110F HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG KHIEUKH +← (‎ ᆨᆿ ‎) 11A8 11BF HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG KHIEUKH +← (‎ ᇽ ‎) 11FD HANGUL JONGSEONG KIYEOK-KHIEUKH # →ᆨᆿ→ + +# ᄀᄒ ᆨᇂ ᇾ + (‎ ᄀᄒ ‎) 1100 1112 HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG HIEUH +← (‎ ᆨᇂ ‎) 11A8 11C2 HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG HIEUH +← (‎ ᇾ ‎) 11FE HANGUL JONGSEONG KIYEOK-HIEUH # →ᆨᇂ→ + +# ᄂ ᆫ ㄴ + (‎ ᄂ ‎) 1102 HANGUL CHOSEONG NIEUN +← (‎ ᆫ ‎) 11AB HANGUL JONGSEONG NIEUN +← (‎ ㄴ ‎) 3134 HANGUL LETTER NIEUN + +# ᄂᄀ ᆫᆨ ᄓ ᇅ + (‎ ᄂᄀ ‎) 1102 1100 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG KIYEOK +← (‎ ᆫᆨ ‎) 11AB 11A8 HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG KIYEOK # →ᇅ→→ᄓ→ +← (‎ ᄓ ‎) 1113 HANGUL CHOSEONG NIEUN-KIYEOK +← (‎ ᇅ ‎) 11C5 HANGUL JONGSEONG NIEUN-KIYEOK # →ᄓ→ + +# ᄂᄂ ᆫᆫ ᄔ ᇿ ㅥ + (‎ ᄂᄂ ‎) 1102 1102 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG NIEUN +← (‎ ᆫᆫ ‎) 11AB 11AB HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG NIEUN +← (‎ ᄔ ‎) 1114 HANGUL CHOSEONG SSANGNIEUN +← (‎ ᇿ ‎) 11FF HANGUL JONGSEONG SSANGNIEUN # →ᆫᆫ→ +← (‎ ㅥ ‎) 3165 HANGUL LETTER SSANGNIEUN # →ᄔ→ + +# ᄂᄃ ᆫᆮ ᄕ ᇆ ㅦ + (‎ ᄂᄃ ‎) 1102 1103 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG TIKEUT +← (‎ ᆫᆮ ‎) 11AB 11AE HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG TIKEUT # →ᇆ→→ᄕ→ +← (‎ ᄕ ‎) 1115 HANGUL CHOSEONG NIEUN-TIKEUT +← (‎ ᇆ ‎) 11C6 HANGUL JONGSEONG NIEUN-TIKEUT # →ᄕ→ +← (‎ ㅦ ‎) 3166 HANGUL LETTER NIEUN-TIKEUT # →ᄕ→ + +# ᄂᄅ ᆫᆯ ퟋ + (‎ ᄂᄅ ‎) 1102 1105 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG RIEUL +← (‎ ᆫᆯ ‎) 11AB 11AF HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG RIEUL +← (‎ ퟋ ‎) D7CB HANGUL JONGSEONG NIEUN-RIEUL # →ᆫᆯ→ + +# ᄂᄇ ᄖ + (‎ ᄂᄇ ‎) 1102 1107 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG PIEUP +← (‎ ᄖ ‎) 1116 HANGUL CHOSEONG NIEUN-PIEUP + +# ᄂᄉ ᆫᆺ ᅛ ᇇ ㅧ + (‎ ᄂᄉ ‎) 1102 1109 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG SIOS +← (‎ ᆫᆺ ‎) 11AB 11BA HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG SIOS +← (‎ ᅛ ‎) 115B HANGUL CHOSEONG NIEUN-SIOS +← (‎ ᇇ ‎) 11C7 HANGUL JONGSEONG NIEUN-SIOS # →ᆫᆺ→ +← (‎ ㅧ ‎) 3167 HANGUL LETTER NIEUN-SIOS # →ᇇ→→ᆫᆺ→ + +# ᄂᄌ ᆫᆽ ᅜ ᆬ ㄵ + (‎ ᄂᄌ ‎) 1102 110C HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG CIEUC +← (‎ ᆫᆽ ‎) 11AB 11BD HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG CIEUC +← (‎ ᅜ ‎) 115C HANGUL CHOSEONG NIEUN-CIEUC +← (‎ ᆬ ‎) 11AC HANGUL JONGSEONG NIEUN-CIEUC # →ᆫᆽ→ +← (‎ ㄵ ‎) 3135 HANGUL LETTER NIEUN-CIEUC # →ᆬ→→ᆫᆽ→ + +# ᄂᄎ ᆫᆾ ퟌ + (‎ ᄂᄎ ‎) 1102 110E HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG CHIEUCH +← (‎ ᆫᆾ ‎) 11AB 11BE HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG CHIEUCH +← (‎ ퟌ ‎) D7CC HANGUL JONGSEONG NIEUN-CHIEUCH # →ᆫᆾ→ + +# ᄂᄐ ᆫᇀ ᇉ + (‎ ᄂᄐ ‎) 1102 1110 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG THIEUTH +← (‎ ᆫᇀ ‎) 11AB 11C0 HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG THIEUTH +← (‎ ᇉ ‎) 11C9 HANGUL JONGSEONG NIEUN-THIEUTH # →ᆫᇀ→ + +# ᄂᄒ ᆫᇂ ᅝ ᆭ ㄶ + (‎ ᄂᄒ ‎) 1102 1112 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG HIEUH +← (‎ ᆫᇂ ‎) 11AB 11C2 HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG HIEUH +← (‎ ᅝ ‎) 115D HANGUL CHOSEONG NIEUN-HIEUH +← (‎ ᆭ ‎) 11AD HANGUL JONGSEONG NIEUN-HIEUH # →ᆫᇂ→ +← (‎ ㄶ ‎) 3136 HANGUL LETTER NIEUN-HIEUH # →ᆭ→→ᆫᇂ→ + +# ᄂᅀ ᆫᇫ ᇈ ㅨ + (‎ ᄂᅀ ‎) 1102 1140 HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG PANSIOS +← (‎ ᆫᇫ ‎) 11AB 11EB HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG PANSIOS +← (‎ ᇈ ‎) 11C8 HANGUL JONGSEONG NIEUN-PANSIOS # →ᆫᇫ→ +← (‎ ㅨ ‎) 3168 HANGUL LETTER NIEUN-PANSIOS # →ᇈ→→ᆫᇫ→ + +# ᄃ ᆮ ㄷ + (‎ ᄃ ‎) 1103 HANGUL CHOSEONG TIKEUT +← (‎ ᆮ ‎) 11AE HANGUL JONGSEONG TIKEUT +← (‎ ㄷ ‎) 3137 HANGUL LETTER TIKEUT + +# ᄃᄀ ᆮᆨ ᄗ ᇊ + (‎ ᄃᄀ ‎) 1103 1100 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG KIYEOK +← (‎ ᆮᆨ ‎) 11AE 11A8 HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG KIYEOK # →ᇊ→→ᄗ→ +← (‎ ᄗ ‎) 1117 HANGUL CHOSEONG TIKEUT-KIYEOK +← (‎ ᇊ ‎) 11CA HANGUL JONGSEONG TIKEUT-KIYEOK # →ᄗ→ + +# ᄃᄃ ᆮᆮ ᄄ ퟍ ㄸ + (‎ ᄃᄃ ‎) 1103 1103 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG TIKEUT +← (‎ ᆮᆮ ‎) 11AE 11AE HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG TIKEUT +← (‎ ᄄ ‎) 1104 HANGUL CHOSEONG SSANGTIKEUT +← (‎ ퟍ ‎) D7CD HANGUL JONGSEONG SSANGTIKEUT # →ᆮᆮ→ +← (‎ ㄸ ‎) 3138 HANGUL LETTER SSANGTIKEUT # →ᄄ→ + +# ᄃᄃᄇ ᆮᆮᆸ ퟎ + (‎ ᄃᄃᄇ ‎) 1103 1103 1107 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG PIEUP +← (‎ ᆮᆮᆸ ‎) 11AE 11AE 11B8 HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG PIEUP +← (‎ ퟎ ‎) D7CE HANGUL JONGSEONG SSANGTIKEUT-PIEUP # →ᆮᆮᆸ→ + +# ᄃᄅ ᆮᆯ ᅞ ᇋ + (‎ ᄃᄅ ‎) 1103 1105 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG RIEUL +← (‎ ᆮᆯ ‎) 11AE 11AF HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG RIEUL +← (‎ ᅞ ‎) 115E HANGUL CHOSEONG TIKEUT-RIEUL +← (‎ ᇋ ‎) 11CB HANGUL JONGSEONG TIKEUT-RIEUL # →ᆮᆯ→ + +# ᄃᄆ ꥠ + (‎ ᄃᄆ ‎) 1103 1106 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG MIEUM +← (‎ ꥠ ‎) A960 HANGUL CHOSEONG TIKEUT-MIEUM + +# ᄃᄇ ᆮᆸ ꥡ ퟏ + (‎ ᄃᄇ ‎) 1103 1107 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG PIEUP +← (‎ ᆮᆸ ‎) 11AE 11B8 HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG PIEUP +← (‎ ꥡ ‎) A961 HANGUL CHOSEONG TIKEUT-PIEUP +← (‎ ퟏ ‎) D7CF HANGUL JONGSEONG TIKEUT-PIEUP # →ᆮᆸ→ + +# ᄃᄉ ᆮᆺ ꥢ ퟐ + (‎ ᄃᄉ ‎) 1103 1109 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG SIOS +← (‎ ᆮᆺ ‎) 11AE 11BA HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG SIOS +← (‎ ꥢ ‎) A962 HANGUL CHOSEONG TIKEUT-SIOS +← (‎ ퟐ ‎) D7D0 HANGUL JONGSEONG TIKEUT-SIOS # →ᆮᆺ→ + +# ᄃᄉᄀ ᆮᆺᆨ ퟑ + (‎ ᄃᄉᄀ ‎) 1103 1109 1100 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG KIYEOK +← (‎ ᆮᆺᆨ ‎) 11AE 11BA 11A8 HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG KIYEOK +← (‎ ퟑ ‎) D7D1 HANGUL JONGSEONG TIKEUT-SIOS-KIYEOK # →ᆮᆺᆨ→ + +# ᄃᄌ ᆮᆽ ꥣ ퟒ + (‎ ᄃᄌ ‎) 1103 110C HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG CIEUC +← (‎ ᆮᆽ ‎) 11AE 11BD HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG CIEUC +← (‎ ꥣ ‎) A963 HANGUL CHOSEONG TIKEUT-CIEUC +← (‎ ퟒ ‎) D7D2 HANGUL JONGSEONG TIKEUT-CIEUC # →ᆮᆽ→ + +# ᄃᄎ ᆮᆾ ퟓ + (‎ ᄃᄎ ‎) 1103 110E HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG CHIEUCH +← (‎ ᆮᆾ ‎) 11AE 11BE HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG CHIEUCH +← (‎ ퟓ ‎) D7D3 HANGUL JONGSEONG TIKEUT-CHIEUCH # →ᆮᆾ→ + +# ᄃᄐ ᆮᇀ ퟔ + (‎ ᄃᄐ ‎) 1103 1110 HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG THIEUTH +← (‎ ᆮᇀ ‎) 11AE 11C0 HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG THIEUTH +← (‎ ퟔ ‎) D7D4 HANGUL JONGSEONG TIKEUT-THIEUTH # →ᆮᇀ→ + +# ᄅ ᆯ ㄹ + (‎ ᄅ ‎) 1105 HANGUL CHOSEONG RIEUL +← (‎ ᆯ ‎) 11AF HANGUL JONGSEONG RIEUL +← (‎ ㄹ ‎) 3139 HANGUL LETTER RIEUL + +# ᄅᄀ ᆯᆨ ᆰ ꥤ ㄺ + (‎ ᄅᄀ ‎) 1105 1100 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG KIYEOK +← (‎ ᆯᆨ ‎) 11AF 11A8 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG KIYEOK +← (‎ ᆰ ‎) 11B0 HANGUL JONGSEONG RIEUL-KIYEOK # →ᆯᆨ→ +← (‎ ꥤ ‎) A964 HANGUL CHOSEONG RIEUL-KIYEOK +← (‎ ㄺ ‎) 313A HANGUL LETTER RIEUL-KIYEOK # →ᆰ→→ᆯᆨ→ + +# ᄅᄀᄀ ᆯᆨᆨ ꥥ ퟕ + (‎ ᄅᄀᄀ ‎) 1105 1100 1100 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG KIYEOK +← (‎ ᆯᆨᆨ ‎) 11AF 11A8 11A8 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG KIYEOK +← (‎ ꥥ ‎) A965 HANGUL CHOSEONG RIEUL-SSANGKIYEOK +← (‎ ퟕ ‎) D7D5 HANGUL JONGSEONG RIEUL-SSANGKIYEOK # →ᆯᆨᆨ→ + +# ᄅᄀᄉ ᆯᆨᆺ ᇌ ㅩ + (‎ ᄅᄀᄉ ‎) 1105 1100 1109 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG SIOS +← (‎ ᆯᆨᆺ ‎) 11AF 11A8 11BA HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG SIOS +← (‎ ᇌ ‎) 11CC HANGUL JONGSEONG RIEUL-KIYEOK-SIOS # →ᆯᆨᆺ→ +← (‎ ㅩ ‎) 3169 HANGUL LETTER RIEUL-KIYEOK-SIOS # →ᇌ→→ᆯᆨᆺ→ + +# ᄅᄀᄒ ᆯᆨᇂ ퟖ + (‎ ᄅᄀᄒ ‎) 1105 1100 1112 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG HIEUH +← (‎ ᆯᆨᇂ ‎) 11AF 11A8 11C2 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG HIEUH +← (‎ ퟖ ‎) D7D6 HANGUL JONGSEONG RIEUL-KIYEOK-HIEUH # →ᆯᆨᇂ→ + +# ᄅᄂ ᆯᆫ ᄘ ᇍ + (‎ ᄅᄂ ‎) 1105 1102 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG NIEUN +← (‎ ᆯᆫ ‎) 11AF 11AB HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG NIEUN # →ᇍ→→ᄘ→ +← (‎ ᄘ ‎) 1118 HANGUL CHOSEONG RIEUL-NIEUN +← (‎ ᇍ ‎) 11CD HANGUL JONGSEONG RIEUL-NIEUN # →ᄘ→ + +# ᄅᄃ ᆯᆮ ᇎ ꥦ ㅪ + (‎ ᄅᄃ ‎) 1105 1103 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG TIKEUT +← (‎ ᆯᆮ ‎) 11AF 11AE HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG TIKEUT +← (‎ ᇎ ‎) 11CE HANGUL JONGSEONG RIEUL-TIKEUT # →ᆯᆮ→ +← (‎ ꥦ ‎) A966 HANGUL CHOSEONG RIEUL-TIKEUT +← (‎ ㅪ ‎) 316A HANGUL LETTER RIEUL-TIKEUT # →ᇎ→→ᆯᆮ→ + +# ᄅᄃᄃ ꥧ + (‎ ᄅᄃᄃ ‎) 1105 1103 1103 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG TIKEUT +← (‎ ꥧ ‎) A967 HANGUL CHOSEONG RIEUL-SSANGTIKEUT + +# ᄅᄃᄒ ᆯᆮᇂ ᇏ + (‎ ᄅᄃᄒ ‎) 1105 1103 1112 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG TIKEUT, HANGUL CHOSEONG HIEUH +← (‎ ᆯᆮᇂ ‎) 11AF 11AE 11C2 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG TIKEUT, HANGUL JONGSEONG HIEUH +← (‎ ᇏ ‎) 11CF HANGUL JONGSEONG RIEUL-TIKEUT-HIEUH # →ᆯᆮᇂ→ + +# ᄅᄅ ᆯᆯ ᄙ ᇐ + (‎ ᄅᄅ ‎) 1105 1105 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG RIEUL +← (‎ ᆯᆯ ‎) 11AF 11AF HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG RIEUL # →ᇐ→→ᄙ→ +← (‎ ᄙ ‎) 1119 HANGUL CHOSEONG SSANGRIEUL +← (‎ ᇐ ‎) 11D0 HANGUL JONGSEONG SSANGRIEUL # →ᄙ→ + +# ᄅᄅᄏ ᆯᆯᆿ ퟗ + (‎ ᄅᄅᄏ ‎) 1105 1105 110F HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG KHIEUKH +← (‎ ᆯᆯᆿ ‎) 11AF 11AF 11BF HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG KHIEUKH +← (‎ ퟗ ‎) D7D7 HANGUL JONGSEONG SSANGRIEUL-KHIEUKH # →ᆯᆯᆿ→ + +# ᄅᄆ ᆯᆷ ᆱ ꥨ ㄻ + (‎ ᄅᄆ ‎) 1105 1106 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG MIEUM +← (‎ ᆯᆷ ‎) 11AF 11B7 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG MIEUM +← (‎ ᆱ ‎) 11B1 HANGUL JONGSEONG RIEUL-MIEUM # →ᆯᆷ→ +← (‎ ꥨ ‎) A968 HANGUL CHOSEONG RIEUL-MIEUM +← (‎ ㄻ ‎) 313B HANGUL LETTER RIEUL-MIEUM # →ᆱ→→ᆯᆷ→ + +# ᄅᄆᄀ ᆯᆷᆨ ᇑ + (‎ ᄅᄆᄀ ‎) 1105 1106 1100 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG KIYEOK +← (‎ ᆯᆷᆨ ‎) 11AF 11B7 11A8 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG KIYEOK +← (‎ ᇑ ‎) 11D1 HANGUL JONGSEONG RIEUL-MIEUM-KIYEOK # →ᆯᆷᆨ→ + +# ᄅᄆᄉ ᆯᆷᆺ ᇒ + (‎ ᄅᄆᄉ ‎) 1105 1106 1109 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG SIOS +← (‎ ᆯᆷᆺ ‎) 11AF 11B7 11BA HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG SIOS +← (‎ ᇒ ‎) 11D2 HANGUL JONGSEONG RIEUL-MIEUM-SIOS # →ᆯᆷᆺ→ + +# ᄅᄆᄒ ᆯᆷᇂ ퟘ + (‎ ᄅᄆᄒ ‎) 1105 1106 1112 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG HIEUH +← (‎ ᆯᆷᇂ ‎) 11AF 11B7 11C2 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG HIEUH +← (‎ ퟘ ‎) D7D8 HANGUL JONGSEONG RIEUL-MIEUM-HIEUH # →ᆯᆷᇂ→ + +# ᄅᄇ ᆯᆸ ᆲ ꥩ ㄼ + (‎ ᄅᄇ ‎) 1105 1107 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP +← (‎ ᆯᆸ ‎) 11AF 11B8 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PIEUP +← (‎ ᆲ ‎) 11B2 HANGUL JONGSEONG RIEUL-PIEUP # →ᆯᆸ→ +← (‎ ꥩ ‎) A969 HANGUL CHOSEONG RIEUL-PIEUP +← (‎ ㄼ ‎) 313C HANGUL LETTER RIEUL-PIEUP # →ᆲ→→ᆯᆸ→ + +# ᄅᄇᄃ ᆯᆸᆮ ퟙ + (‎ ᄅᄇᄃ ‎) 1105 1107 1103 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG TIKEUT +← (‎ ᆯᆸᆮ ‎) 11AF 11B8 11AE HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG TIKEUT +← (‎ ퟙ ‎) D7D9 HANGUL JONGSEONG RIEUL-PIEUP-TIKEUT # →ᆯᆸᆮ→ + +# ᄅᄇᄇ ꥪ + (‎ ᄅᄇᄇ ‎) 1105 1107 1107 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG PIEUP +← (‎ ꥪ ‎) A96A HANGUL CHOSEONG RIEUL-SSANGPIEUP + +# ᄅᄇᄉ ᆯᆸᆺ ᇓ ㅫ + (‎ ᄅᄇᄉ ‎) 1105 1107 1109 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS +← (‎ ᆯᆸᆺ ‎) 11AF 11B8 11BA HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG SIOS +← (‎ ᇓ ‎) 11D3 HANGUL JONGSEONG RIEUL-PIEUP-SIOS # →ᆯᆸᆺ→ +← (‎ ㅫ ‎) 316B HANGUL LETTER RIEUL-PIEUP-SIOS # →ᇓ→→ᆯᆸᆺ→ + +# ᄅᄇᄋ ᆯᆸᆼ ᇕ ꥫ + (‎ ᄅᄇᄋ ‎) 1105 1107 110B HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG IEUNG +← (‎ ᆯᆸᆼ ‎) 11AF 11B8 11BC HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG IEUNG +← (‎ ᇕ ‎) 11D5 HANGUL JONGSEONG RIEUL-KAPYEOUNPIEUP # →ᆯᆸᆼ→ +← (‎ ꥫ ‎) A96B HANGUL CHOSEONG RIEUL-KAPYEOUNPIEUP + +# ᄅᄇᄑ ᆯᆸᇁ ퟚ + (‎ ᄅᄇᄑ ‎) 1105 1107 1111 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG PHIEUPH +← (‎ ᆯᆸᇁ ‎) 11AF 11B8 11C1 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG PHIEUPH +← (‎ ퟚ ‎) D7DA HANGUL JONGSEONG RIEUL-PIEUP-PHIEUPH # →ᆯᆸᇁ→ + +# ᄅᄇᄒ ᆯᆸᇂ ᇔ + (‎ ᄅᄇᄒ ‎) 1105 1107 1112 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG HIEUH +← (‎ ᆯᆸᇂ ‎) 11AF 11B8 11C2 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG HIEUH +← (‎ ᇔ ‎) 11D4 HANGUL JONGSEONG RIEUL-PIEUP-HIEUH # →ᆯᆸᇂ→ + +# ᄅᄉ ᆯᆺ ᆳ ꥬ ㄽ + (‎ ᄅᄉ ‎) 1105 1109 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG SIOS +← (‎ ᆯᆺ ‎) 11AF 11BA HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG SIOS +← (‎ ᆳ ‎) 11B3 HANGUL JONGSEONG RIEUL-SIOS # →ᆯᆺ→ +← (‎ ꥬ ‎) A96C HANGUL CHOSEONG RIEUL-SIOS +← (‎ ㄽ ‎) 313D HANGUL LETTER RIEUL-SIOS # →ᆳ→→ᆯᆺ→ + +# ᄅᄉᄉ ᆯᆺᆺ ᇖ + (‎ ᄅᄉᄉ ‎) 1105 1109 1109 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS +← (‎ ᆯᆺᆺ ‎) 11AF 11BA 11BA HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG SIOS +← (‎ ᇖ ‎) 11D6 HANGUL JONGSEONG RIEUL-SSANGSIOS # →ᆯᆺᆺ→ + +# ᄅᄋ ᆯᆼ ᄛ ퟝ + (‎ ᄅᄋ ‎) 1105 110B HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG IEUNG +← (‎ ᆯᆼ ‎) 11AF 11BC HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG IEUNG +← (‎ ᄛ ‎) 111B HANGUL CHOSEONG KAPYEOUNRIEUL +← (‎ ퟝ ‎) D7DD HANGUL JONGSEONG KAPYEOUNRIEUL # →ᆯᆼ→ + +# ᄅᄌ ꥭ + (‎ ᄅᄌ ‎) 1105 110C HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG CIEUC +← (‎ ꥭ ‎) A96D HANGUL CHOSEONG RIEUL-CIEUC + +# ᄅᄏ ᆯᆿ ᇘ ꥮ + (‎ ᄅᄏ ‎) 1105 110F HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG KHIEUKH +← (‎ ᆯᆿ ‎) 11AF 11BF HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG KHIEUKH +← (‎ ᇘ ‎) 11D8 HANGUL JONGSEONG RIEUL-KHIEUKH # →ᆯᆿ→ +← (‎ ꥮ ‎) A96E HANGUL CHOSEONG RIEUL-KHIEUKH + +# ᄅᄐ ᆯᇀ ᆴ ㄾ + (‎ ᄅᄐ ‎) 1105 1110 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG THIEUTH +← (‎ ᆯᇀ ‎) 11AF 11C0 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG THIEUTH +← (‎ ᆴ ‎) 11B4 HANGUL JONGSEONG RIEUL-THIEUTH # →ᆯᇀ→ +← (‎ ㄾ ‎) 313E HANGUL LETTER RIEUL-THIEUTH # →ᆴ→→ᆯᇀ→ + +# ᄅᄑ ᆯᇁ ᆵ ㄿ + (‎ ᄅᄑ ‎) 1105 1111 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PHIEUPH +← (‎ ᆯᇁ ‎) 11AF 11C1 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PHIEUPH +← (‎ ᆵ ‎) 11B5 HANGUL JONGSEONG RIEUL-PHIEUPH # →ᆯᇁ→ +← (‎ ㄿ ‎) 313F HANGUL LETTER RIEUL-PHIEUPH # →ᆵ→→ᆯᇁ→ + +# ᄅᄒ ᄉᄒ ᆯᇂ ᆺᇂ ᄚ ᄻ ᆶ ퟲ ㅀ + (‎ ᄅᄒ ‎) 1105 1112 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG HIEUH +← (‎ ᄉᄒ ‎) 1109 1112 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG HIEUH # →ᄻ→→ᄚ→ +← (‎ ᆯᇂ ‎) 11AF 11C2 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG HIEUH # →ᆶ→→ᄚ→ +← (‎ ᆺᇂ ‎) 11BA 11C2 HANGUL JONGSEONG SIOS, HANGUL JONGSEONG HIEUH # →ᄉᄒ→→ᄻ→→ᄚ→ +← (‎ ᄚ ‎) 111A HANGUL CHOSEONG RIEUL-HIEUH +← (‎ ᄻ ‎) 113B HANGUL CHOSEONG SIOS-HIEUH # →ᄚ→ +← (‎ ᆶ ‎) 11B6 HANGUL JONGSEONG RIEUL-HIEUH # →ᄚ→ +← (‎ ퟲ ‎) D7F2 HANGUL JONGSEONG SIOS-HIEUH # →ᆺᇂ→→ᄉᄒ→→ᄻ→→ᄚ→ +← (‎ ㅀ ‎) 3140 HANGUL LETTER RIEUL-HIEUH # →ᄚ→ + +# ᄅᅀ ᆯᇫ ᇗ ㅬ + (‎ ᄅᅀ ‎) 1105 1140 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PANSIOS +← (‎ ᆯᇫ ‎) 11AF 11EB HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PANSIOS +← (‎ ᇗ ‎) 11D7 HANGUL JONGSEONG RIEUL-PANSIOS # →ᆯᇫ→ +← (‎ ㅬ ‎) 316C HANGUL LETTER RIEUL-PANSIOS # →ᇗ→→ᆯᇫ→ + +# ᄅᅌ ᆯᇰ ퟛ + (‎ ᄅᅌ ‎) 1105 114C HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG YESIEUNG +← (‎ ᆯᇰ ‎) 11AF 11F0 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG YESIEUNG +← (‎ ퟛ ‎) D7DB HANGUL JONGSEONG RIEUL-YESIEUNG # →ᆯᇰ→ + +# ᄅᅙ ᆯᇹ ᇙ ㅭ + (‎ ᄅᅙ ‎) 1105 1159 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG YEORINHIEUH +← (‎ ᆯᇹ ‎) 11AF 11F9 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG YEORINHIEUH +← (‎ ᇙ ‎) 11D9 HANGUL JONGSEONG RIEUL-YEORINHIEUH # →ᆯᇹ→ +← (‎ ㅭ ‎) 316D HANGUL LETTER RIEUL-YEORINHIEUH # →ᇙ→→ᆯᇹ→ + +# ᄅᅙᄒ ᆯᇹᇂ ퟜ + (‎ ᄅᅙᄒ ‎) 1105 1159 1112 HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG YEORINHIEUH, HANGUL CHOSEONG HIEUH +← (‎ ᆯᇹᇂ ‎) 11AF 11F9 11C2 HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG YEORINHIEUH, HANGUL JONGSEONG HIEUH +← (‎ ퟜ ‎) D7DC HANGUL JONGSEONG RIEUL-YEORINHIEUH-HIEUH # →ᆯᇹᇂ→ + +# ᄆ ᆷ ㅁ + (‎ ᄆ ‎) 1106 HANGUL CHOSEONG MIEUM +← (‎ ᆷ ‎) 11B7 HANGUL JONGSEONG MIEUM +← (‎ ㅁ ‎) 3141 HANGUL LETTER MIEUM + +# ᄆᄀ ᆷᆨ ᇚ ꥯ + (‎ ᄆᄀ ‎) 1106 1100 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG KIYEOK +← (‎ ᆷᆨ ‎) 11B7 11A8 HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG KIYEOK +← (‎ ᇚ ‎) 11DA HANGUL JONGSEONG MIEUM-KIYEOK # →ᆷᆨ→ +← (‎ ꥯ ‎) A96F HANGUL CHOSEONG MIEUM-KIYEOK + +# ᄆᄂ ᆷᆫ ퟞ + (‎ ᄆᄂ ‎) 1106 1102 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG NIEUN +← (‎ ᆷᆫ ‎) 11B7 11AB HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG NIEUN +← (‎ ퟞ ‎) D7DE HANGUL JONGSEONG MIEUM-NIEUN # →ᆷᆫ→ + +# ᄆᄂᄂ ᆷᆫᆫ ퟟ + (‎ ᄆᄂᄂ ‎) 1106 1102 1102 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG NIEUN, HANGUL CHOSEONG NIEUN +← (‎ ᆷᆫᆫ ‎) 11B7 11AB 11AB HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG NIEUN, HANGUL JONGSEONG NIEUN +← (‎ ퟟ ‎) D7DF HANGUL JONGSEONG MIEUM-SSANGNIEUN # →ᆷᆫᆫ→ + +# ᄆᄃ ꥰ + (‎ ᄆᄃ ‎) 1106 1103 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG TIKEUT +← (‎ ꥰ ‎) A970 HANGUL CHOSEONG MIEUM-TIKEUT + +# ᄆᄅ ᆷᆯ ᇛ + (‎ ᄆᄅ ‎) 1106 1105 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG RIEUL +← (‎ ᆷᆯ ‎) 11B7 11AF HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG RIEUL +← (‎ ᇛ ‎) 11DB HANGUL JONGSEONG MIEUM-RIEUL # →ᆷᆯ→ + +# ᄆᄆ ᆷᆷ ퟠ + (‎ ᄆᄆ ‎) 1106 1106 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG MIEUM +← (‎ ᆷᆷ ‎) 11B7 11B7 HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG MIEUM +← (‎ ퟠ ‎) D7E0 HANGUL JONGSEONG SSANGMIEUM # →ᆷᆷ→ + +# ᄆᄇ ᆷᆸ ᄜ ᇜ ㅮ + (‎ ᄆᄇ ‎) 1106 1107 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG PIEUP +← (‎ ᆷᆸ ‎) 11B7 11B8 HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG PIEUP # →ᇜ→→ᄜ→ +← (‎ ᄜ ‎) 111C HANGUL CHOSEONG MIEUM-PIEUP +← (‎ ᇜ ‎) 11DC HANGUL JONGSEONG MIEUM-PIEUP # →ᄜ→ +← (‎ ㅮ ‎) 316E HANGUL LETTER MIEUM-PIEUP # →ᄜ→ + +# ᄆᄇᄉ ᆷᆸᆺ ퟡ + (‎ ᄆᄇᄉ ‎) 1106 1107 1109 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS +← (‎ ᆷᆸᆺ ‎) 11B7 11B8 11BA HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG SIOS +← (‎ ퟡ ‎) D7E1 HANGUL JONGSEONG MIEUM-PIEUP-SIOS # →ᆷᆸᆺ→ + +# ᄆᄉ ᆷᆺ ᇝ ꥱ ㅯ + (‎ ᄆᄉ ‎) 1106 1109 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG SIOS +← (‎ ᆷᆺ ‎) 11B7 11BA HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG SIOS +← (‎ ᇝ ‎) 11DD HANGUL JONGSEONG MIEUM-SIOS # →ᆷᆺ→ +← (‎ ꥱ ‎) A971 HANGUL CHOSEONG MIEUM-SIOS +← (‎ ㅯ ‎) 316F HANGUL LETTER MIEUM-SIOS # →ᇝ→→ᆷᆺ→ + +# ᄆᄉᄉ ᆷᆺᆺ ᇞ + (‎ ᄆᄉᄉ ‎) 1106 1109 1109 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS +← (‎ ᆷᆺᆺ ‎) 11B7 11BA 11BA HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG SIOS +← (‎ ᇞ ‎) 11DE HANGUL JONGSEONG MIEUM-SSANGSIOS # →ᆷᆺᆺ→ + +# ᄆᄋ ᆷᆼ ᄝ ᇢ ㅱ + (‎ ᄆᄋ ‎) 1106 110B HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG IEUNG +← (‎ ᆷᆼ ‎) 11B7 11BC HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG IEUNG # →ᇢ→→ᄝ→ +← (‎ ᄝ ‎) 111D HANGUL CHOSEONG KAPYEOUNMIEUM +← (‎ ᇢ ‎) 11E2 HANGUL JONGSEONG KAPYEOUNMIEUM # →ᄝ→ +← (‎ ㅱ ‎) 3171 HANGUL LETTER KAPYEOUNMIEUM # →ᄝ→ + +# ᄆᄌ ᆷᆽ ퟢ + (‎ ᄆᄌ ‎) 1106 110C HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG CIEUC +← (‎ ᆷᆽ ‎) 11B7 11BD HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG CIEUC +← (‎ ퟢ ‎) D7E2 HANGUL JONGSEONG MIEUM-CIEUC # →ᆷᆽ→ + +# ᄆᄎ ᆷᆾ ᇠ + (‎ ᄆᄎ ‎) 1106 110E HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG CHIEUCH +← (‎ ᆷᆾ ‎) 11B7 11BE HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG CHIEUCH +← (‎ ᇠ ‎) 11E0 HANGUL JONGSEONG MIEUM-CHIEUCH # →ᆷᆾ→ + +# ᄆᄒ ᆷᇂ ᇡ + (‎ ᄆᄒ ‎) 1106 1112 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG HIEUH +← (‎ ᆷᇂ ‎) 11B7 11C2 HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG HIEUH +← (‎ ᇡ ‎) 11E1 HANGUL JONGSEONG MIEUM-HIEUH # →ᆷᇂ→ + +# ᄆᅀ ᆷᇫ ᇟ ㅰ + (‎ ᄆᅀ ‎) 1106 1140 HANGUL CHOSEONG MIEUM, HANGUL CHOSEONG PANSIOS +← (‎ ᆷᇫ ‎) 11B7 11EB HANGUL JONGSEONG MIEUM, HANGUL JONGSEONG PANSIOS +← (‎ ᇟ ‎) 11DF HANGUL JONGSEONG MIEUM-PANSIOS # →ᆷᇫ→ +← (‎ ㅰ ‎) 3170 HANGUL LETTER MIEUM-PANSIOS # →ᇟ→→ᆷᇫ→ + +# ᄇ ᆸ ㅂ + (‎ ᄇ ‎) 1107 HANGUL CHOSEONG PIEUP +← (‎ ᆸ ‎) 11B8 HANGUL JONGSEONG PIEUP +← (‎ ㅂ ‎) 3142 HANGUL LETTER PIEUP + +# ᄇᄀ ᄞ ㅲ + (‎ ᄇᄀ ‎) 1107 1100 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG KIYEOK +← (‎ ᄞ ‎) 111E HANGUL CHOSEONG PIEUP-KIYEOK +← (‎ ㅲ ‎) 3172 HANGUL LETTER PIEUP-KIYEOK # →ᄞ→ + +# ᄇᄂ ᄟ + (‎ ᄇᄂ ‎) 1107 1102 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG NIEUN +← (‎ ᄟ ‎) 111F HANGUL CHOSEONG PIEUP-NIEUN + +# ᄇᄃ ᆸᆮ ᄠ ퟣ ㅳ + (‎ ᄇᄃ ‎) 1107 1103 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG TIKEUT +← (‎ ᆸᆮ ‎) 11B8 11AE HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG TIKEUT +← (‎ ᄠ ‎) 1120 HANGUL CHOSEONG PIEUP-TIKEUT +← (‎ ퟣ ‎) D7E3 HANGUL JONGSEONG PIEUP-TIKEUT # →ᆸᆮ→ +← (‎ ㅳ ‎) 3173 HANGUL LETTER PIEUP-TIKEUT # →ᄠ→ + +# ᄇᄅ ᆸᆯ ᇣ + (‎ ᄇᄅ ‎) 1107 1105 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG RIEUL +← (‎ ᆸᆯ ‎) 11B8 11AF HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG RIEUL +← (‎ ᇣ ‎) 11E3 HANGUL JONGSEONG PIEUP-RIEUL # →ᆸᆯ→ + +# ᄇᄅᄑ ᆸᆯᇁ ퟤ + (‎ ᄇᄅᄑ ‎) 1107 1105 1111 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG RIEUL, HANGUL CHOSEONG PHIEUPH +← (‎ ᆸᆯᇁ ‎) 11B8 11AF 11C1 HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG RIEUL, HANGUL JONGSEONG PHIEUPH +← (‎ ퟤ ‎) D7E4 HANGUL JONGSEONG PIEUP-RIEUL-PHIEUPH # →ᆸᆯᇁ→ + +# ᄇᄆ ᆸᆷ ퟥ + (‎ ᄇᄆ ‎) 1107 1106 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG MIEUM +← (‎ ᆸᆷ ‎) 11B8 11B7 HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG MIEUM +← (‎ ퟥ ‎) D7E5 HANGUL JONGSEONG PIEUP-MIEUM # →ᆸᆷ→ + +# ᄇᄇ ᆸᆸ ᄈ ퟦ ㅃ + (‎ ᄇᄇ ‎) 1107 1107 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG PIEUP +← (‎ ᆸᆸ ‎) 11B8 11B8 HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG PIEUP +← (‎ ᄈ ‎) 1108 HANGUL CHOSEONG SSANGPIEUP +← (‎ ퟦ ‎) D7E6 HANGUL JONGSEONG SSANGPIEUP # →ᆸᆸ→ +← (‎ ㅃ ‎) 3143 HANGUL LETTER SSANGPIEUP # →ᄈ→ + +# ᄇᄇᄋ ᄬ ㅹ + (‎ ᄇᄇᄋ ‎) 1107 1107 110B HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG IEUNG +← (‎ ᄬ ‎) 112C HANGUL CHOSEONG KAPYEOUNSSANGPIEUP +← (‎ ㅹ ‎) 3179 HANGUL LETTER KAPYEOUNSSANGPIEUP # →ᄬ→ + +# ᄇᄉ ᆸᆺ ᄡ ᆹ ㅄ + (‎ ᄇᄉ ‎) 1107 1109 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS +← (‎ ᆸᆺ ‎) 11B8 11BA HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG SIOS # →ᆹ→→ᄡ→ +← (‎ ᄡ ‎) 1121 HANGUL CHOSEONG PIEUP-SIOS +← (‎ ᆹ ‎) 11B9 HANGUL JONGSEONG PIEUP-SIOS # →ᄡ→ +← (‎ ㅄ ‎) 3144 HANGUL LETTER PIEUP-SIOS # →ᄡ→ + +# ᄇᄉᄀ ᄢ ㅴ + (‎ ᄇᄉᄀ ‎) 1107 1109 1100 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG KIYEOK +← (‎ ᄢ ‎) 1122 HANGUL CHOSEONG PIEUP-SIOS-KIYEOK +← (‎ ㅴ ‎) 3174 HANGUL LETTER PIEUP-SIOS-KIYEOK # →ᄢ→ + +# ᄇᄉᄃ ᆸᆺᆮ ᄣ ퟧ ㅵ + (‎ ᄇᄉᄃ ‎) 1107 1109 1103 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG TIKEUT +← (‎ ᆸᆺᆮ ‎) 11B8 11BA 11AE HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG TIKEUT +← (‎ ᄣ ‎) 1123 HANGUL CHOSEONG PIEUP-SIOS-TIKEUT +← (‎ ퟧ ‎) D7E7 HANGUL JONGSEONG PIEUP-SIOS-TIKEUT # →ᆸᆺᆮ→ +← (‎ ㅵ ‎) 3175 HANGUL LETTER PIEUP-SIOS-TIKEUT # →ᄣ→ + +# ᄇᄉᄇ ᄤ + (‎ ᄇᄉᄇ ‎) 1107 1109 1107 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PIEUP +← (‎ ᄤ ‎) 1124 HANGUL CHOSEONG PIEUP-SIOS-PIEUP + +# ᄇᄉᄉ ᄥ + (‎ ᄇᄉᄉ ‎) 1107 1109 1109 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS +← (‎ ᄥ ‎) 1125 HANGUL CHOSEONG PIEUP-SSANGSIOS + +# ᄇᄉᄌ ᄦ + (‎ ᄇᄉᄌ ‎) 1107 1109 110C HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG CIEUC +← (‎ ᄦ ‎) 1126 HANGUL CHOSEONG PIEUP-SIOS-CIEUC + +# ᄇᄉᄐ ꥲ + (‎ ᄇᄉᄐ ‎) 1107 1109 1110 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG THIEUTH +← (‎ ꥲ ‎) A972 HANGUL CHOSEONG PIEUP-SIOS-THIEUTH + +# ᄇᄋ ᆸᆼ ᄫ ᇦ ㅸ + (‎ ᄇᄋ ‎) 1107 110B HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG IEUNG +← (‎ ᆸᆼ ‎) 11B8 11BC HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG IEUNG # →ᇦ→→ᄫ→ +← (‎ ᄫ ‎) 112B HANGUL CHOSEONG KAPYEOUNPIEUP +← (‎ ᇦ ‎) 11E6 HANGUL JONGSEONG KAPYEOUNPIEUP # →ᄫ→ +← (‎ ㅸ ‎) 3178 HANGUL LETTER KAPYEOUNPIEUP # →ᄫ→ + +# ᄇᄌ ᆸᆽ ᄧ ퟨ ㅶ + (‎ ᄇᄌ ‎) 1107 110C HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG CIEUC +← (‎ ᆸᆽ ‎) 11B8 11BD HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG CIEUC +← (‎ ᄧ ‎) 1127 HANGUL CHOSEONG PIEUP-CIEUC +← (‎ ퟨ ‎) D7E8 HANGUL JONGSEONG PIEUP-CIEUC # →ᆸᆽ→ +← (‎ ㅶ ‎) 3176 HANGUL LETTER PIEUP-CIEUC # →ᄧ→ + +# ᄇᄎ ᆸᆾ ᄨ ퟩ + (‎ ᄇᄎ ‎) 1107 110E HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG CHIEUCH +← (‎ ᆸᆾ ‎) 11B8 11BE HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG CHIEUCH +← (‎ ᄨ ‎) 1128 HANGUL CHOSEONG PIEUP-CHIEUCH +← (‎ ퟩ ‎) D7E9 HANGUL JONGSEONG PIEUP-CHIEUCH # →ᆸᆾ→ + +# ᄇᄏ ꥳ + (‎ ᄇᄏ ‎) 1107 110F HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG KHIEUKH +← (‎ ꥳ ‎) A973 HANGUL CHOSEONG PIEUP-KHIEUKH + +# ᄇᄐ ᄩ ㅷ + (‎ ᄇᄐ ‎) 1107 1110 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG THIEUTH +← (‎ ᄩ ‎) 1129 HANGUL CHOSEONG PIEUP-THIEUTH +← (‎ ㅷ ‎) 3177 HANGUL LETTER PIEUP-THIEUTH # →ᄩ→ + +# ᄇᄑ ᆸᇁ ᄪ ᇤ + (‎ ᄇᄑ ‎) 1107 1111 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG PHIEUPH +← (‎ ᆸᇁ ‎) 11B8 11C1 HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG PHIEUPH +← (‎ ᄪ ‎) 112A HANGUL CHOSEONG PIEUP-PHIEUPH +← (‎ ᇤ ‎) 11E4 HANGUL JONGSEONG PIEUP-PHIEUPH # →ᄪ→ + +# ᄇᄒ ᆸᇂ ᇥ ꥴ + (‎ ᄇᄒ ‎) 1107 1112 HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG HIEUH +← (‎ ᆸᇂ ‎) 11B8 11C2 HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG HIEUH +← (‎ ᇥ ‎) 11E5 HANGUL JONGSEONG PIEUP-HIEUH # →ᆸᇂ→ +← (‎ ꥴ ‎) A974 HANGUL CHOSEONG PIEUP-HIEUH + +# ᄉ ᆺ ㅅ + (‎ ᄉ ‎) 1109 HANGUL CHOSEONG SIOS +← (‎ ᆺ ‎) 11BA HANGUL JONGSEONG SIOS +← (‎ ㅅ ‎) 3145 HANGUL LETTER SIOS + +# ᄉᄀ ᆺᆨ ᄭ ᇧ ㅺ + (‎ ᄉᄀ ‎) 1109 1100 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG KIYEOK +← (‎ ᆺᆨ ‎) 11BA 11A8 HANGUL JONGSEONG SIOS, HANGUL JONGSEONG KIYEOK # →ᇧ→→ᄭ→ +← (‎ ᄭ ‎) 112D HANGUL CHOSEONG SIOS-KIYEOK +← (‎ ᇧ ‎) 11E7 HANGUL JONGSEONG SIOS-KIYEOK # →ᄭ→ +← (‎ ㅺ ‎) 317A HANGUL LETTER SIOS-KIYEOK # →ᄭ→ + +# ᄉᄂ ᄮ ㅻ + (‎ ᄉᄂ ‎) 1109 1102 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG NIEUN +← (‎ ᄮ ‎) 112E HANGUL CHOSEONG SIOS-NIEUN +← (‎ ㅻ ‎) 317B HANGUL LETTER SIOS-NIEUN # →ᄮ→ + +# ᄉᄃ ᆺᆮ ᄯ ᇨ ㅼ + (‎ ᄉᄃ ‎) 1109 1103 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG TIKEUT +← (‎ ᆺᆮ ‎) 11BA 11AE HANGUL JONGSEONG SIOS, HANGUL JONGSEONG TIKEUT # →ᇨ→→ᄯ→ +← (‎ ᄯ ‎) 112F HANGUL CHOSEONG SIOS-TIKEUT +← (‎ ᇨ ‎) 11E8 HANGUL JONGSEONG SIOS-TIKEUT # →ᄯ→ +← (‎ ㅼ ‎) 317C HANGUL LETTER SIOS-TIKEUT # →ᄯ→ + +# ᄉᄅ ᆺᆯ ᄰ ᇩ + (‎ ᄉᄅ ‎) 1109 1105 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG RIEUL +← (‎ ᆺᆯ ‎) 11BA 11AF HANGUL JONGSEONG SIOS, HANGUL JONGSEONG RIEUL # →ᇩ→→ᄰ→ +← (‎ ᄰ ‎) 1130 HANGUL CHOSEONG SIOS-RIEUL +← (‎ ᇩ ‎) 11E9 HANGUL JONGSEONG SIOS-RIEUL # →ᄰ→ + +# ᄉᄆ ᆺᆷ ᄱ ퟪ + (‎ ᄉᄆ ‎) 1109 1106 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG MIEUM +← (‎ ᆺᆷ ‎) 11BA 11B7 HANGUL JONGSEONG SIOS, HANGUL JONGSEONG MIEUM +← (‎ ᄱ ‎) 1131 HANGUL CHOSEONG SIOS-MIEUM +← (‎ ퟪ ‎) D7EA HANGUL JONGSEONG SIOS-MIEUM # →ᆺᆷ→ + +# ᄉᄇ ᆺᆸ ᄲ ᇪ ㅽ + (‎ ᄉᄇ ‎) 1109 1107 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PIEUP +← (‎ ᆺᆸ ‎) 11BA 11B8 HANGUL JONGSEONG SIOS, HANGUL JONGSEONG PIEUP # →ᇪ→→ᄲ→ +← (‎ ᄲ ‎) 1132 HANGUL CHOSEONG SIOS-PIEUP +← (‎ ᇪ ‎) 11EA HANGUL JONGSEONG SIOS-PIEUP # →ᄲ→ +← (‎ ㅽ ‎) 317D HANGUL LETTER SIOS-PIEUP # →ᄲ→ + +# ᄉᄇᄀ ᄳ + (‎ ᄉᄇᄀ ‎) 1109 1107 1100 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG KIYEOK +← (‎ ᄳ ‎) 1133 HANGUL CHOSEONG SIOS-PIEUP-KIYEOK + +# ᄉᄇᄋ ᆺᆸᆼ ퟫ + (‎ ᄉᄇᄋ ‎) 1109 1107 110B HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG IEUNG +← (‎ ᆺᆸᆼ ‎) 11BA 11B8 11BC HANGUL JONGSEONG SIOS, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG IEUNG +← (‎ ퟫ ‎) D7EB HANGUL JONGSEONG SIOS-KAPYEOUNPIEUP # →ᆺᆸᆼ→ + +# ᄉᄉ ᆺᆺ ᄊ ᆻ ㅆ + (‎ ᄉᄉ ‎) 1109 1109 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS +← (‎ ᆺᆺ ‎) 11BA 11BA HANGUL JONGSEONG SIOS, HANGUL JONGSEONG SIOS # →ᆻ→→ᄊ→ +← (‎ ᄊ ‎) 110A HANGUL CHOSEONG SSANGSIOS +← (‎ ᆻ ‎) 11BB HANGUL JONGSEONG SSANGSIOS # →ᄊ→ +← (‎ ㅆ ‎) 3146 HANGUL LETTER SSANGSIOS # →ᄊ→ + +# ᄉᄉᄀ ᆺᆺᆨ ퟬ + (‎ ᄉᄉᄀ ‎) 1109 1109 1100 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG KIYEOK +← (‎ ᆺᆺᆨ ‎) 11BA 11BA 11A8 HANGUL JONGSEONG SIOS, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG KIYEOK +← (‎ ퟬ ‎) D7EC HANGUL JONGSEONG SSANGSIOS-KIYEOK # →ᆺᆺᆨ→ + +# ᄉᄉᄃ ᆺᆺᆮ ퟭ + (‎ ᄉᄉᄃ ‎) 1109 1109 1103 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG TIKEUT +← (‎ ᆺᆺᆮ ‎) 11BA 11BA 11AE HANGUL JONGSEONG SIOS, HANGUL JONGSEONG SIOS, HANGUL JONGSEONG TIKEUT +← (‎ ퟭ ‎) D7ED HANGUL JONGSEONG SSANGSIOS-TIKEUT # →ᆺᆺᆮ→ + +# ᄉᄉᄇ ꥵ + (‎ ᄉᄉᄇ ‎) 1109 1109 1107 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PIEUP +← (‎ ꥵ ‎) A975 HANGUL CHOSEONG SSANGSIOS-PIEUP + +# ᄉᄉᄉ ᄴ + (‎ ᄉᄉᄉ ‎) 1109 1109 1109 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS, HANGUL CHOSEONG SIOS +← (‎ ᄴ ‎) 1134 HANGUL CHOSEONG SIOS-SSANGSIOS + +# ᄉᄋ ᄵ + (‎ ᄉᄋ ‎) 1109 110B HANGUL CHOSEONG SIOS, HANGUL CHOSEONG IEUNG +← (‎ ᄵ ‎) 1135 HANGUL CHOSEONG SIOS-IEUNG + +# ᄉᄌ ᆺᆽ ᄶ ퟯ ㅾ + (‎ ᄉᄌ ‎) 1109 110C HANGUL CHOSEONG SIOS, HANGUL CHOSEONG CIEUC +← (‎ ᆺᆽ ‎) 11BA 11BD HANGUL JONGSEONG SIOS, HANGUL JONGSEONG CIEUC +← (‎ ᄶ ‎) 1136 HANGUL CHOSEONG SIOS-CIEUC +← (‎ ퟯ ‎) D7EF HANGUL JONGSEONG SIOS-CIEUC # →ᆺᆽ→ +← (‎ ㅾ ‎) 317E HANGUL LETTER SIOS-CIEUC # →ᄶ→ + +# ᄉᄎ ᆺᆾ ᄷ ퟰ + (‎ ᄉᄎ ‎) 1109 110E HANGUL CHOSEONG SIOS, HANGUL CHOSEONG CHIEUCH +← (‎ ᆺᆾ ‎) 11BA 11BE HANGUL JONGSEONG SIOS, HANGUL JONGSEONG CHIEUCH +← (‎ ᄷ ‎) 1137 HANGUL CHOSEONG SIOS-CHIEUCH +← (‎ ퟰ ‎) D7F0 HANGUL JONGSEONG SIOS-CHIEUCH # →ᆺᆾ→ + +# ᄉᄏ ᄸ + (‎ ᄉᄏ ‎) 1109 110F HANGUL CHOSEONG SIOS, HANGUL CHOSEONG KHIEUKH +← (‎ ᄸ ‎) 1138 HANGUL CHOSEONG SIOS-KHIEUKH + +# ᄉᄐ ᆺᇀ ᄹ ퟱ + (‎ ᄉᄐ ‎) 1109 1110 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG THIEUTH +← (‎ ᆺᇀ ‎) 11BA 11C0 HANGUL JONGSEONG SIOS, HANGUL JONGSEONG THIEUTH +← (‎ ᄹ ‎) 1139 HANGUL CHOSEONG SIOS-THIEUTH +← (‎ ퟱ ‎) D7F1 HANGUL JONGSEONG SIOS-THIEUTH # →ᆺᇀ→ + +# ᄉᄑ ᄺ + (‎ ᄉᄑ ‎) 1109 1111 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PHIEUPH +← (‎ ᄺ ‎) 113A HANGUL CHOSEONG SIOS-PHIEUPH + +# ᄉᅀ ᆺᇫ ퟮ + (‎ ᄉᅀ ‎) 1109 1140 HANGUL CHOSEONG SIOS, HANGUL CHOSEONG PANSIOS +← (‎ ᆺᇫ ‎) 11BA 11EB HANGUL JONGSEONG SIOS, HANGUL JONGSEONG PANSIOS +← (‎ ퟮ ‎) D7EE HANGUL JONGSEONG SIOS-PANSIOS # →ᆺᇫ→ + +# ᄋ ᆼ ㅇ + (‎ ᄋ ‎) 110B HANGUL CHOSEONG IEUNG +← (‎ ᆼ ‎) 11BC HANGUL JONGSEONG IEUNG +← (‎ ㅇ ‎) 3147 HANGUL LETTER IEUNG + +# ᄋᄀ ᆼᆨ ᅁ ᇬ + (‎ ᄋᄀ ‎) 110B 1100 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG KIYEOK +← (‎ ᆼᆨ ‎) 11BC 11A8 HANGUL JONGSEONG IEUNG, HANGUL JONGSEONG KIYEOK # →ᇬ→→ᅁ→ +← (‎ ᅁ ‎) 1141 HANGUL CHOSEONG IEUNG-KIYEOK +← (‎ ᇬ ‎) 11EC HANGUL JONGSEONG IEUNG-KIYEOK # →ᅁ→ + +# ᄋᄀᄀ ᆼᆨᆨ ᇭ + (‎ ᄋᄀᄀ ‎) 110B 1100 1100 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG KIYEOK, HANGUL CHOSEONG KIYEOK +← (‎ ᆼᆨᆨ ‎) 11BC 11A8 11A8 HANGUL JONGSEONG IEUNG, HANGUL JONGSEONG KIYEOK, HANGUL JONGSEONG KIYEOK +← (‎ ᇭ ‎) 11ED HANGUL JONGSEONG IEUNG-SSANGKIYEOK # →ᆼᆨᆨ→ + +# ᄋᄃ ᅂ + (‎ ᄋᄃ ‎) 110B 1103 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG TIKEUT +← (‎ ᅂ ‎) 1142 HANGUL CHOSEONG IEUNG-TIKEUT + +# ᄋᄅ ꥶ + (‎ ᄋᄅ ‎) 110B 1105 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG RIEUL +← (‎ ꥶ ‎) A976 HANGUL CHOSEONG IEUNG-RIEUL + +# ᄋᄆ ᅃ + (‎ ᄋᄆ ‎) 110B 1106 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG MIEUM +← (‎ ᅃ ‎) 1143 HANGUL CHOSEONG IEUNG-MIEUM + +# ᄋᄇ ᅄ + (‎ ᄋᄇ ‎) 110B 1107 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG PIEUP +← (‎ ᅄ ‎) 1144 HANGUL CHOSEONG IEUNG-PIEUP + +# ᄋᄉ ᅌᄉ ᇰᆺ ᅅ ᇱ ㆂ + (‎ ᄋᄉ ‎) 110B 1109 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG SIOS +← (‎ ᅌᄉ ‎) 114C 1109 HANGUL CHOSEONG YESIEUNG, HANGUL CHOSEONG SIOS # →ᇰᆺ→→ᇱ→→ᅅ→ +← (‎ ᇰᆺ ‎) 11F0 11BA HANGUL JONGSEONG YESIEUNG, HANGUL JONGSEONG SIOS # →ᇱ→→ᅅ→ +← (‎ ᅅ ‎) 1145 HANGUL CHOSEONG IEUNG-SIOS +← (‎ ᇱ ‎) 11F1 HANGUL JONGSEONG YESIEUNG-SIOS # →ᅅ→ +← (‎ ㆂ ‎) 3182 HANGUL LETTER YESIEUNG-SIOS # →ᇱ→→ᅅ→ + +# ᄋᄋ ᆼᆼ ᅇ ᇮ ㆀ + (‎ ᄋᄋ ‎) 110B 110B HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG IEUNG +← (‎ ᆼᆼ ‎) 11BC 11BC HANGUL JONGSEONG IEUNG, HANGUL JONGSEONG IEUNG # →ᇮ→→ᅇ→ +← (‎ ᅇ ‎) 1147 HANGUL CHOSEONG SSANGIEUNG +← (‎ ᇮ ‎) 11EE HANGUL JONGSEONG SSANGIEUNG # →ᅇ→ +← (‎ ㆀ ‎) 3180 HANGUL LETTER SSANGIEUNG # →ᅇ→ + +# ᄋᄌ ᅈ + (‎ ᄋᄌ ‎) 110B 110C HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG CIEUC +← (‎ ᅈ ‎) 1148 HANGUL CHOSEONG IEUNG-CIEUC + +# ᄋᄎ ᅉ + (‎ ᄋᄎ ‎) 110B 110E HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG CHIEUCH +← (‎ ᅉ ‎) 1149 HANGUL CHOSEONG IEUNG-CHIEUCH + +# ᄋᄏ ᆼᆿ ᇯ + (‎ ᄋᄏ ‎) 110B 110F HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG KHIEUKH +← (‎ ᆼᆿ ‎) 11BC 11BF HANGUL JONGSEONG IEUNG, HANGUL JONGSEONG KHIEUKH +← (‎ ᇯ ‎) 11EF HANGUL JONGSEONG IEUNG-KHIEUKH # →ᆼᆿ→ + +# ᄋᄐ ᅊ + (‎ ᄋᄐ ‎) 110B 1110 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG THIEUTH +← (‎ ᅊ ‎) 114A HANGUL CHOSEONG IEUNG-THIEUTH + +# ᄋᄑ ᅋ + (‎ ᄋᄑ ‎) 110B 1111 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG PHIEUPH +← (‎ ᅋ ‎) 114B HANGUL CHOSEONG IEUNG-PHIEUPH + +# ᄋᄒ ꥷ + (‎ ᄋᄒ ‎) 110B 1112 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG HIEUH +← (‎ ꥷ ‎) A977 HANGUL CHOSEONG IEUNG-HIEUH + +# ᄋᅀ ᅌᅀ ᇰᇫ ᅆ ᇲ ㆃ + (‎ ᄋᅀ ‎) 110B 1140 HANGUL CHOSEONG IEUNG, HANGUL CHOSEONG PANSIOS +← (‎ ᅌᅀ ‎) 114C 1140 HANGUL CHOSEONG YESIEUNG, HANGUL CHOSEONG PANSIOS # →ᇰᇫ→→ᇲ→→ᅆ→ +← (‎ ᇰᇫ ‎) 11F0 11EB HANGUL JONGSEONG YESIEUNG, HANGUL JONGSEONG PANSIOS # →ᇲ→→ᅆ→ +← (‎ ᅆ ‎) 1146 HANGUL CHOSEONG IEUNG-PANSIOS +← (‎ ᇲ ‎) 11F2 HANGUL JONGSEONG YESIEUNG-PANSIOS # →ᅆ→ +← (‎ ㆃ ‎) 3183 HANGUL LETTER YESIEUNG-PANSIOS # →ᇲ→→ᅆ→ + +# ᄌ ᆽ ㅈ + (‎ ᄌ ‎) 110C HANGUL CHOSEONG CIEUC +← (‎ ᆽ ‎) 11BD HANGUL JONGSEONG CIEUC +← (‎ ㅈ ‎) 3148 HANGUL LETTER CIEUC + +# ᄌᄇ ᆽᆸ ퟷ + (‎ ᄌᄇ ‎) 110C 1107 HANGUL CHOSEONG CIEUC, HANGUL CHOSEONG PIEUP +← (‎ ᆽᆸ ‎) 11BD 11B8 HANGUL JONGSEONG CIEUC, HANGUL JONGSEONG PIEUP +← (‎ ퟷ ‎) D7F7 HANGUL JONGSEONG CIEUC-PIEUP # →ᆽᆸ→ + +# ᄌᄇᄇ ᆽᆸᆸ ퟸ + (‎ ᄌᄇᄇ ‎) 110C 1107 1107 HANGUL CHOSEONG CIEUC, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG PIEUP +← (‎ ᆽᆸᆸ ‎) 11BD 11B8 11B8 HANGUL JONGSEONG CIEUC, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG PIEUP +← (‎ ퟸ ‎) D7F8 HANGUL JONGSEONG CIEUC-SSANGPIEUP # →ᆽᆸᆸ→ + +# ᄌᄋ ᅍ + (‎ ᄌᄋ ‎) 110C 110B HANGUL CHOSEONG CIEUC, HANGUL CHOSEONG IEUNG +← (‎ ᅍ ‎) 114D HANGUL CHOSEONG CIEUC-IEUNG + +# ᄌᄌ ᆽᆽ ᄍ ퟹ ㅉ + (‎ ᄌᄌ ‎) 110C 110C HANGUL CHOSEONG CIEUC, HANGUL CHOSEONG CIEUC +← (‎ ᆽᆽ ‎) 11BD 11BD HANGUL JONGSEONG CIEUC, HANGUL JONGSEONG CIEUC +← (‎ ᄍ ‎) 110D HANGUL CHOSEONG SSANGCIEUC +← (‎ ퟹ ‎) D7F9 HANGUL JONGSEONG SSANGCIEUC # →ᆽᆽ→ +← (‎ ㅉ ‎) 3149 HANGUL LETTER SSANGCIEUC # →ᄍ→ + +# ᄌᄌᄒ ꥸ + (‎ ᄌᄌᄒ ‎) 110C 110C 1112 HANGUL CHOSEONG CIEUC, HANGUL CHOSEONG CIEUC, HANGUL CHOSEONG HIEUH +← (‎ ꥸ ‎) A978 HANGUL CHOSEONG SSANGCIEUC-HIEUH + +# ᄎ ᆾ ㅊ + (‎ ᄎ ‎) 110E HANGUL CHOSEONG CHIEUCH +← (‎ ᆾ ‎) 11BE HANGUL JONGSEONG CHIEUCH +← (‎ ㅊ ‎) 314A HANGUL LETTER CHIEUCH + +# ᄎᄏ ᅒ + (‎ ᄎᄏ ‎) 110E 110F HANGUL CHOSEONG CHIEUCH, HANGUL CHOSEONG KHIEUKH +← (‎ ᅒ ‎) 1152 HANGUL CHOSEONG CHIEUCH-KHIEUKH + +# ᄎᄒ ᅓ + (‎ ᄎᄒ ‎) 110E 1112 HANGUL CHOSEONG CHIEUCH, HANGUL CHOSEONG HIEUH +← (‎ ᅓ ‎) 1153 HANGUL CHOSEONG CHIEUCH-HIEUH + +# ᄏ ᆿ ㅋ + (‎ ᄏ ‎) 110F HANGUL CHOSEONG KHIEUKH +← (‎ ᆿ ‎) 11BF HANGUL JONGSEONG KHIEUKH +← (‎ ㅋ ‎) 314B HANGUL LETTER KHIEUKH + +# ᄐ ᇀ ㅌ + (‎ ᄐ ‎) 1110 HANGUL CHOSEONG THIEUTH +← (‎ ᇀ ‎) 11C0 HANGUL JONGSEONG THIEUTH +← (‎ ㅌ ‎) 314C HANGUL LETTER THIEUTH + +# ᄐᄐ ꥹ + (‎ ᄐᄐ ‎) 1110 1110 HANGUL CHOSEONG THIEUTH, HANGUL CHOSEONG THIEUTH +← (‎ ꥹ ‎) A979 HANGUL CHOSEONG SSANGTHIEUTH + +# ᄑ ᇁ ㅍ + (‎ ᄑ ‎) 1111 HANGUL CHOSEONG PHIEUPH +← (‎ ᇁ ‎) 11C1 HANGUL JONGSEONG PHIEUPH +← (‎ ㅍ ‎) 314D HANGUL LETTER PHIEUPH + +# ᄑᄇ ᇁᆸ ᅖ ᇳ + (‎ ᄑᄇ ‎) 1111 1107 HANGUL CHOSEONG PHIEUPH, HANGUL CHOSEONG PIEUP +← (‎ ᇁᆸ ‎) 11C1 11B8 HANGUL JONGSEONG PHIEUPH, HANGUL JONGSEONG PIEUP # →ᇳ→→ᅖ→ +← (‎ ᅖ ‎) 1156 HANGUL CHOSEONG PHIEUPH-PIEUP +← (‎ ᇳ ‎) 11F3 HANGUL JONGSEONG PHIEUPH-PIEUP # →ᅖ→ + +# ᄑᄉ ᇁᆺ ퟺ + (‎ ᄑᄉ ‎) 1111 1109 HANGUL CHOSEONG PHIEUPH, HANGUL CHOSEONG SIOS +← (‎ ᇁᆺ ‎) 11C1 11BA HANGUL JONGSEONG PHIEUPH, HANGUL JONGSEONG SIOS +← (‎ ퟺ ‎) D7FA HANGUL JONGSEONG PHIEUPH-SIOS # →ᇁᆺ→ + +# ᄑᄋ ᇁᆼ ᅗ ᇴ ㆄ + (‎ ᄑᄋ ‎) 1111 110B HANGUL CHOSEONG PHIEUPH, HANGUL CHOSEONG IEUNG +← (‎ ᇁᆼ ‎) 11C1 11BC HANGUL JONGSEONG PHIEUPH, HANGUL JONGSEONG IEUNG # →ᇴ→→ᅗ→ +← (‎ ᅗ ‎) 1157 HANGUL CHOSEONG KAPYEOUNPHIEUPH +← (‎ ᇴ ‎) 11F4 HANGUL JONGSEONG KAPYEOUNPHIEUPH # →ᅗ→ +← (‎ ㆄ ‎) 3184 HANGUL LETTER KAPYEOUNPHIEUPH # →ᅗ→ + +# ᄑᄐ ᇁᇀ ퟻ + (‎ ᄑᄐ ‎) 1111 1110 HANGUL CHOSEONG PHIEUPH, HANGUL CHOSEONG THIEUTH +← (‎ ᇁᇀ ‎) 11C1 11C0 HANGUL JONGSEONG PHIEUPH, HANGUL JONGSEONG THIEUTH +← (‎ ퟻ ‎) D7FB HANGUL JONGSEONG PHIEUPH-THIEUTH # →ᇁᇀ→ + +# ᄑᄒ ꥺ + (‎ ᄑᄒ ‎) 1111 1112 HANGUL CHOSEONG PHIEUPH, HANGUL CHOSEONG HIEUH +← (‎ ꥺ ‎) A97A HANGUL CHOSEONG PHIEUPH-HIEUH + +# ᄒ ᇂ ㅎ + (‎ ᄒ ‎) 1112 HANGUL CHOSEONG HIEUH +← (‎ ᇂ ‎) 11C2 HANGUL JONGSEONG HIEUH +← (‎ ㅎ ‎) 314E HANGUL LETTER HIEUH + +# ᄒᄂ ᇂᆫ ᇵ + (‎ ᄒᄂ ‎) 1112 1102 HANGUL CHOSEONG HIEUH, HANGUL CHOSEONG NIEUN +← (‎ ᇂᆫ ‎) 11C2 11AB HANGUL JONGSEONG HIEUH, HANGUL JONGSEONG NIEUN +← (‎ ᇵ ‎) 11F5 HANGUL JONGSEONG HIEUH-NIEUN # →ᇂᆫ→ + +# ᄒᄅ ᇂᆯ ᇶ + (‎ ᄒᄅ ‎) 1112 1105 HANGUL CHOSEONG HIEUH, HANGUL CHOSEONG RIEUL +← (‎ ᇂᆯ ‎) 11C2 11AF HANGUL JONGSEONG HIEUH, HANGUL JONGSEONG RIEUL +← (‎ ᇶ ‎) 11F6 HANGUL JONGSEONG HIEUH-RIEUL # →ᇂᆯ→ + +# ᄒᄆ ᇂᆷ ᇷ + (‎ ᄒᄆ ‎) 1112 1106 HANGUL CHOSEONG HIEUH, HANGUL CHOSEONG MIEUM +← (‎ ᇂᆷ ‎) 11C2 11B7 HANGUL JONGSEONG HIEUH, HANGUL JONGSEONG MIEUM +← (‎ ᇷ ‎) 11F7 HANGUL JONGSEONG HIEUH-MIEUM # →ᇂᆷ→ + +# ᄒᄇ ᇂᆸ ᇸ + (‎ ᄒᄇ ‎) 1112 1107 HANGUL CHOSEONG HIEUH, HANGUL CHOSEONG PIEUP +← (‎ ᇂᆸ ‎) 11C2 11B8 HANGUL JONGSEONG HIEUH, HANGUL JONGSEONG PIEUP +← (‎ ᇸ ‎) 11F8 HANGUL JONGSEONG HIEUH-PIEUP # →ᇂᆸ→ + +# ᄒᄉ ꥻ + (‎ ᄒᄉ ‎) 1112 1109 HANGUL CHOSEONG HIEUH, HANGUL CHOSEONG SIOS +← (‎ ꥻ ‎) A97B HANGUL CHOSEONG HIEUH-SIOS + +# ᄒᄒ ᅘ ㆅ + (‎ ᄒᄒ ‎) 1112 1112 HANGUL CHOSEONG HIEUH, HANGUL CHOSEONG HIEUH +← (‎ ᅘ ‎) 1158 HANGUL CHOSEONG SSANGHIEUH +← (‎ ㆅ ‎) 3185 HANGUL LETTER SSANGHIEUH # →ᅘ→ + +# ᄼᄼ ᄽ + (‎ ᄼᄼ ‎) 113C 113C HANGUL CHOSEONG CHITUEUMSIOS, HANGUL CHOSEONG CHITUEUMSIOS +← (‎ ᄽ ‎) 113D HANGUL CHOSEONG CHITUEUMSSANGSIOS + +# ᄾᄾ ᄿ + (‎ ᄾᄾ ‎) 113E 113E HANGUL CHOSEONG CEONGCHIEUMSIOS, HANGUL CHOSEONG CEONGCHIEUMSIOS +← (‎ ᄿ ‎) 113F HANGUL CHOSEONG CEONGCHIEUMSSANGSIOS + +# ᅀ ᇫ ㅿ + (‎ ᅀ ‎) 1140 HANGUL CHOSEONG PANSIOS +← (‎ ᇫ ‎) 11EB HANGUL JONGSEONG PANSIOS +← (‎ ㅿ ‎) 317F HANGUL LETTER PANSIOS + +# ᅀᄇ ᇫᆸ ퟳ + (‎ ᅀᄇ ‎) 1140 1107 HANGUL CHOSEONG PANSIOS, HANGUL CHOSEONG PIEUP +← (‎ ᇫᆸ ‎) 11EB 11B8 HANGUL JONGSEONG PANSIOS, HANGUL JONGSEONG PIEUP +← (‎ ퟳ ‎) D7F3 HANGUL JONGSEONG PANSIOS-PIEUP # →ᇫᆸ→ + +# ᅀᄇᄋ ᇫᆸᆼ ퟴ + (‎ ᅀᄇᄋ ‎) 1140 1107 110B HANGUL CHOSEONG PANSIOS, HANGUL CHOSEONG PIEUP, HANGUL CHOSEONG IEUNG +← (‎ ᇫᆸᆼ ‎) 11EB 11B8 11BC HANGUL JONGSEONG PANSIOS, HANGUL JONGSEONG PIEUP, HANGUL JONGSEONG IEUNG +← (‎ ퟴ ‎) D7F4 HANGUL JONGSEONG PANSIOS-KAPYEOUNPIEUP # →ᇫᆸᆼ→ + +# ᅌ ᇰ ㆁ + (‎ ᅌ ‎) 114C HANGUL CHOSEONG YESIEUNG +← (‎ ᇰ ‎) 11F0 HANGUL JONGSEONG YESIEUNG +← (‎ ㆁ ‎) 3181 HANGUL LETTER YESIEUNG + +# ᅌᄆ ᇰᆷ ퟵ + (‎ ᅌᄆ ‎) 114C 1106 HANGUL CHOSEONG YESIEUNG, HANGUL CHOSEONG MIEUM +← (‎ ᇰᆷ ‎) 11F0 11B7 HANGUL JONGSEONG YESIEUNG, HANGUL JONGSEONG MIEUM +← (‎ ퟵ ‎) D7F5 HANGUL JONGSEONG YESIEUNG-MIEUM # →ᇰᆷ→ + +# ᅌᄒ ᇰᇂ ퟶ + (‎ ᅌᄒ ‎) 114C 1112 HANGUL CHOSEONG YESIEUNG, HANGUL CHOSEONG HIEUH +← (‎ ᇰᇂ ‎) 11F0 11C2 HANGUL JONGSEONG YESIEUNG, HANGUL JONGSEONG HIEUH +← (‎ ퟶ ‎) D7F6 HANGUL JONGSEONG YESIEUNG-HIEUH # →ᇰᇂ→ + +# ᅎᅎ ᅏ + (‎ ᅎᅎ ‎) 114E 114E HANGUL CHOSEONG CHITUEUMCIEUC, HANGUL CHOSEONG CHITUEUMCIEUC +← (‎ ᅏ ‎) 114F HANGUL CHOSEONG CHITUEUMSSANGCIEUC + +# ᅐᅐ ᅑ + (‎ ᅐᅐ ‎) 1150 1150 HANGUL CHOSEONG CEONGCHIEUMCIEUC, HANGUL CHOSEONG CEONGCHIEUMCIEUC +← (‎ ᅑ ‎) 1151 HANGUL CHOSEONG CEONGCHIEUMSSANGCIEUC + +# ᅙ ᇹ ㆆ + (‎ ᅙ ‎) 1159 HANGUL CHOSEONG YEORINHIEUH +← (‎ ᇹ ‎) 11F9 HANGUL JONGSEONG YEORINHIEUH +← (‎ ㆆ ‎) 3186 HANGUL LETTER YEORINHIEUH + +# ᅙᅙ ꥼ + (‎ ᅙᅙ ‎) 1159 1159 HANGUL CHOSEONG YEORINHIEUH, HANGUL CHOSEONG YEORINHIEUH +← (‎ ꥼ ‎) A97C HANGUL CHOSEONG SSANGYEORINHIEUH + +# ᅠ ㅤ + (‎ ᅠ ‎) 1160 HANGUL JUNGSEONG FILLER +← (‎ ㅤ ‎) 3164 HANGUL FILLER + +# ᅡ ㅏ + (‎ ᅡ ‎) 1161 HANGUL JUNGSEONG A +← (‎ ㅏ ‎) 314F HANGUL LETTER A + +# ᅡᅩ ᅶ + (‎ ᅡᅩ ‎) 1161 1169 HANGUL JUNGSEONG A, HANGUL JUNGSEONG O +← (‎ ᅶ ‎) 1176 HANGUL JUNGSEONG A-O + +# ᅡᅮ ᅷ + (‎ ᅡᅮ ‎) 1161 116E HANGUL JUNGSEONG A, HANGUL JUNGSEONG U +← (‎ ᅷ ‎) 1177 HANGUL JUNGSEONG A-U + +# ᅡー ᅡᅳ ᆣ + (‎ ᅡᅳ ‎) 1161 1173 HANGUL JUNGSEONG A, HANGUL JUNGSEONG EU +← (‎ ᅡー ‎) 1161 30FC HANGUL JUNGSEONG A, KATAKANA-HIRAGANA PROLONGED SOUND MARK +← (‎ ᆣ ‎) 11A3 HANGUL JUNGSEONG A-EU + +# ᅡ丨 ᅡᅵ ᅢ ㅐ + (‎ ᅡᅵ ‎) 1161 1175 HANGUL JUNGSEONG A, HANGUL JUNGSEONG I +← (‎ ᅡ丨 ‎) 1161 4E28 HANGUL JUNGSEONG A, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅢ ‎) 1162 HANGUL JUNGSEONG AE +← (‎ ㅐ ‎) 3150 HANGUL LETTER AE # →ᅢ→ + +# ᅣ ㅑ + (‎ ᅣ ‎) 1163 HANGUL JUNGSEONG YA +← (‎ ㅑ ‎) 3151 HANGUL LETTER YA + +# ᅣᅩ ᅸ + (‎ ᅣᅩ ‎) 1163 1169 HANGUL JUNGSEONG YA, HANGUL JUNGSEONG O +← (‎ ᅸ ‎) 1178 HANGUL JUNGSEONG YA-O + +# ᅣᅭ ᅹ + (‎ ᅣᅭ ‎) 1163 116D HANGUL JUNGSEONG YA, HANGUL JUNGSEONG YO +← (‎ ᅹ ‎) 1179 HANGUL JUNGSEONG YA-YO + +# ᅣᅮ ᆤ + (‎ ᅣᅮ ‎) 1163 116E HANGUL JUNGSEONG YA, HANGUL JUNGSEONG U +← (‎ ᆤ ‎) 11A4 HANGUL JUNGSEONG YA-U + +# ᅣ丨 ᅣᅵ ᅤ ㅒ + (‎ ᅣᅵ ‎) 1163 1175 HANGUL JUNGSEONG YA, HANGUL JUNGSEONG I +← (‎ ᅣ丨 ‎) 1163 4E28 HANGUL JUNGSEONG YA, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅤ ‎) 1164 HANGUL JUNGSEONG YAE +← (‎ ㅒ ‎) 3152 HANGUL LETTER YAE # →ᅤ→ + +# ᅥ ㅓ + (‎ ᅥ ‎) 1165 HANGUL JUNGSEONG EO +← (‎ ㅓ ‎) 3153 HANGUL LETTER EO + +# ᅥᅩ ᅺ + (‎ ᅥᅩ ‎) 1165 1169 HANGUL JUNGSEONG EO, HANGUL JUNGSEONG O +← (‎ ᅺ ‎) 117A HANGUL JUNGSEONG EO-O + +# ᅥᅮ ᅻ + (‎ ᅥᅮ ‎) 1165 116E HANGUL JUNGSEONG EO, HANGUL JUNGSEONG U +← (‎ ᅻ ‎) 117B HANGUL JUNGSEONG EO-U + +# ᅥー ᅥᅳ ᅼ + (‎ ᅥᅳ ‎) 1165 1173 HANGUL JUNGSEONG EO, HANGUL JUNGSEONG EU +← (‎ ᅥー ‎) 1165 30FC HANGUL JUNGSEONG EO, KATAKANA-HIRAGANA PROLONGED SOUND MARK +← (‎ ᅼ ‎) 117C HANGUL JUNGSEONG EO-EU + +# ᅥ丨 ᅥᅵ ᅦ ㅔ + (‎ ᅥᅵ ‎) 1165 1175 HANGUL JUNGSEONG EO, HANGUL JUNGSEONG I +← (‎ ᅥ丨 ‎) 1165 4E28 HANGUL JUNGSEONG EO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅦ ‎) 1166 HANGUL JUNGSEONG E +← (‎ ㅔ ‎) 3154 HANGUL LETTER E # →ᅦ→ + +# ᅧ ㅕ + (‎ ᅧ ‎) 1167 HANGUL JUNGSEONG YEO +← (‎ ㅕ ‎) 3155 HANGUL LETTER YEO + +# ᅧᅣ ᆥ + (‎ ᅧᅣ ‎) 1167 1163 HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG YA +← (‎ ᆥ ‎) 11A5 HANGUL JUNGSEONG YEO-YA + +# ᅧᅩ ᅽ + (‎ ᅧᅩ ‎) 1167 1169 HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG O +← (‎ ᅽ ‎) 117D HANGUL JUNGSEONG YEO-O + +# ᅧᅮ ᅾ + (‎ ᅧᅮ ‎) 1167 116E HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG U +← (‎ ᅾ ‎) 117E HANGUL JUNGSEONG YEO-U + +# ᅧ丨 ᅧᅵ ᅨ ㅖ + (‎ ᅧᅵ ‎) 1167 1175 HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG I +← (‎ ᅧ丨 ‎) 1167 4E28 HANGUL JUNGSEONG YEO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅨ ‎) 1168 HANGUL JUNGSEONG YE +← (‎ ㅖ ‎) 3156 HANGUL LETTER YE # →ᅨ→ + +# ᅩ ㅗ + (‎ ᅩ ‎) 1169 HANGUL JUNGSEONG O +← (‎ ㅗ ‎) 3157 HANGUL LETTER O + +# ᅩᅡ ᅪ ㅘ + (‎ ᅩᅡ ‎) 1169 1161 HANGUL JUNGSEONG O, HANGUL JUNGSEONG A +← (‎ ᅪ ‎) 116A HANGUL JUNGSEONG WA +← (‎ ㅘ ‎) 3158 HANGUL LETTER WA # →ᅪ→ + +# ᅩᅡ丨 ᅩᅡᅵ ᅫ ㅙ + (‎ ᅩᅡᅵ ‎) 1169 1161 1175 HANGUL JUNGSEONG O, HANGUL JUNGSEONG A, HANGUL JUNGSEONG I +← (‎ ᅩᅡ丨 ‎) 1169 1161 4E28 HANGUL JUNGSEONG O, HANGUL JUNGSEONG A, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅫ ‎) 116B HANGUL JUNGSEONG WAE +← (‎ ㅙ ‎) 3159 HANGUL LETTER WAE # →ᅫ→ + +# ᅩᅣ ᆦ + (‎ ᅩᅣ ‎) 1169 1163 HANGUL JUNGSEONG O, HANGUL JUNGSEONG YA +← (‎ ᆦ ‎) 11A6 HANGUL JUNGSEONG O-YA + +# ᅩᅣ丨 ᅩᅣᅵ ᆧ + (‎ ᅩᅣᅵ ‎) 1169 1163 1175 HANGUL JUNGSEONG O, HANGUL JUNGSEONG YA, HANGUL JUNGSEONG I +← (‎ ᅩᅣ丨 ‎) 1169 1163 4E28 HANGUL JUNGSEONG O, HANGUL JUNGSEONG YA, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆧ ‎) 11A7 HANGUL JUNGSEONG O-YAE + +# ᅩᅥ ᅿ + (‎ ᅩᅥ ‎) 1169 1165 HANGUL JUNGSEONG O, HANGUL JUNGSEONG EO +← (‎ ᅿ ‎) 117F HANGUL JUNGSEONG O-EO + +# ᅩᅥ丨 ᅩᅥᅵ ᆀ + (‎ ᅩᅥᅵ ‎) 1169 1165 1175 HANGUL JUNGSEONG O, HANGUL JUNGSEONG EO, HANGUL JUNGSEONG I +← (‎ ᅩᅥ丨 ‎) 1169 1165 4E28 HANGUL JUNGSEONG O, HANGUL JUNGSEONG EO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆀ ‎) 1180 HANGUL JUNGSEONG O-E + +# ᅩᅧ ힰ + (‎ ᅩᅧ ‎) 1169 1167 HANGUL JUNGSEONG O, HANGUL JUNGSEONG YEO +← (‎ ힰ ‎) D7B0 HANGUL JUNGSEONG O-YEO + +# ᅩᅧ丨 ᅩᅧᅵ ᆁ + (‎ ᅩᅧᅵ ‎) 1169 1167 1175 HANGUL JUNGSEONG O, HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG I +← (‎ ᅩᅧ丨 ‎) 1169 1167 4E28 HANGUL JUNGSEONG O, HANGUL JUNGSEONG YEO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆁ ‎) 1181 HANGUL JUNGSEONG O-YE + +# ᅩᅩ ᆂ + (‎ ᅩᅩ ‎) 1169 1169 HANGUL JUNGSEONG O, HANGUL JUNGSEONG O +← (‎ ᆂ ‎) 1182 HANGUL JUNGSEONG O-O + +# ᅩᅩ丨 ᅩᅩᅵ ힱ + (‎ ᅩᅩᅵ ‎) 1169 1169 1175 HANGUL JUNGSEONG O, HANGUL JUNGSEONG O, HANGUL JUNGSEONG I +← (‎ ᅩᅩ丨 ‎) 1169 1169 4E28 HANGUL JUNGSEONG O, HANGUL JUNGSEONG O, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ힱ ‎) D7B1 HANGUL JUNGSEONG O-O-I + +# ᅩᅮ ᆃ + (‎ ᅩᅮ ‎) 1169 116E HANGUL JUNGSEONG O, HANGUL JUNGSEONG U +← (‎ ᆃ ‎) 1183 HANGUL JUNGSEONG O-U + +# ᅩ丨 ᅩᅵ ᅬ ㅚ + (‎ ᅩᅵ ‎) 1169 1175 HANGUL JUNGSEONG O, HANGUL JUNGSEONG I +← (‎ ᅩ丨 ‎) 1169 4E28 HANGUL JUNGSEONG O, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅬ ‎) 116C HANGUL JUNGSEONG OE +← (‎ ㅚ ‎) 315A HANGUL LETTER OE # →ᅬ→ + +# ᅭ ㅛ + (‎ ᅭ ‎) 116D HANGUL JUNGSEONG YO +← (‎ ㅛ ‎) 315B HANGUL LETTER YO + +# ᅭᅡ ힲ + (‎ ᅭᅡ ‎) 116D 1161 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG A +← (‎ ힲ ‎) D7B2 HANGUL JUNGSEONG YO-A + +# ᅭᅡ丨 ᅭᅡᅵ ힳ + (‎ ᅭᅡᅵ ‎) 116D 1161 1175 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG A, HANGUL JUNGSEONG I +← (‎ ᅭᅡ丨 ‎) 116D 1161 4E28 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG A, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ힳ ‎) D7B3 HANGUL JUNGSEONG YO-AE + +# ᅭᅣ ᅭᅧ ᆄ ᆆ ㆇ + (‎ ᅭᅣ ‎) 116D 1163 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG YA +← (‎ ᅭᅧ ‎) 116D 1167 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG YEO # →ᆆ→→ᆄ→ +← (‎ ᆄ ‎) 1184 HANGUL JUNGSEONG YO-YA +← (‎ ᆆ ‎) 1186 HANGUL JUNGSEONG YO-YEO # →ᆄ→ +← (‎ ㆇ ‎) 3187 HANGUL LETTER YO-YA # →ᆄ→ + +# ᅭᅣ丨 ᅭᅣᅵ ᆅ ㆈ + (‎ ᅭᅣᅵ ‎) 116D 1163 1175 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG YA, HANGUL JUNGSEONG I +← (‎ ᅭᅣ丨 ‎) 116D 1163 4E28 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG YA, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆅ ‎) 1185 HANGUL JUNGSEONG YO-YAE +← (‎ ㆈ ‎) 3188 HANGUL LETTER YO-YAE # →ᆅ→ + +# ᅭᅥ ힴ + (‎ ᅭᅥ ‎) 116D 1165 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG EO +← (‎ ힴ ‎) D7B4 HANGUL JUNGSEONG YO-EO + +# ᅭᅩ ᆇ + (‎ ᅭᅩ ‎) 116D 1169 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG O +← (‎ ᆇ ‎) 1187 HANGUL JUNGSEONG YO-O + +# ᅭ丨 ᅭᅵ ᆈ ㆉ + (‎ ᅭᅵ ‎) 116D 1175 HANGUL JUNGSEONG YO, HANGUL JUNGSEONG I +← (‎ ᅭ丨 ‎) 116D 4E28 HANGUL JUNGSEONG YO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆈ ‎) 1188 HANGUL JUNGSEONG YO-I +← (‎ ㆉ ‎) 3189 HANGUL LETTER YO-I # →ᆈ→ + +# ᅮ ㅜ + (‎ ᅮ ‎) 116E HANGUL JUNGSEONG U +← (‎ ㅜ ‎) 315C HANGUL LETTER U + +# ᅮᅡ ᆉ + (‎ ᅮᅡ ‎) 116E 1161 HANGUL JUNGSEONG U, HANGUL JUNGSEONG A +← (‎ ᆉ ‎) 1189 HANGUL JUNGSEONG U-A + +# ᅮᅡ丨 ᅮᅡᅵ ᆊ + (‎ ᅮᅡᅵ ‎) 116E 1161 1175 HANGUL JUNGSEONG U, HANGUL JUNGSEONG A, HANGUL JUNGSEONG I +← (‎ ᅮᅡ丨 ‎) 116E 1161 4E28 HANGUL JUNGSEONG U, HANGUL JUNGSEONG A, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆊ ‎) 118A HANGUL JUNGSEONG U-AE + +# ᅮᅥ ᅯ ㅝ + (‎ ᅮᅥ ‎) 116E 1165 HANGUL JUNGSEONG U, HANGUL JUNGSEONG EO +← (‎ ᅯ ‎) 116F HANGUL JUNGSEONG WEO +← (‎ ㅝ ‎) 315D HANGUL LETTER WEO # →ᅯ→ + +# ᅮᅥー ᅮᅥᅳ ᆋ + (‎ ᅮᅥᅳ ‎) 116E 1165 1173 HANGUL JUNGSEONG U, HANGUL JUNGSEONG EO, HANGUL JUNGSEONG EU +← (‎ ᅮᅥー ‎) 116E 1165 30FC HANGUL JUNGSEONG U, HANGUL JUNGSEONG EO, KATAKANA-HIRAGANA PROLONGED SOUND MARK +← (‎ ᆋ ‎) 118B HANGUL JUNGSEONG U-EO-EU + +# ᅮᅥ丨 ᅮᅥᅵ ᅰ ㅞ + (‎ ᅮᅥᅵ ‎) 116E 1165 1175 HANGUL JUNGSEONG U, HANGUL JUNGSEONG EO, HANGUL JUNGSEONG I +← (‎ ᅮᅥ丨 ‎) 116E 1165 4E28 HANGUL JUNGSEONG U, HANGUL JUNGSEONG EO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅰ ‎) 1170 HANGUL JUNGSEONG WE +← (‎ ㅞ ‎) 315E HANGUL LETTER WE # →ᅰ→ + +# ᅮᅧ ힵ + (‎ ᅮᅧ ‎) 116E 1167 HANGUL JUNGSEONG U, HANGUL JUNGSEONG YEO +← (‎ ힵ ‎) D7B5 HANGUL JUNGSEONG U-YEO + +# ᅮᅧ丨 ᅮᅧᅵ ᆌ + (‎ ᅮᅧᅵ ‎) 116E 1167 1175 HANGUL JUNGSEONG U, HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG I +← (‎ ᅮᅧ丨 ‎) 116E 1167 4E28 HANGUL JUNGSEONG U, HANGUL JUNGSEONG YEO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆌ ‎) 118C HANGUL JUNGSEONG U-YE + +# ᅮᅮ ᆍ + (‎ ᅮᅮ ‎) 116E 116E HANGUL JUNGSEONG U, HANGUL JUNGSEONG U +← (‎ ᆍ ‎) 118D HANGUL JUNGSEONG U-U + +# ᅮ丨 ᅮᅵ ᅱ ㅟ + (‎ ᅮᅵ ‎) 116E 1175 HANGUL JUNGSEONG U, HANGUL JUNGSEONG I +← (‎ ᅮ丨 ‎) 116E 4E28 HANGUL JUNGSEONG U, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅱ ‎) 1171 HANGUL JUNGSEONG WI +← (‎ ㅟ ‎) 315F HANGUL LETTER WI # →ᅱ→ + +# ᅮ丨丨 ᅮᅵᅵ ힶ + (‎ ᅮᅵᅵ ‎) 116E 1175 1175 HANGUL JUNGSEONG U, HANGUL JUNGSEONG I, HANGUL JUNGSEONG I +← (‎ ᅮ丨丨 ‎) 116E 4E28 4E28 HANGUL JUNGSEONG U, CJK UNIFIED IDEOGRAPH-4E28, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ힶ ‎) D7B6 HANGUL JUNGSEONG U-I-I + +# ᅲ ㅠ + (‎ ᅲ ‎) 1172 HANGUL JUNGSEONG YU +← (‎ ㅠ ‎) 3160 HANGUL LETTER YU + +# ᅲᅡ ᆎ + (‎ ᅲᅡ ‎) 1172 1161 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG A +← (‎ ᆎ ‎) 118E HANGUL JUNGSEONG YU-A + +# ᅲᅡ丨 ᅲᅡᅵ ힷ + (‎ ᅲᅡᅵ ‎) 1172 1161 1175 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG A, HANGUL JUNGSEONG I +← (‎ ᅲᅡ丨 ‎) 1172 1161 4E28 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG A, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ힷ ‎) D7B7 HANGUL JUNGSEONG YU-AE + +# ᅲᅥ ᆏ + (‎ ᅲᅥ ‎) 1172 1165 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG EO +← (‎ ᆏ ‎) 118F HANGUL JUNGSEONG YU-EO + +# ᅲᅥ丨 ᅲᅥᅵ ᆐ + (‎ ᅲᅥᅵ ‎) 1172 1165 1175 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG EO, HANGUL JUNGSEONG I +← (‎ ᅲᅥ丨 ‎) 1172 1165 4E28 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG EO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆐ ‎) 1190 HANGUL JUNGSEONG YU-E + +# ᅲᅧ ᆑ ㆊ + (‎ ᅲᅧ ‎) 1172 1167 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG YEO +← (‎ ᆑ ‎) 1191 HANGUL JUNGSEONG YU-YEO +← (‎ ㆊ ‎) 318A HANGUL LETTER YU-YEO # →ᆑ→ + +# ᅲᅧ丨 ᅲᅧᅵ ᆒ ㆋ + (‎ ᅲᅧᅵ ‎) 1172 1167 1175 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG I +← (‎ ᅲᅧ丨 ‎) 1172 1167 4E28 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG YEO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆒ ‎) 1192 HANGUL JUNGSEONG YU-YE +← (‎ ㆋ ‎) 318B HANGUL LETTER YU-YE # →ᆒ→ + +# ᅲᅩ ힸ + (‎ ᅲᅩ ‎) 1172 1169 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG O +← (‎ ힸ ‎) D7B8 HANGUL JUNGSEONG YU-O + +# ᅲᅮ ᆓ + (‎ ᅲᅮ ‎) 1172 116E HANGUL JUNGSEONG YU, HANGUL JUNGSEONG U +← (‎ ᆓ ‎) 1193 HANGUL JUNGSEONG YU-U + +# ᅲ丨 ᅲᅵ ᆔ ㆌ + (‎ ᅲᅵ ‎) 1172 1175 HANGUL JUNGSEONG YU, HANGUL JUNGSEONG I +← (‎ ᅲ丨 ‎) 1172 4E28 HANGUL JUNGSEONG YU, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆔ ‎) 1194 HANGUL JUNGSEONG YU-I +← (‎ ㆌ ‎) 318C HANGUL LETTER YU-I # →ᆔ→ + +# ー 一 ᅳ — ― ─ ━ ㇐ ꟷ ㅡ ⼀ - + (‎ ᅳ ‎) 1173 HANGUL JUNGSEONG EU +← (‎ ー ‎) 30FC KATAKANA-HIRAGANA PROLONGED SOUND MARK # →一→→—→→ㅡ→ +← (‎ 一 ‎) 4E00 CJK UNIFIED IDEOGRAPH-4E00 # →—→→ㅡ→ +← (‎ — ‎) 2014 EM DASH # →ㅡ→ +← (‎ ― ‎) 2015 HORIZONTAL BAR # →—→→ㅡ→ +← (‎ ─ ‎) 2500 BOX DRAWINGS LIGHT HORIZONTAL # →━→→—→→ㅡ→ +← (‎ ━ ‎) 2501 BOX DRAWINGS HEAVY HORIZONTAL # →—→→ㅡ→ +← (‎ ㇐ ‎) 31D0 CJK STROKE H # →一→→—→→ㅡ→ +← (‎ ꟷ ‎) A7F7 LATIN EPIGRAPHIC LETTER SIDEWAYS I # →—→→ㅡ→ +← (‎ ㅡ ‎) 3161 HANGUL LETTER EU +← (‎ ⼀ ‎) 2F00 KANGXI RADICAL ONE # →一→→—→→ㅡ→ +← (‎ - ‎) FF0D FULLWIDTH HYPHEN-MINUS # →ー→→一→→—→→ㅡ→ + +# ーᅡ ᅳᅡ ힹ + (‎ ᅳᅡ ‎) 1173 1161 HANGUL JUNGSEONG EU, HANGUL JUNGSEONG A +← (‎ ーᅡ ‎) 30FC 1161 KATAKANA-HIRAGANA PROLONGED SOUND MARK, HANGUL JUNGSEONG A +← (‎ ힹ ‎) D7B9 HANGUL JUNGSEONG EU-A + +# ーᅥ ᅳᅥ ힺ + (‎ ᅳᅥ ‎) 1173 1165 HANGUL JUNGSEONG EU, HANGUL JUNGSEONG EO +← (‎ ーᅥ ‎) 30FC 1165 KATAKANA-HIRAGANA PROLONGED SOUND MARK, HANGUL JUNGSEONG EO +← (‎ ힺ ‎) D7BA HANGUL JUNGSEONG EU-EO + +# ーᅥ丨 ᅳᅥᅵ ힻ + (‎ ᅳᅥᅵ ‎) 1173 1165 1175 HANGUL JUNGSEONG EU, HANGUL JUNGSEONG EO, HANGUL JUNGSEONG I +← (‎ ーᅥ丨 ‎) 30FC 1165 4E28 KATAKANA-HIRAGANA PROLONGED SOUND MARK, HANGUL JUNGSEONG EO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ힻ ‎) D7BB HANGUL JUNGSEONG EU-E + +# ーᅩ ᅳᅩ ힼ + (‎ ᅳᅩ ‎) 1173 1169 HANGUL JUNGSEONG EU, HANGUL JUNGSEONG O +← (‎ ーᅩ ‎) 30FC 1169 KATAKANA-HIRAGANA PROLONGED SOUND MARK, HANGUL JUNGSEONG O +← (‎ ힼ ‎) D7BC HANGUL JUNGSEONG EU-O + +# ーᅮ ᅳᅮ ᆕ + (‎ ᅳᅮ ‎) 1173 116E HANGUL JUNGSEONG EU, HANGUL JUNGSEONG U +← (‎ ーᅮ ‎) 30FC 116E KATAKANA-HIRAGANA PROLONGED SOUND MARK, HANGUL JUNGSEONG U +← (‎ ᆕ ‎) 1195 HANGUL JUNGSEONG EU-U + +# ーー ᅳᅳ ᆖ + (‎ ᅳᅳ ‎) 1173 1173 HANGUL JUNGSEONG EU, HANGUL JUNGSEONG EU +← (‎ ーー ‎) 30FC 30FC KATAKANA-HIRAGANA PROLONGED SOUND MARK, KATAKANA-HIRAGANA PROLONGED SOUND MARK +← (‎ ᆖ ‎) 1196 HANGUL JUNGSEONG EU-EU + +# ー丨 ᅳᅵ ᅴ ㅢ + (‎ ᅳᅵ ‎) 1173 1175 HANGUL JUNGSEONG EU, HANGUL JUNGSEONG I +← (‎ ー丨 ‎) 30FC 4E28 KATAKANA-HIRAGANA PROLONGED SOUND MARK, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᅴ ‎) 1174 HANGUL JUNGSEONG YI +← (‎ ㅢ ‎) 3162 HANGUL LETTER YI # →ᅴ→ + +# ー丨ᅮ ᅳᅵᅮ ᆗ + (‎ ᅳᅵᅮ ‎) 1173 1175 116E HANGUL JUNGSEONG EU, HANGUL JUNGSEONG I, HANGUL JUNGSEONG U +← (‎ ー丨ᅮ ‎) 30FC 4E28 116E KATAKANA-HIRAGANA PROLONGED SOUND MARK, CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG U +← (‎ ᆗ ‎) 1197 HANGUL JUNGSEONG YI-U + +# 丨 ᅵ ⎜ ⎟ ⎢ ⎥ ⎪ ⎮ ㇑ ㅣ ⼁ + (‎ ᅵ ‎) 1175 HANGUL JUNGSEONG I +← (‎ 丨 ‎) 4E28 CJK UNIFIED IDEOGRAPH-4E28 # →ㅣ→ +← (‎ ⎜ ‎) 239C LEFT PARENTHESIS EXTENSION # →⎥→→⎮→→丨→→ㅣ→ +← (‎ ⎟ ‎) 239F RIGHT PARENTHESIS EXTENSION # →⎥→→⎮→→丨→→ㅣ→ +← (‎ ⎢ ‎) 23A2 LEFT SQUARE BRACKET EXTENSION # →⎥→→⎮→→丨→→ㅣ→ +← (‎ ⎥ ‎) 23A5 RIGHT SQUARE BRACKET EXTENSION # →⎮→→丨→→ㅣ→ +← (‎ ⎪ ‎) 23AA CURLY BRACKET EXTENSION # →⎥→→⎮→→丨→→ㅣ→ +← (‎ ⎮ ‎) 23AE INTEGRAL EXTENSION # →丨→→ㅣ→ +← (‎ ㇑ ‎) 31D1 CJK STROKE S # →丨→→ㅣ→ +← (‎ ㅣ ‎) 3163 HANGUL LETTER I +← (‎ ⼁ ‎) 2F01 KANGXI RADICAL LINE # →丨→→ㅣ→ + +# 丨ᅡ ᅵᅡ ᆘ + (‎ ᅵᅡ ‎) 1175 1161 HANGUL JUNGSEONG I, HANGUL JUNGSEONG A +← (‎ 丨ᅡ ‎) 4E28 1161 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG A +← (‎ ᆘ ‎) 1198 HANGUL JUNGSEONG I-A + +# 丨ᅣ ᅵᅣ ᆙ + (‎ ᅵᅣ ‎) 1175 1163 HANGUL JUNGSEONG I, HANGUL JUNGSEONG YA +← (‎ 丨ᅣ ‎) 4E28 1163 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YA +← (‎ ᆙ ‎) 1199 HANGUL JUNGSEONG I-YA + +# 丨ᅣᅩ ᅵᅣᅩ ힽ + (‎ ᅵᅣᅩ ‎) 1175 1163 1169 HANGUL JUNGSEONG I, HANGUL JUNGSEONG YA, HANGUL JUNGSEONG O +← (‎ 丨ᅣᅩ ‎) 4E28 1163 1169 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YA, HANGUL JUNGSEONG O +← (‎ ힽ ‎) D7BD HANGUL JUNGSEONG I-YA-O + +# 丨ᅣ丨 ᅵᅣᅵ ힾ + (‎ ᅵᅣᅵ ‎) 1175 1163 1175 HANGUL JUNGSEONG I, HANGUL JUNGSEONG YA, HANGUL JUNGSEONG I +← (‎ 丨ᅣ丨 ‎) 4E28 1163 4E28 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YA, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ힾ ‎) D7BE HANGUL JUNGSEONG I-YAE + +# 丨ᅧ ᅵᅧ ힿ + (‎ ᅵᅧ ‎) 1175 1167 HANGUL JUNGSEONG I, HANGUL JUNGSEONG YEO +← (‎ 丨ᅧ ‎) 4E28 1167 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YEO +← (‎ ힿ ‎) D7BF HANGUL JUNGSEONG I-YEO + +# 丨ᅧ丨 ᅵᅧᅵ ퟀ + (‎ ᅵᅧᅵ ‎) 1175 1167 1175 HANGUL JUNGSEONG I, HANGUL JUNGSEONG YEO, HANGUL JUNGSEONG I +← (‎ 丨ᅧ丨 ‎) 4E28 1167 4E28 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YEO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ퟀ ‎) D7C0 HANGUL JUNGSEONG I-YE + +# 丨ᅩ ᅵᅩ ᆚ + (‎ ᅵᅩ ‎) 1175 1169 HANGUL JUNGSEONG I, HANGUL JUNGSEONG O +← (‎ 丨ᅩ ‎) 4E28 1169 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG O +← (‎ ᆚ ‎) 119A HANGUL JUNGSEONG I-O + +# 丨ᅩ丨 ᅵᅩᅵ ퟁ + (‎ ᅵᅩᅵ ‎) 1175 1169 1175 HANGUL JUNGSEONG I, HANGUL JUNGSEONG O, HANGUL JUNGSEONG I +← (‎ 丨ᅩ丨 ‎) 4E28 1169 4E28 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG O, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ퟁ ‎) D7C1 HANGUL JUNGSEONG I-O-I + +# 丨ᅭ ᅵᅭ ퟂ + (‎ ᅵᅭ ‎) 1175 116D HANGUL JUNGSEONG I, HANGUL JUNGSEONG YO +← (‎ 丨ᅭ ‎) 4E28 116D CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YO +← (‎ ퟂ ‎) D7C2 HANGUL JUNGSEONG I-YO + +# 丨ᅮ ᅵᅮ ᆛ + (‎ ᅵᅮ ‎) 1175 116E HANGUL JUNGSEONG I, HANGUL JUNGSEONG U +← (‎ 丨ᅮ ‎) 4E28 116E CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG U +← (‎ ᆛ ‎) 119B HANGUL JUNGSEONG I-U + +# 丨ᅲ ᅵᅲ ퟃ + (‎ ᅵᅲ ‎) 1175 1172 HANGUL JUNGSEONG I, HANGUL JUNGSEONG YU +← (‎ 丨ᅲ ‎) 4E28 1172 CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG YU +← (‎ ퟃ ‎) D7C3 HANGUL JUNGSEONG I-YU + +# 丨ー ᅵᅳ ᆜ + (‎ ᅵᅳ ‎) 1175 1173 HANGUL JUNGSEONG I, HANGUL JUNGSEONG EU +← (‎ 丨ー ‎) 4E28 30FC CJK UNIFIED IDEOGRAPH-4E28, KATAKANA-HIRAGANA PROLONGED SOUND MARK +← (‎ ᆜ ‎) 119C HANGUL JUNGSEONG I-EU + +# 丨丨 ᅵᅵ ퟄ + (‎ ᅵᅵ ‎) 1175 1175 HANGUL JUNGSEONG I, HANGUL JUNGSEONG I +← (‎ 丨丨 ‎) 4E28 4E28 CJK UNIFIED IDEOGRAPH-4E28, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ퟄ ‎) D7C4 HANGUL JUNGSEONG I-I + +# 丨ᆞ ᅵᆞ ᆝ + (‎ ᅵᆞ ‎) 1175 119E HANGUL JUNGSEONG I, HANGUL JUNGSEONG ARAEA +← (‎ 丨ᆞ ‎) 4E28 119E CJK UNIFIED IDEOGRAPH-4E28, HANGUL JUNGSEONG ARAEA +← (‎ ᆝ ‎) 119D HANGUL JUNGSEONG I-ARAEA + +# ᆞ ㆍ + (‎ ᆞ ‎) 119E HANGUL JUNGSEONG ARAEA +← (‎ ㆍ ‎) 318D HANGUL LETTER ARAEA + +# ᆞᅡ ퟅ + (‎ ᆞᅡ ‎) 119E 1161 HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG A +← (‎ ퟅ ‎) D7C5 HANGUL JUNGSEONG ARAEA-A + +# ᆞᅥ ᆟ + (‎ ᆞᅥ ‎) 119E 1165 HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG EO +← (‎ ᆟ ‎) 119F HANGUL JUNGSEONG ARAEA-EO + +# ᆞᅥ丨 ᆞᅥᅵ ퟆ + (‎ ᆞᅥᅵ ‎) 119E 1165 1175 HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG EO, HANGUL JUNGSEONG I +← (‎ ᆞᅥ丨 ‎) 119E 1165 4E28 HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG EO, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ퟆ ‎) D7C6 HANGUL JUNGSEONG ARAEA-E + +# ᆞᅮ ᆠ + (‎ ᆞᅮ ‎) 119E 116E HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG U +← (‎ ᆠ ‎) 11A0 HANGUL JUNGSEONG ARAEA-U + +# ᆞ丨 ᆞᅵ ᆡ ㆎ + (‎ ᆞᅵ ‎) 119E 1175 HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG I +← (‎ ᆞ丨 ‎) 119E 4E28 HANGUL JUNGSEONG ARAEA, CJK UNIFIED IDEOGRAPH-4E28 +← (‎ ᆡ ‎) 11A1 HANGUL JUNGSEONG ARAEA-I +← (‎ ㆎ ‎) 318E HANGUL LETTER ARAEAE # →ᆡ→ + +# ᆞᆞ ᆢ + (‎ ᆞᆞ ‎) 119E 119E HANGUL JUNGSEONG ARAEA, HANGUL JUNGSEONG ARAEA +← (‎ ᆢ ‎) 11A2 HANGUL JUNGSEONG SSANGARAEA + +# Ꮿ ῶ + (‎ Ꮿ ‎) 13EF CHEROKEE LETTER YA +← (‎ ῶ ‎) 1FF6 GREEK SMALL LETTER OMEGA WITH PERISPOMENI + +# ᐁ· ᐁᐧ ᐍ + (‎ ᐁ· ‎) 1401 00B7 CANADIAN SYLLABICS E, MIDDLE DOT +← (‎ ᐁᐧ ‎) 1401 1427 CANADIAN SYLLABICS E, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐍ ‎) 140D CANADIAN SYLLABICS WEST-CREE WE # →ᐁᐧ→ + +# ᐁᐠ ᐫ + (‎ ᐁᐠ ‎) 1401 1420 CANADIAN SYLLABICS E, CANADIAN SYLLABICS FINAL GRAVE +← (‎ ᐫ ‎) 142B CANADIAN SYLLABICS EN + +# ᐄ· ᐄᐧ ᐑ + (‎ ᐄ· ‎) 1404 00B7 CANADIAN SYLLABICS II, MIDDLE DOT +← (‎ ᐄᐧ ‎) 1404 1427 CANADIAN SYLLABICS II, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐑ ‎) 1411 CANADIAN SYLLABICS WEST-CREE WII # →ᐄᐧ→ + +# ᐅ· ᐅᐧ ᐓ + (‎ ᐅ· ‎) 1405 00B7 CANADIAN SYLLABICS O, MIDDLE DOT +← (‎ ᐅᐧ ‎) 1405 1427 CANADIAN SYLLABICS O, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐓ ‎) 1413 CANADIAN SYLLABICS WEST-CREE WO # →ᐅᐧ→ + +# ᐅᐠ ᐭ + (‎ ᐅᐠ ‎) 1405 1420 CANADIAN SYLLABICS O, CANADIAN SYLLABICS FINAL GRAVE +← (‎ ᐭ ‎) 142D CANADIAN SYLLABICS ON + +# ᐆ· ᐆᐧ ᐕ + (‎ ᐆ· ‎) 1406 00B7 CANADIAN SYLLABICS OO, MIDDLE DOT +← (‎ ᐆᐧ ‎) 1406 1427 CANADIAN SYLLABICS OO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐕ ‎) 1415 CANADIAN SYLLABICS WEST-CREE WOO # →ᐆᐧ→ + +# ᐊ· ᐊᐧ ᐘ + (‎ ᐊ· ‎) 140A 00B7 CANADIAN SYLLABICS A, MIDDLE DOT +← (‎ ᐊᐧ ‎) 140A 1427 CANADIAN SYLLABICS A, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐘ ‎) 1418 CANADIAN SYLLABICS WEST-CREE WA # →ᐊᐧ→ + +# ᐊᐠ ᐮ + (‎ ᐊᐠ ‎) 140A 1420 CANADIAN SYLLABICS A, CANADIAN SYLLABICS FINAL GRAVE +← (‎ ᐮ ‎) 142E CANADIAN SYLLABICS AN + +# ᐋ· ᐋᐧ ᐚ + (‎ ᐋ· ‎) 140B 00B7 CANADIAN SYLLABICS AA, MIDDLE DOT +← (‎ ᐋᐧ ‎) 140B 1427 CANADIAN SYLLABICS AA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐚ ‎) 141A CANADIAN SYLLABICS WEST-CREE WAA # →ᐋᐧ→ + +# ᐞᣟ ᣝ + (‎ ᐞᣟ ‎) 141E 18DF CANADIAN SYLLABICS GLOTTAL STOP, CANADIAN SYLLABICS FINAL RAISED DOT +← (‎ ᣝ ‎) 18DD CANADIAN SYLLABICS WESTERN W + +# ᐡ ᓑ + (‎ ᐡ ‎) 1421 CANADIAN SYLLABICS FINAL BOTTOM HALF RING +← (‎ ᓑ ‎) 14D1 CANADIAN SYLLABICS CARRIER NG + +# ᐩ ᕀ + (‎ ᐩ ‎) 1429 CANADIAN SYLLABICS FINAL PLUS +← (‎ ᕀ ‎) 1540 CANADIAN SYLLABICS WEST-CREE Y + +# ᐲ· ᐲᐧ ᐿ + (‎ ᐲ· ‎) 1432 00B7 CANADIAN SYLLABICS PII, MIDDLE DOT +← (‎ ᐲᐧ ‎) 1432 1427 CANADIAN SYLLABICS PII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᐿ ‎) 143F CANADIAN SYLLABICS WEST-CREE PWII # →ᐲᐧ→ + +# ᐴ· ᐴᐧ ᑃ + (‎ ᐴ· ‎) 1434 00B7 CANADIAN SYLLABICS POO, MIDDLE DOT +← (‎ ᐴᐧ ‎) 1434 1427 CANADIAN SYLLABICS POO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑃ ‎) 1443 CANADIAN SYLLABICS WEST-CREE PWOO # →ᐴᐧ→ + +# ᐵ ⍩ + (‎ ᐵ ‎) 1435 CANADIAN SYLLABICS Y-CREE POO +← (‎ ⍩ ‎) 2369 APL FUNCTIONAL SYMBOL GREATER-THAN DIAERESIS + +# ᐹ· ᐹᐧ ᑇ + (‎ ᐹ· ‎) 1439 00B7 CANADIAN SYLLABICS PAA, MIDDLE DOT +← (‎ ᐹᐧ ‎) 1439 1427 CANADIAN SYLLABICS PAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑇ ‎) 1447 CANADIAN SYLLABICS WEST-CREE PWAA # →ᐹᐧ→ + +# ᑏ· ᑏᐧ ᑜ + (‎ ᑏ· ‎) 144F 00B7 CANADIAN SYLLABICS TII, MIDDLE DOT +← (‎ ᑏᐧ ‎) 144F 1427 CANADIAN SYLLABICS TII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑜ ‎) 145C CANADIAN SYLLABICS WEST-CREE TWII # →ᑏᐧ→ + +# ᑐ ⊃ ⸧ + (‎ ᑐ ‎) 1450 CANADIAN SYLLABICS TO +← (‎ ⊃ ‎) 2283 SUPERSET OF +← (‎ ⸧ ‎) 2E27 RIGHT SIDEWAYS U BRACKET # →⊃→ + +# ᑐ' ᑐᑊ ᑩ + (‎ ᑐ' ‎) 1450 0027 CANADIAN SYLLABICS TO, APOSTROPHE +← (‎ ᑐᑊ ‎) 1450 144A CANADIAN SYLLABICS TO, CANADIAN SYLLABICS WEST-CREE P +← (‎ ᑩ ‎) 1469 CANADIAN SYLLABICS TTO # →ᑐᑊ→ + +# ᑐ/ ⊃/ ⟉ + (‎ ᑐ/ ‎) 1450 002F CANADIAN SYLLABICS TO, SOLIDUS +← (‎ ⊃/ ‎) 2283 002F SUPERSET OF, SOLIDUS +← (‎ ⟉ ‎) 27C9 SUPERSET PRECEDING SOLIDUS # →⊃/→ + +# ᑐ· ᑐᐧ ᑞ + (‎ ᑐ· ‎) 1450 00B7 CANADIAN SYLLABICS TO, MIDDLE DOT +← (‎ ᑐᐧ ‎) 1450 1427 CANADIAN SYLLABICS TO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑞ ‎) 145E CANADIAN SYLLABICS WEST-CREE TWO # →ᑐᐧ→ + +# ᑐᑕ ⊃⊂ ⫗ + (‎ ᑐᑕ ‎) 1450 1455 CANADIAN SYLLABICS TO, CANADIAN SYLLABICS TA +← (‎ ⊃⊂ ‎) 2283 2282 SUPERSET OF, SUBSET OF +← (‎ ⫗ ‎) 2AD7 SUPERSET BESIDE SUBSET # →⊃⊂→ + +# ᑑ· ᑑᐧ ᑠ + (‎ ᑑ· ‎) 1451 00B7 CANADIAN SYLLABICS TOO, MIDDLE DOT +← (‎ ᑑᐧ ‎) 1451 1427 CANADIAN SYLLABICS TOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑠ ‎) 1460 CANADIAN SYLLABICS WEST-CREE TWOO # →ᑑᐧ→ + +# ᑕ ⊂ ⸦ + (‎ ᑕ ‎) 1455 CANADIAN SYLLABICS TA +← (‎ ⊂ ‎) 2282 SUBSET OF +← (‎ ⸦ ‎) 2E26 LEFT SIDEWAYS U BRACKET # →⊂→ + +# ᑕ' ᑕᑊ ᑪ + (‎ ᑕ' ‎) 1455 0027 CANADIAN SYLLABICS TA, APOSTROPHE +← (‎ ᑕᑊ ‎) 1455 144A CANADIAN SYLLABICS TA, CANADIAN SYLLABICS WEST-CREE P +← (‎ ᑪ ‎) 146A CANADIAN SYLLABICS TTA # →ᑕᑊ→ + +# ᑕ· ᑕᐧ ᑢ + (‎ ᑕ· ‎) 1455 00B7 CANADIAN SYLLABICS TA, MIDDLE DOT +← (‎ ᑕᐧ ‎) 1455 1427 CANADIAN SYLLABICS TA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑢ ‎) 1462 CANADIAN SYLLABICS WEST-CREE TWA # →ᑕᐧ→ + +# ᑖ· ᑖᐧ ᑤ + (‎ ᑖ· ‎) 1456 00B7 CANADIAN SYLLABICS TAA, MIDDLE DOT +← (‎ ᑖᐧ ‎) 1456 1427 CANADIAN SYLLABICS TAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑤ ‎) 1464 CANADIAN SYLLABICS WEST-CREE TWAA # →ᑖᐧ→ + +# ᑫ' ᑫᑊ ᒅ + (‎ ᑫ' ‎) 146B 0027 CANADIAN SYLLABICS KE, APOSTROPHE +← (‎ ᑫᑊ ‎) 146B 144A CANADIAN SYLLABICS KE, CANADIAN SYLLABICS WEST-CREE P +← (‎ ᒅ ‎) 1485 CANADIAN SYLLABICS SOUTH-SLAVEY KEH # →ᑫᑊ→ + +# ᑫ· ᑫᐧ ᑵ + (‎ ᑫ· ‎) 146B 00B7 CANADIAN SYLLABICS KE, MIDDLE DOT +← (‎ ᑫᐧ ‎) 146B 1427 CANADIAN SYLLABICS KE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑵ ‎) 1475 CANADIAN SYLLABICS WEST-CREE KWE # →ᑫᐧ→ + +# ᑮ· ᑮᐧ ᑹ + (‎ ᑮ· ‎) 146E 00B7 CANADIAN SYLLABICS KII, MIDDLE DOT +← (‎ ᑮᐧ ‎) 146E 1427 CANADIAN SYLLABICS KII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑹ ‎) 1479 CANADIAN SYLLABICS WEST-CREE KWII # →ᑮᐧ→ + +# ᑰ· ᑰᐧ ᑽ + (‎ ᑰ· ‎) 1470 00B7 CANADIAN SYLLABICS KOO, MIDDLE DOT +← (‎ ᑰᐧ ‎) 1470 1427 CANADIAN SYLLABICS KOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᑽ ‎) 147D CANADIAN SYLLABICS WEST-CREE KWOO # →ᑰᐧ→ + +# ᒉ ᘃ + (‎ ᒉ ‎) 1489 CANADIAN SYLLABICS CE +← (‎ ᘃ ‎) 1603 CANADIAN SYLLABICS CARRIER NO + +# ᒉ· ᒉᐧ ᒓ + (‎ ᒉ· ‎) 1489 00B7 CANADIAN SYLLABICS CE, MIDDLE DOT +← (‎ ᒉᐧ ‎) 1489 1427 CANADIAN SYLLABICS CE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒓ ‎) 1493 CANADIAN SYLLABICS WEST-CREE CWE # →ᒉᐧ→ + +# ᒋ· ᒋᐧ ᒕ + (‎ ᒋ· ‎) 148B 00B7 CANADIAN SYLLABICS CI, MIDDLE DOT +← (‎ ᒋᐧ ‎) 148B 1427 CANADIAN SYLLABICS CI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒕ ‎) 1495 CANADIAN SYLLABICS WEST-CREE CWI # →ᒋᐧ→ + +# ᒌ· ᒌᐧ ᒗ + (‎ ᒌ· ‎) 148C 00B7 CANADIAN SYLLABICS CII, MIDDLE DOT +← (‎ ᒌᐧ ‎) 148C 1427 CANADIAN SYLLABICS CII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒗ ‎) 1497 CANADIAN SYLLABICS WEST-CREE CWII # →ᒌᐧ→ + +# ᒎ· ᒎᐧ ᒛ + (‎ ᒎ· ‎) 148E 00B7 CANADIAN SYLLABICS COO, MIDDLE DOT +← (‎ ᒎᐧ ‎) 148E 1427 CANADIAN SYLLABICS COO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒛ ‎) 149B CANADIAN SYLLABICS WEST-CREE CWOO # →ᒎᐧ→ + +# ᒐ ᘂ + (‎ ᒐ ‎) 1490 CANADIAN SYLLABICS CA +← (‎ ᘂ ‎) 1602 CANADIAN SYLLABICS CARRIER NU + +# ᒐ· ᒐᐧ ᒝ + (‎ ᒐ· ‎) 1490 00B7 CANADIAN SYLLABICS CA, MIDDLE DOT +← (‎ ᒐᐧ ‎) 1490 1427 CANADIAN SYLLABICS CA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒝ ‎) 149D CANADIAN SYLLABICS WEST-CREE CWA # →ᒐᐧ→ + +# ᒑ· ᒑᐧ ᒟ + (‎ ᒑ· ‎) 1491 00B7 CANADIAN SYLLABICS CAA, MIDDLE DOT +← (‎ ᒑᐧ ‎) 1491 1427 CANADIAN SYLLABICS CAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒟ ‎) 149F CANADIAN SYLLABICS WEST-CREE CWAA # →ᒑᐧ→ + +# ᒣ· ᒣᐧ ᒭ + (‎ ᒣ· ‎) 14A3 00B7 CANADIAN SYLLABICS ME, MIDDLE DOT +← (‎ ᒣᐧ ‎) 14A3 1427 CANADIAN SYLLABICS ME, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒭ ‎) 14AD CANADIAN SYLLABICS WEST-CREE MWE # →ᒣᐧ→ + +# ᒦ· ᒦᐧ ᒱ + (‎ ᒦ· ‎) 14A6 00B7 CANADIAN SYLLABICS MII, MIDDLE DOT +← (‎ ᒦᐧ ‎) 14A6 1427 CANADIAN SYLLABICS MII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒱ ‎) 14B1 CANADIAN SYLLABICS WEST-CREE MWII # →ᒦᐧ→ + +# ᒧ· ᒧᐧ ᒳ + (‎ ᒧ· ‎) 14A7 00B7 CANADIAN SYLLABICS MO, MIDDLE DOT +← (‎ ᒧᐧ ‎) 14A7 1427 CANADIAN SYLLABICS MO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒳ ‎) 14B3 CANADIAN SYLLABICS WEST-CREE MWO # →ᒧᐧ→ + +# ᒨ· ᒨᐧ ᒵ + (‎ ᒨ· ‎) 14A8 00B7 CANADIAN SYLLABICS MOO, MIDDLE DOT +← (‎ ᒨᐧ ‎) 14A8 1427 CANADIAN SYLLABICS MOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒵ ‎) 14B5 CANADIAN SYLLABICS WEST-CREE MWOO # →ᒨᐧ→ + +# ᒫ· ᒫᐧ ᒹ + (‎ ᒫ· ‎) 14AB 00B7 CANADIAN SYLLABICS MAA, MIDDLE DOT +← (‎ ᒫᐧ ‎) 14AB 1427 CANADIAN SYLLABICS MAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᒹ ‎) 14B9 CANADIAN SYLLABICS WEST-CREE MWAA # →ᒫᐧ→ + +# ᓀ· ᓀᐧ ᓊ + (‎ ᓀ· ‎) 14C0 00B7 CANADIAN SYLLABICS NE, MIDDLE DOT +← (‎ ᓀᐧ ‎) 14C0 1427 CANADIAN SYLLABICS NE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓊ ‎) 14CA CANADIAN SYLLABICS WEST-CREE NWE # →ᓀᐧ→ + +# ᓂ· ᓂᐧ ᣇ + (‎ ᓂ· ‎) 14C2 00B7 CANADIAN SYLLABICS NI, MIDDLE DOT +← (‎ ᓂᐧ ‎) 14C2 1427 CANADIAN SYLLABICS NI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣇ ‎) 18C7 CANADIAN SYLLABICS OJIBWAY NWI # →ᓂᐧ→ + +# ᓃ· ᓃᐧ ᣉ + (‎ ᓃ· ‎) 14C3 00B7 CANADIAN SYLLABICS NII, MIDDLE DOT +← (‎ ᓃᐧ ‎) 14C3 1427 CANADIAN SYLLABICS NII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣉ ‎) 18C9 CANADIAN SYLLABICS OJIBWAY NWII # →ᓃᐧ→ + +# ᓄ· ᓄᐧ ᣋ + (‎ ᓄ· ‎) 14C4 00B7 CANADIAN SYLLABICS NO, MIDDLE DOT +← (‎ ᓄᐧ ‎) 14C4 1427 CANADIAN SYLLABICS NO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣋ ‎) 18CB CANADIAN SYLLABICS OJIBWAY NWO # →ᓄᐧ→ + +# ᓅ· ᓅᐧ ᣍ + (‎ ᓅ· ‎) 14C5 00B7 CANADIAN SYLLABICS NOO, MIDDLE DOT +← (‎ ᓅᐧ ‎) 14C5 1427 CANADIAN SYLLABICS NOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣍ ‎) 18CD CANADIAN SYLLABICS OJIBWAY NWOO # →ᓅᐧ→ + +# ᓇ· ᓇᐧ ᓌ + (‎ ᓇ· ‎) 14C7 00B7 CANADIAN SYLLABICS NA, MIDDLE DOT +← (‎ ᓇᐧ ‎) 14C7 1427 CANADIAN SYLLABICS NA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓌ ‎) 14CC CANADIAN SYLLABICS WEST-CREE NWA # →ᓇᐧ→ + +# ᓈ· ᓈᐧ ᓎ + (‎ ᓈ· ‎) 14C8 00B7 CANADIAN SYLLABICS NAA, MIDDLE DOT +← (‎ ᓈᐧ ‎) 14C8 1427 CANADIAN SYLLABICS NAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓎ ‎) 14CE CANADIAN SYLLABICS WEST-CREE NWAA # →ᓈᐧ→ + +# ᓓ ᘄ + (‎ ᓓ ‎) 14D3 CANADIAN SYLLABICS LE +← (‎ ᘄ ‎) 1604 CANADIAN SYLLABICS CARRIER NE + +# ᓓ· ᓓᐧ ᓝ + (‎ ᓓ· ‎) 14D3 00B7 CANADIAN SYLLABICS LE, MIDDLE DOT +← (‎ ᓓᐧ ‎) 14D3 1427 CANADIAN SYLLABICS LE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓝ ‎) 14DD CANADIAN SYLLABICS WEST-CREE LWE # →ᓓᐧ→ + +# ᓕ· ᓕᐧ ᓟ + (‎ ᓕ· ‎) 14D5 00B7 CANADIAN SYLLABICS LI, MIDDLE DOT +← (‎ ᓕᐧ ‎) 14D5 1427 CANADIAN SYLLABICS LI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓟ ‎) 14DF CANADIAN SYLLABICS WEST-CREE LWI # →ᓕᐧ→ + +# ᓖ· ᓖᐧ ᓡ + (‎ ᓖ· ‎) 14D6 00B7 CANADIAN SYLLABICS LII, MIDDLE DOT +← (‎ ᓖᐧ ‎) 14D6 1427 CANADIAN SYLLABICS LII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓡ ‎) 14E1 CANADIAN SYLLABICS WEST-CREE LWII # →ᓖᐧ→ + +# ᓗ· ᓗᐧ ᓣ + (‎ ᓗ· ‎) 14D7 00B7 CANADIAN SYLLABICS LO, MIDDLE DOT +← (‎ ᓗᐧ ‎) 14D7 1427 CANADIAN SYLLABICS LO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓣ ‎) 14E3 CANADIAN SYLLABICS WEST-CREE LWO # →ᓗᐧ→ + +# ᓘ· ᓘᐧ ᓥ + (‎ ᓘ· ‎) 14D8 00B7 CANADIAN SYLLABICS LOO, MIDDLE DOT +← (‎ ᓘᐧ ‎) 14D8 1427 CANADIAN SYLLABICS LOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓥ ‎) 14E5 CANADIAN SYLLABICS WEST-CREE LWOO # →ᓘᐧ→ + +# ᓚ ᘇ + (‎ ᓚ ‎) 14DA CANADIAN SYLLABICS LA +← (‎ ᘇ ‎) 1607 CANADIAN SYLLABICS CARRIER NA + +# ᓚ· ᓚᐧ ᓧ + (‎ ᓚ· ‎) 14DA 00B7 CANADIAN SYLLABICS LA, MIDDLE DOT +← (‎ ᓚᐧ ‎) 14DA 1427 CANADIAN SYLLABICS LA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓧ ‎) 14E7 CANADIAN SYLLABICS WEST-CREE LWA # →ᓚᐧ→ + +# ᓛ· ᓛᐧ ᓩ + (‎ ᓛ· ‎) 14DB 00B7 CANADIAN SYLLABICS LAA, MIDDLE DOT +← (‎ ᓛᐧ ‎) 14DB 1427 CANADIAN SYLLABICS LAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓩ ‎) 14E9 CANADIAN SYLLABICS WEST-CREE LWAA # →ᓛᐧ→ + +# ᓭ· ᓭᐧ ᓷ + (‎ ᓭ· ‎) 14ED 00B7 CANADIAN SYLLABICS SE, MIDDLE DOT +← (‎ ᓭᐧ ‎) 14ED 1427 CANADIAN SYLLABICS SE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓷ ‎) 14F7 CANADIAN SYLLABICS WEST-CREE SWE # →ᓭᐧ→ + +# ᓯ· ᓯᐧ ᓹ + (‎ ᓯ· ‎) 14EF 00B7 CANADIAN SYLLABICS SI, MIDDLE DOT +← (‎ ᓯᐧ ‎) 14EF 1427 CANADIAN SYLLABICS SI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓹ ‎) 14F9 CANADIAN SYLLABICS WEST-CREE SWI # →ᓯᐧ→ + +# ᓰ· ᓰᐧ ᓻ + (‎ ᓰ· ‎) 14F0 00B7 CANADIAN SYLLABICS SII, MIDDLE DOT +← (‎ ᓰᐧ ‎) 14F0 1427 CANADIAN SYLLABICS SII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓻ ‎) 14FB CANADIAN SYLLABICS WEST-CREE SWII # →ᓰᐧ→ + +# ᓱ· ᓱᐧ ᓽ + (‎ ᓱ· ‎) 14F1 00B7 CANADIAN SYLLABICS SO, MIDDLE DOT +← (‎ ᓱᐧ ‎) 14F1 1427 CANADIAN SYLLABICS SO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓽ ‎) 14FD CANADIAN SYLLABICS WEST-CREE SWO # →ᓱᐧ→ + +# ᓲ· ᓲᐧ ᓿ + (‎ ᓲ· ‎) 14F2 00B7 CANADIAN SYLLABICS SOO, MIDDLE DOT +← (‎ ᓲᐧ ‎) 14F2 1427 CANADIAN SYLLABICS SOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᓿ ‎) 14FF CANADIAN SYLLABICS WEST-CREE SWOO # →ᓲᐧ→ + +# ᓴ· ᓴᐧ ᔁ + (‎ ᓴ· ‎) 14F4 00B7 CANADIAN SYLLABICS SA, MIDDLE DOT +← (‎ ᓴᐧ ‎) 14F4 1427 CANADIAN SYLLABICS SA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔁ ‎) 1501 CANADIAN SYLLABICS WEST-CREE SWA # →ᓴᐧ→ + +# ᓵ· ᓵᐧ ᔃ + (‎ ᓵ· ‎) 14F5 00B7 CANADIAN SYLLABICS SAA, MIDDLE DOT +← (‎ ᓵᐧ ‎) 14F5 1427 CANADIAN SYLLABICS SAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔃ ‎) 1503 CANADIAN SYLLABICS WEST-CREE SWAA # →ᓵᐧ→ + +# ᔋ< ᔋᐸ ᔌ + (‎ ᔋ< ‎) 150B 003C CANADIAN SYLLABICS NASKAPI S-W, LESS-THAN SIGN +← (‎ ᔋᐸ ‎) 150B 1438 CANADIAN SYLLABICS NASKAPI S-W, CANADIAN SYLLABICS PA +← (‎ ᔌ ‎) 150C CANADIAN SYLLABICS NASKAPI SPWA # →ᔋᐸ→ + +# ᔋb ᔋᑲ ᔎ + (‎ ᔋb ‎) 150B 0062 CANADIAN SYLLABICS NASKAPI S-W, LATIN SMALL LETTER B +← (‎ ᔋᑲ ‎) 150B 1472 CANADIAN SYLLABICS NASKAPI S-W, CANADIAN SYLLABICS KA +← (‎ ᔎ ‎) 150E CANADIAN SYLLABICS NASKAPI SKWA # →ᔋᑲ→ + +# ᔋᑕ ᔍ + (‎ ᔋᑕ ‎) 150B 1455 CANADIAN SYLLABICS NASKAPI S-W, CANADIAN SYLLABICS TA +← (‎ ᔍ ‎) 150D CANADIAN SYLLABICS NASKAPI STWA + +# ᔋᒐ ᔏ + (‎ ᔋᒐ ‎) 150B 1490 CANADIAN SYLLABICS NASKAPI S-W, CANADIAN SYLLABICS CA +← (‎ ᔏ ‎) 150F CANADIAN SYLLABICS NASKAPI SCWA + +# ᔐ· ᔐᐧ ᔘ + (‎ ᔐ· ‎) 1510 00B7 CANADIAN SYLLABICS SHE, MIDDLE DOT +← (‎ ᔐᐧ ‎) 1510 1427 CANADIAN SYLLABICS SHE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔘ ‎) 1518 CANADIAN SYLLABICS WEST-CREE SHWE # →ᔐᐧ→ + +# ᔑ· ᔑᐧ ᔚ + (‎ ᔑ· ‎) 1511 00B7 CANADIAN SYLLABICS SHI, MIDDLE DOT +← (‎ ᔑᐧ ‎) 1511 1427 CANADIAN SYLLABICS SHI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔚ ‎) 151A CANADIAN SYLLABICS WEST-CREE SHWI # →ᔑᐧ→ + +# ᔒ· ᔒᐧ ᔜ + (‎ ᔒ· ‎) 1512 00B7 CANADIAN SYLLABICS SHII, MIDDLE DOT +← (‎ ᔒᐧ ‎) 1512 1427 CANADIAN SYLLABICS SHII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔜ ‎) 151C CANADIAN SYLLABICS WEST-CREE SHWII # →ᔒᐧ→ + +# ᔓ· ᔓᐧ ᔞ + (‎ ᔓ· ‎) 1513 00B7 CANADIAN SYLLABICS SHO, MIDDLE DOT +← (‎ ᔓᐧ ‎) 1513 1427 CANADIAN SYLLABICS SHO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔞ ‎) 151E CANADIAN SYLLABICS WEST-CREE SHWO # →ᔓᐧ→ + +# ᔔ· ᔔᐧ ᔠ + (‎ ᔔ· ‎) 1514 00B7 CANADIAN SYLLABICS SHOO, MIDDLE DOT +← (‎ ᔔᐧ ‎) 1514 1427 CANADIAN SYLLABICS SHOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔠ ‎) 1520 CANADIAN SYLLABICS WEST-CREE SHWOO # →ᔔᐧ→ + +# ᔕ· ᔕᐧ ᔢ + (‎ ᔕ· ‎) 1515 00B7 CANADIAN SYLLABICS SHA, MIDDLE DOT +← (‎ ᔕᐧ ‎) 1515 1427 CANADIAN SYLLABICS SHA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔢ ‎) 1522 CANADIAN SYLLABICS WEST-CREE SHWA # →ᔕᐧ→ + +# ᔖ· ᔖᐧ ᔤ + (‎ ᔖ· ‎) 1516 00B7 CANADIAN SYLLABICS SHAA, MIDDLE DOT +← (‎ ᔖᐧ ‎) 1516 1427 CANADIAN SYLLABICS SHAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔤ ‎) 1524 CANADIAN SYLLABICS WEST-CREE SHWAA # →ᔖᐧ→ + +# ᔨ· ᔨᐧ ᔲ + (‎ ᔨ· ‎) 1528 00B7 CANADIAN SYLLABICS YI, MIDDLE DOT +← (‎ ᔨᐧ ‎) 1528 1427 CANADIAN SYLLABICS YI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔲ ‎) 1532 CANADIAN SYLLABICS WEST-CREE YWI # →ᔨᐧ→ + +# ᔩ· ᔩᐧ ᔴ + (‎ ᔩ· ‎) 1529 00B7 CANADIAN SYLLABICS YII, MIDDLE DOT +← (‎ ᔩᐧ ‎) 1529 1427 CANADIAN SYLLABICS YII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔴ ‎) 1534 CANADIAN SYLLABICS WEST-CREE YWII # →ᔩᐧ→ + +# ᔪ· ᔪᐧ ᔶ + (‎ ᔪ· ‎) 152A 00B7 CANADIAN SYLLABICS YO, MIDDLE DOT +← (‎ ᔪᐧ ‎) 152A 1427 CANADIAN SYLLABICS YO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔶ ‎) 1536 CANADIAN SYLLABICS WEST-CREE YWO # →ᔪᐧ→ + +# ᔫ· ᔫᐧ ᔸ + (‎ ᔫ· ‎) 152B 00B7 CANADIAN SYLLABICS YOO, MIDDLE DOT +← (‎ ᔫᐧ ‎) 152B 1427 CANADIAN SYLLABICS YOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔸ ‎) 1538 CANADIAN SYLLABICS WEST-CREE YWOO # →ᔫᐧ→ + +# ᔭ· ᔭᐧ ᔺ + (‎ ᔭ· ‎) 152D 00B7 CANADIAN SYLLABICS YA, MIDDLE DOT +← (‎ ᔭᐧ ‎) 152D 1427 CANADIAN SYLLABICS YA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔺ ‎) 153A CANADIAN SYLLABICS WEST-CREE YWA # →ᔭᐧ→ + +# ᔮ· ᔮᐧ ᔼ + (‎ ᔮ· ‎) 152E 00B7 CANADIAN SYLLABICS YAA, MIDDLE DOT +← (‎ ᔮᐧ ‎) 152E 1427 CANADIAN SYLLABICS YAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᔼ ‎) 153C CANADIAN SYLLABICS WEST-CREE YWAA # →ᔮᐧ→ + +# ᕃ ᘢ + (‎ ᕃ ‎) 1543 CANADIAN SYLLABICS R-CREE RE +← (‎ ᘢ ‎) 1622 CANADIAN SYLLABICS CARRIER LU + +# ᕃ· ᕃᐧ ᣠ + (‎ ᕃ· ‎) 1543 00B7 CANADIAN SYLLABICS R-CREE RE, MIDDLE DOT +← (‎ ᕃᐧ ‎) 1543 1427 CANADIAN SYLLABICS R-CREE RE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣠ ‎) 18E0 CANADIAN SYLLABICS R-CREE RWE # →ᕃᐧ→ + +# ᕆ ᘣ + (‎ ᕆ ‎) 1546 CANADIAN SYLLABICS RI +← (‎ ᘣ ‎) 1623 CANADIAN SYLLABICS CARRIER LO + +# ᕊ ᘤ + (‎ ᕊ ‎) 154A CANADIAN SYLLABICS WEST-CREE LO +← (‎ ᘤ ‎) 1624 CANADIAN SYLLABICS CARRIER LE + +# ᕌ· ᕌᐧ ᕏ + (‎ ᕌ· ‎) 154C 00B7 CANADIAN SYLLABICS RAA, MIDDLE DOT +← (‎ ᕌᐧ ‎) 154C 1427 CANADIAN SYLLABICS RAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᕏ ‎) 154F CANADIAN SYLLABICS WEST-CREE RWAA # →ᕌᐧ→ + +# ᕐP ᕐᏢ ᕐᑭ ᕿ + (‎ ᕐP ‎) 1550 0050 CANADIAN SYLLABICS R, LATIN CAPITAL LETTER P +← (‎ ᕐᏢ ‎) 1550 13E2 CANADIAN SYLLABICS R, CHEROKEE LETTER TLV # →ᕐᑭ→ +← (‎ ᕐᑭ ‎) 1550 146D CANADIAN SYLLABICS R, CANADIAN SYLLABICS KI +← (‎ ᕿ ‎) 157F CANADIAN SYLLABICS QI # →ᕐᑭ→ + +# ᕐb ᕐᑲ ᖃ + (‎ ᕐb ‎) 1550 0062 CANADIAN SYLLABICS R, LATIN SMALL LETTER B +← (‎ ᕐᑲ ‎) 1550 1472 CANADIAN SYLLABICS R, CANADIAN SYLLABICS KA +← (‎ ᖃ ‎) 1583 CANADIAN SYLLABICS QA # →ᕐᑲ→ + +# ᕐḃ ᕐᑳ ᖄ + (‎ ᕐḃ ‎) 1550 0062 0307 CANADIAN SYLLABICS R, LATIN SMALL LETTER B, COMBINING DOT ABOVE +← (‎ ᕐᑳ ‎) 1550 1473 CANADIAN SYLLABICS R, CANADIAN SYLLABICS KAA +← (‎ ᖄ ‎) 1584 CANADIAN SYLLABICS QAA # →ᕐᑳ→ + +# ᕐd ᕐᑯ ᖁ + (‎ ᕐd ‎) 1550 0064 CANADIAN SYLLABICS R, LATIN SMALL LETTER D +← (‎ ᕐᑯ ‎) 1550 146F CANADIAN SYLLABICS R, CANADIAN SYLLABICS KO +← (‎ ᖁ ‎) 1581 CANADIAN SYLLABICS QO # →ᕐᑯ→ + +# ᕐᑫ ᙯ + (‎ ᕐᑫ ‎) 1550 146B CANADIAN SYLLABICS R, CANADIAN SYLLABICS KE +← (‎ ᙯ ‎) 166F CANADIAN SYLLABICS QAI + +# ᕐᑬ ᕾ + (‎ ᕐᑬ ‎) 1550 146C CANADIAN SYLLABICS R, CANADIAN SYLLABICS KAAI +← (‎ ᕾ ‎) 157E CANADIAN SYLLABICS QAAI + +# ᕐᑮ ᖀ + (‎ ᕐᑮ ‎) 1550 146E CANADIAN SYLLABICS R, CANADIAN SYLLABICS KII +← (‎ ᖀ ‎) 1580 CANADIAN SYLLABICS QII + +# ᕐᑰ ᖂ + (‎ ᕐᑰ ‎) 1550 1470 CANADIAN SYLLABICS R, CANADIAN SYLLABICS KOO +← (‎ ᖂ ‎) 1582 CANADIAN SYLLABICS QOO + +# ᕐᒃ ᖅ + (‎ ᕐᒃ ‎) 1550 1483 CANADIAN SYLLABICS R, CANADIAN SYLLABICS K +← (‎ ᖅ ‎) 1585 CANADIAN SYLLABICS Q + +# ᕚ· ᕚᐧ ᕜ + (‎ ᕚ· ‎) 155A 00B7 CANADIAN SYLLABICS FAA, MIDDLE DOT +← (‎ ᕚᐧ ‎) 155A 1427 CANADIAN SYLLABICS FAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᕜ ‎) 155C CANADIAN SYLLABICS WEST-CREE FWAA # →ᕚᐧ→ + +# ᕞ· ᕞᐧ ᣣ + (‎ ᕞ· ‎) 155E 00B7 CANADIAN SYLLABICS THE, MIDDLE DOT +← (‎ ᕞᐧ ‎) 155E 1427 CANADIAN SYLLABICS THE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣣ ‎) 18E3 CANADIAN SYLLABICS THWE # →ᕞᐧ→ + +# ᕦ· ᕦᐧ ᣤ + (‎ ᕦ· ‎) 1566 00B7 CANADIAN SYLLABICS THA, MIDDLE DOT +← (‎ ᕦᐧ ‎) 1566 1427 CANADIAN SYLLABICS THA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣤ ‎) 18E4 CANADIAN SYLLABICS THWA # →ᕦᐧ→ + +# ᕧ· ᕧᐧ ᕩ + (‎ ᕧ· ‎) 1567 00B7 CANADIAN SYLLABICS THAA, MIDDLE DOT +← (‎ ᕧᐧ ‎) 1567 1427 CANADIAN SYLLABICS THAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᕩ ‎) 1569 CANADIAN SYLLABICS WEST-CREE THWAA # →ᕧᐧ→ + +# ᕫ· ᕫᐧ ᣥ + (‎ ᕫ· ‎) 156B 00B7 CANADIAN SYLLABICS TTHE, MIDDLE DOT +← (‎ ᕫᐧ ‎) 156B 1427 CANADIAN SYLLABICS TTHE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣥ ‎) 18E5 CANADIAN SYLLABICS TTHWE # →ᕫᐧ→ + +# ᖆ· ᖆᐧ ᣨ + (‎ ᖆ· ‎) 1586 00B7 CANADIAN SYLLABICS TLHE, MIDDLE DOT +← (‎ ᖆᐧ ‎) 1586 1427 CANADIAN SYLLABICS TLHE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣨ ‎) 18E8 CANADIAN SYLLABICS TLHWE # →ᖆᐧ→ + +# ᖕᒊ ᖎ + (‎ ᖎ ‎) 158E CANADIAN SYLLABICS NGAAI +← (‎ ᖕᒊ ‎) 1595 148A CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CAAI + +# ᖕᒋ ᖏ + (‎ ᖏ ‎) 158F CANADIAN SYLLABICS NGI +← (‎ ᖕᒋ ‎) 1595 148B CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CI + +# ᖕᒌ ᖐ + (‎ ᖐ ‎) 1590 CANADIAN SYLLABICS NGII +← (‎ ᖕᒌ ‎) 1595 148C CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CII + +# ᖕJ ᖕᎫ ᖕᒍ ᖑ + (‎ ᖑ ‎) 1591 CANADIAN SYLLABICS NGO +← (‎ ᖕJ ‎) 1595 004A CANADIAN SYLLABICS NG, LATIN CAPITAL LETTER J # →ᖕᒍ→ +← (‎ ᖕᎫ ‎) 1595 13AB CANADIAN SYLLABICS NG, CHEROKEE LETTER GU # →ᖕᒍ→ +← (‎ ᖕᒍ ‎) 1595 148D CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CO + +# ᖕᒎ ᖒ + (‎ ᖒ ‎) 1592 CANADIAN SYLLABICS NGOO +← (‎ ᖕᒎ ‎) 1595 148E CANADIAN SYLLABICS NG, CANADIAN SYLLABICS COO + +# ᖕᒐ ᖓ + (‎ ᖓ ‎) 1593 CANADIAN SYLLABICS NGA +← (‎ ᖕᒐ ‎) 1595 1490 CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CA + +# ᖕᒑ ᖔ + (‎ ᖔ ‎) 1594 CANADIAN SYLLABICS NGAA +← (‎ ᖕᒑ ‎) 1595 1491 CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CAA + +# ᖕᒉ ᙰ + (‎ ᖕᒉ ‎) 1595 1489 CANADIAN SYLLABICS NG, CANADIAN SYLLABICS CE +← (‎ ᙰ ‎) 1670 CANADIAN SYLLABICS NGAI + +# ᖖJ ᖖᎫ ᖖᒍ ᙳ + (‎ ᖖJ ‎) 1596 004A CANADIAN SYLLABICS NNG, LATIN CAPITAL LETTER J +← (‎ ᖖᎫ ‎) 1596 13AB CANADIAN SYLLABICS NNG, CHEROKEE LETTER GU # →ᖖᒍ→ +← (‎ ᖖᒍ ‎) 1596 148D CANADIAN SYLLABICS NNG, CANADIAN SYLLABICS CO +← (‎ ᙳ ‎) 1673 CANADIAN SYLLABICS NNGO # →ᖖᒍ→ + +# ᖖᒋ ᙱ + (‎ ᖖᒋ ‎) 1596 148B CANADIAN SYLLABICS NNG, CANADIAN SYLLABICS CI +← (‎ ᙱ ‎) 1671 CANADIAN SYLLABICS NNGI + +# ᖖᒌ ᙲ + (‎ ᖖᒌ ‎) 1596 148C CANADIAN SYLLABICS NNG, CANADIAN SYLLABICS CII +← (‎ ᙲ ‎) 1672 CANADIAN SYLLABICS NNGII + +# ᖖᒎ ᙴ + (‎ ᖖᒎ ‎) 1596 148E CANADIAN SYLLABICS NNG, CANADIAN SYLLABICS COO +← (‎ ᙴ ‎) 1674 CANADIAN SYLLABICS NNGOO + +# ᖖᒐ ᙵ + (‎ ᖖᒐ ‎) 1596 1490 CANADIAN SYLLABICS NNG, CANADIAN SYLLABICS CA +← (‎ ᙵ ‎) 1675 CANADIAN SYLLABICS NNGA + +# ᖖᒑ ᙶ + (‎ ᖖᒑ ‎) 1596 1491 CANADIAN SYLLABICS NNG, CANADIAN SYLLABICS CAA +← (‎ ᙶ ‎) 1676 CANADIAN SYLLABICS NNGAA + +# ᖗ· ᖗᐧ ᣪ + (‎ ᖗ· ‎) 1597 00B7 CANADIAN SYLLABICS SAYISI SHE, MIDDLE DOT +← (‎ ᖗᐧ ‎) 1597 1427 CANADIAN SYLLABICS SAYISI SHE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣪ ‎) 18EA CANADIAN SYLLABICS SAYISI SHWE # →ᖗᐧ→ + +# ᖧ· ᖧᐧ ᙷ + (‎ ᖧ· ‎) 15A7 00B7 CANADIAN SYLLABICS TH-CREE THE, MIDDLE DOT +← (‎ ᖧᐧ ‎) 15A7 1427 CANADIAN SYLLABICS TH-CREE THE, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙷ ‎) 1677 CANADIAN SYLLABICS WOODS-CREE THWEE # →ᖧᐧ→ + +# ᖨ· ᖨᐧ ᙸ + (‎ ᖨ· ‎) 15A8 00B7 CANADIAN SYLLABICS TH-CREE THI, MIDDLE DOT +← (‎ ᖨᐧ ‎) 15A8 1427 CANADIAN SYLLABICS TH-CREE THI, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙸ ‎) 1678 CANADIAN SYLLABICS WOODS-CREE THWI # →ᖨᐧ→ + +# ᖩ· ᖩᐧ ᙹ + (‎ ᖩ· ‎) 15A9 00B7 CANADIAN SYLLABICS TH-CREE THII, MIDDLE DOT +← (‎ ᖩᐧ ‎) 15A9 1427 CANADIAN SYLLABICS TH-CREE THII, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙹ ‎) 1679 CANADIAN SYLLABICS WOODS-CREE THWII # →ᖩᐧ→ + +# ᖪ· ᖪᐧ ᙺ + (‎ ᖪ· ‎) 15AA 00B7 CANADIAN SYLLABICS TH-CREE THO, MIDDLE DOT +← (‎ ᖪᐧ ‎) 15AA 1427 CANADIAN SYLLABICS TH-CREE THO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙺ ‎) 167A CANADIAN SYLLABICS WOODS-CREE THWO # →ᖪᐧ→ + +# ᖫ· ᖫᐧ ᙻ + (‎ ᖫ· ‎) 15AB 00B7 CANADIAN SYLLABICS TH-CREE THOO, MIDDLE DOT +← (‎ ᖫᐧ ‎) 15AB 1427 CANADIAN SYLLABICS TH-CREE THOO, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙻ ‎) 167B CANADIAN SYLLABICS WOODS-CREE THWOO # →ᖫᐧ→ + +# ᖬ· ᖬᐧ ᙼ + (‎ ᖬ· ‎) 15AC 00B7 CANADIAN SYLLABICS TH-CREE THA, MIDDLE DOT +← (‎ ᖬᐧ ‎) 15AC 1427 CANADIAN SYLLABICS TH-CREE THA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙼ ‎) 167C CANADIAN SYLLABICS WOODS-CREE THWA # →ᖬᐧ→ + +# ᖭ· ᖭᐧ ᙽ + (‎ ᖭ· ‎) 15AD 00B7 CANADIAN SYLLABICS TH-CREE THAA, MIDDLE DOT +← (‎ ᖭᐧ ‎) 15AD 1427 CANADIAN SYLLABICS TH-CREE THAA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᙽ ‎) 167D CANADIAN SYLLABICS WOODS-CREE THWAA # →ᖭᐧ→ + +# Ⅎ ᖵ ꓞ + (‎ ᖵ ‎) 15B5 CANADIAN SYLLABICS BLACKFOOT WI +← (‎ Ⅎ ‎) 2132 TURNED CAPITAL F +← (‎ ꓞ ‎) A4DE LISU LETTER TSHA # →Ⅎ→ + +# ꟻ ᖷ 𝈰 + (‎ ᖷ ‎) 15B7 CANADIAN SYLLABICS BLACKFOOT WA +← (‎ ꟻ ‎) A7FB LATIN EPIGRAPHIC LETTER REVERSED F +← (‎ 𝈰 ‎) 1D230 GREEK INSTRUMENTAL NOTATION SYMBOL-30 # →ꟻ→ + +# Ɐ ᗄ ꓯ ∀ 𝈗 + (‎ ᗄ ‎) 15C4 CANADIAN SYLLABICS CARRIER GHU +← (‎ Ɐ ‎) 2C6F LATIN CAPITAL LETTER TURNED A # →∀→ +← (‎ ꓯ ‎) A4EF LISU LETTER AE # →∀→ +← (‎ ∀ ‎) 2200 FOR ALL +← (‎ 𝈗 ‎) 1D217 GREEK VOCAL NOTATION SYMBOL-24 # →Ɐ→→∀→ + +# ᗒ ⪫ + (‎ ᗒ ‎) 15D2 CANADIAN SYLLABICS CARRIER WE +← (‎ ⪫ ‎) 2AAB LARGER THAN + +# ᗕ ⪪ + (‎ ᗕ ‎) 15D5 CANADIAN SYLLABICS CARRIER WA +← (‎ ⪪ ‎) 2AAA SMALLER THAN + +# ᗡ ꓷ + (‎ ᗡ ‎) 15E1 CANADIAN SYLLABICS CARRIER THA +← (‎ ꓷ ‎) A4F7 LISU LETTER OE + +# ᗴ· ᗴᐧ ᣰ + (‎ ᗴ· ‎) 15F4 00B7 CANADIAN SYLLABICS CARRIER GA, MIDDLE DOT +← (‎ ᗴᐧ ‎) 15F4 1427 CANADIAN SYLLABICS CARRIER GA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣰ ‎) 18F0 CANADIAN SYLLABICS CARRIER GWA # →ᗴᐧ→ + +# ᘛ· ᘛᐧ ᣲ + (‎ ᘛ· ‎) 161B 00B7 CANADIAN SYLLABICS CARRIER JA, MIDDLE DOT +← (‎ ᘛᐧ ‎) 161B 1427 CANADIAN SYLLABICS CARRIER JA, CANADIAN SYLLABICS FINAL MIDDLE DOT +← (‎ ᣲ ‎) 18F2 CANADIAN SYLLABICS CARRIER JWA # →ᘛᐧ→ + +# ᙆ ᶻ + (‎ ᙆ ‎) 1646 CANADIAN SYLLABICS CARRIER Z +← (‎ ᶻ ‎) 1DBB MODIFIER LETTER SMALL Z + +# ᙠ ꓭ + (‎ ᙠ ‎) 1660 CANADIAN SYLLABICS CARRIER TSA +← (‎ ꓭ ‎) A4ED LISU LETTER GHA + +# ᚹ ꚰ + (‎ ᚹ ‎) 16B9 RUNIC LETTER WUNJO WYNN W +← (‎ ꚰ ‎) A6B0 BAMUM LETTER TAA + +# ᚼ ᛡ + (‎ ᚼ ‎) 16BC RUNIC LETTER LONG-BRANCH-HAGALL H +← (‎ ᛡ ‎) 16E1 RUNIC LETTER IOR + +# ᚽ ᛂ ⍿ + (‎ ᚽ ‎) 16BD RUNIC LETTER SHORT-TWIG-HAGALL H +← (‎ ᛂ ‎) 16C2 RUNIC LETTER E +← (‎ ⍿ ‎) 237F VERTICAL LINE WITH MIDDLE DOT # →ᛂ→ + +# ᛋ 𝈿 + (‎ ᛋ ‎) 16CB RUNIC LETTER SIGEL LONG-BRANCH-SOL S +← (‎ 𝈿 ‎) 1D23F GREEK INSTRUMENTAL NOTATION SYMBOL-52 + +# ᛏ ↑ + (‎ ᛏ ‎) 16CF RUNIC LETTER TIWAZ TIR TYR T +← (‎ ↑ ‎) 2191 UPWARDS ARROW + +# ᛐ ↿ + (‎ ᛐ ‎) 16D0 RUNIC LETTER SHORT-TWIG-TYR T +← (‎ ↿ ‎) 21BF UPWARDS HARPOON WITH BARB LEFTWARDS + +# ᛐᛚ ↿↾ ⥣ + (‎ ᛐᛚ ‎) 16D0 16DA RUNIC LETTER SHORT-TWIG-TYR T, RUNIC LETTER LAUKAZ LAGU LOGR L +← (‎ ↿↾ ‎) 21BF 21BE UPWARDS HARPOON WITH BARB LEFTWARDS, UPWARDS HARPOON WITH BARB RIGHTWARDS +← (‎ ⥣ ‎) 2963 UPWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT # →↿↾→ + +# ᛐ⇂ ↿⇂ ⥮ + (‎ ᛐ⇂ ‎) 16D0 21C2 RUNIC LETTER SHORT-TWIG-TYR T, DOWNWARDS HARPOON WITH BARB RIGHTWARDS +← (‎ ↿⇂ ‎) 21BF 21C2 UPWARDS HARPOON WITH BARB LEFTWARDS, DOWNWARDS HARPOON WITH BARB RIGHTWARDS +← (‎ ⥮ ‎) 296E UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT # →↿⇂→ + +# ᛚ ↾ ⨡ + (‎ ᛚ ‎) 16DA RUNIC LETTER LAUKAZ LAGU LOGR L +← (‎ ↾ ‎) 21BE UPWARDS HARPOON WITH BARB RIGHTWARDS +← (‎ ⨡ ‎) 2A21 Z NOTATION SCHEMA PROJECTION # →↾→ + +# ᛜ 𐊔 ⋄ ◇ ◊ ♢ 🝔 𑢷 + (‎ ᛜ ‎) 16DC RUNIC LETTER INGWAZ +← (‎ 𐊔 ‎) 10294 LYCIAN LETTER KK # →◇→ +← (‎ ⋄ ‎) 22C4 DIAMOND OPERATOR # →◇→ +← (‎ ◇ ‎) 25C7 WHITE DIAMOND +← (‎ ◊ ‎) 25CA LOZENGE # →⋄→→◇→ +← (‎ ♢ ‎) 2662 WHITE DIAMOND SUIT # →◊→→⋄→→◇→ +← (‎ 🝔 ‎) 1F754 ALCHEMICAL SYMBOL FOR SOAP # →◇→ +← (‎ 𑢷 ‎) 118B7 WARANG CITI CAPITAL LETTER BU # →◇→ + +# ᛜ̲ ◇̲ ⍚ + (‎ ᛜ̲ ‎) 16DC 0332 RUNIC LETTER INGWAZ, COMBINING LOW LINE +← (‎ ◇̲ ‎) 25C7 0332 WHITE DIAMOND, COMBINING LOW LINE +← (‎ ⍚ ‎) 235A APL FUNCTIONAL SYMBOL DIAMOND UNDERBAR # →◇̲→ + +# ᛞ ⋈ ⨝ + (‎ ᛞ ‎) 16DE RUNIC LETTER DAGAZ DAEG D +← (‎ ⋈ ‎) 22C8 BOWTIE +← (‎ ⨝ ‎) 2A1D JOIN # →⋈→ + +# ᛦ 𐓐 + (‎ ᛦ ‎) 16E6 RUNIC LETTER LONG-BRANCH-YR +← (‎ 𐓐 ‎) 104D0 OSAGE CAPITAL LETTER KHA + +# ᛨ ↕ + (‎ ᛨ ‎) 16E8 RUNIC LETTER ICELANDIC-YR +← (‎ ↕ ‎) 2195 UP DOWN ARROW + +# ᛯ ⵣ + (‎ ᛯ ‎) 16EF RUNIC TVIMADUR SYMBOL +← (‎ ⵣ ‎) 2D63 TIFINAGH LETTER YAZ + +# អ ឣ + (‎ អ ‎) 17A2 KHMER LETTER QA +← (‎ ឣ ‎) 17A3 KHMER INDEPENDENT VOWEL QAQ + +# ᠵ ᡕ + (‎ ᠵ ‎) 1835 MONGOLIAN LETTER JA +← (‎ ᡕ ‎) 1855 MONGOLIAN LETTER TODO YA + +# ᡜ ᢖ + (‎ ᡜ ‎) 185C MONGOLIAN LETTER TODO DZA +← (‎ ᢖ ‎) 1896 MONGOLIAN LETTER ALI GALI ZA + +# ᣔ ᶺ + (‎ ᣔ ‎) 18D4 CANADIAN SYLLABICS OJIBWAY P +← (‎ ᶺ ‎) 1DBA MODIFIER LETTER SMALL TURNED V + +# ᣖ ᴾ + (‎ ᣖ ‎) 18D6 CANADIAN SYLLABICS OJIBWAY K +← (‎ ᴾ ‎) 1D3E MODIFIER LETTER CAPITAL P + +# ᣟᐞ ᣜ + (‎ ᣜ ‎) 18DC CANADIAN SYLLABICS EASTERN W +← (‎ ᣟᐞ ‎) 18DF 141E CANADIAN SYLLABICS FINAL RAISED DOT, CANADIAN SYLLABICS GLOTTAL STOP + +# ᦞ ᧐ + (‎ ᦞ ‎) 199E NEW TAI LUE LETTER LOW VA +← (‎ ᧐ ‎) 19D0 NEW TAI LUE DIGIT ZERO + +# ᦱ ᧑ + (‎ ᦱ ‎) 19B1 NEW TAI LUE VOWEL SIGN AA +← (‎ ᧑ ‎) 19D1 NEW TAI LUE DIGIT ONE + +# ᩅ ᪀ ᪐ + (‎ ᩅ ‎) 1A45 TAI THAM LETTER WA +← (‎ ᪀ ‎) 1A80 TAI THAM HORA DIGIT ZERO +← (‎ ᪐ ‎) 1A90 TAI THAM THAM DIGIT ZERO + +# ᪨᪨ ᪩ + (‎ ᪨᪨ ‎) 1AA8 1AA8 TAI THAM SIGN KAAN, TAI THAM SIGN KAAN +← (‎ ᪩ ‎) 1AA9 TAI THAM SIGN KAANKUU + +# ᪪᪨ ᪫ + (‎ ᪪᪨ ‎) 1AAA 1AA8 TAI THAM SIGN SATKAAN, TAI THAM SIGN KAAN +← (‎ ᪫ ‎) 1AAB TAI THAM SIGN SATKAANKUU + +# ᬍ ᭒ + (‎ ᬍ ‎) 1B0D BALINESE LETTER LA LENGA +← (‎ ᭒ ‎) 1B52 BALINESE DIGIT TWO + +# ᬑ ᭓ + (‎ ᬑ ‎) 1B11 BALINESE LETTER OKARA +← (‎ ᭓ ‎) 1B53 BALINESE DIGIT THREE + +# ᬨ ᭘ + (‎ ᬨ ‎) 1B28 BALINESE LETTER PA KAPAL +← (‎ ᭘ ‎) 1B58 BALINESE DIGIT EIGHT + +# ᭐ ᭜ + (‎ ᭐ ‎) 1B50 BALINESE DIGIT ZERO +← (‎ ᭜ ‎) 1B5C BALINESE WINDU + +# ᭞᭞ ᭟ + (‎ ᭞᭞ ‎) 1B5E 1B5E BALINESE CARIK SIKI, BALINESE CARIK SIKI +← (‎ ᭟ ‎) 1B5F BALINESE CARIK PAREREN + +# ᰻᰻ ᰼ + (‎ ᰻᰻ ‎) 1C3B 1C3B LEPCHA PUNCTUATION TA-ROL, LEPCHA PUNCTUATION TA-ROL +← (‎ ᰼ ‎) 1C3C LEPCHA PUNCTUATION NYET THYOOM TA-ROL + +# ᱾᱾ ᱿ + (‎ ᱾᱾ ‎) 1C7E 1C7E OL CHIKI PUNCTUATION MUCAAD, OL CHIKI PUNCTUATION MUCAAD +← (‎ ᱿ ‎) 1C7F OL CHIKI PUNCTUATION DOUBLE MUCAAD + +# ᴀ ꭺ + (‎ ᴀ ‎) 1D00 LATIN LETTER SMALL CAPITAL A +← (‎ ꭺ ‎) AB7A CHEROKEE SMALL LETTER GO + +# ᴅ ꭰ + (‎ ᴅ ‎) 1D05 LATIN LETTER SMALL CAPITAL D +← (‎ ꭰ ‎) AB70 CHEROKEE SMALL LETTER A + +# ᴇ ꭼ + (‎ ᴇ ‎) 1D07 LATIN LETTER SMALL CAPITAL E +← (‎ ꭼ ‎) AB7C CHEROKEE SMALL LETTER GV + +# ᴊ ꭻ + (‎ ᴊ ‎) 1D0A LATIN LETTER SMALL CAPITAL J +← (‎ ꭻ ‎) AB7B CHEROKEE SMALL LETTER GU + +# ᴘ ᴩ ꮲ + (‎ ᴘ ‎) 1D18 LATIN LETTER SMALL CAPITAL P +← (‎ ᴩ ‎) 1D29 GREEK LETTER SMALL CAPITAL RHO +← (‎ ꮲ ‎) ABB2 CHEROKEE SMALL LETTER TLV + +# ᴴ ᵸ + (‎ ᴴ ‎) 1D34 MODIFIER LETTER CAPITAL H +← (‎ ᵸ ‎) 1D78 MODIFIER LETTER CYRILLIC EN + +# ᵋ ᶟ + (‎ ᵋ ‎) 1D4B MODIFIER LETTER SMALL OPEN E +← (‎ ᶟ ‎) 1D9F MODIFIER LETTER SMALL REVERSED OPEN E + +# ᵍ ᶢ + (‎ ᵍ ‎) 1D4D MODIFIER LETTER SMALL G +← (‎ ᶢ ‎) 1DA2 MODIFIER LETTER SMALL SCRIPT G + +# ᷟ ⷨ + (‎ ᷟ ‎) 1DDF COMBINING LATIN LETTER SMALL CAPITAL M +← (‎ ⷨ ‎) 2DE8 COMBINING CYRILLIC LETTER EM + +# ⷬ ᷮ + (‎ ᷮ ‎) 1DEE COMBINING LATIN SMALL LETTER P +← (‎ ⷬ ‎) 2DEC COMBINING CYRILLIC LETTER ER + +# ꭑ ṃ + (‎ ṃ ‎) 1E43 LATIN SMALL LETTER M WITH DOT BELOW +← (‎ ꭑ ‎) AB51 LATIN SMALL LETTER TURNED UI + +# ả ẚ + (‎ ẚ ‎) 1E9A LATIN SMALL LETTER A WITH RIGHT HALF RING +← (‎ ả ‎) 1EA3 LATIN SMALL LETTER A WITH HOOK ABOVE + +# ῴ ώ + (‎ ώ ‎) 1F7D GREEK SMALL LETTER OMEGA WITH OXIA +← (‎ ῴ ‎) 1FF4 GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI + +# ⵗ ⁝ ⋮ ︙ + (‎ ⁝ ‎) 205D TRICOLON +← (‎ ⵗ ‎) 2D57 TIFINAGH LETTER TUAREG YAGH +← (‎ ⋮ ‎) 22EE VERTICAL ELLIPSIS # →︙→ +← (‎ ︙ ‎) FE19 PRESENTATION FORM FOR VERTICAL HORIZONTAL ELLIPSIS + +# ⵂ ⁞ ⦙ ⸽ + (‎ ⁞ ‎) 205E VERTICAL FOUR DOTS +← (‎ ⵂ ‎) 2D42 TIFINAGH LETTER TUAREG YAH +← (‎ ⦙ ‎) 2999 DOTTED FENCE +← (‎ ⸽ ‎) 2E3D VERTICAL SIX DOTS + +# ꝰ ⁹ + (‎ ⁹ ‎) 2079 SUPERSCRIPT NINE +← (‎ ꝰ ‎) A770 MODIFIER LETTER US + +# ₁₀ ⏨ + (‎ ₁₀ ‎) 2081 2080 SUBSCRIPT ONE, SUBSCRIPT ZERO +← (‎ ⏨ ‎) 23E8 DECIMAL EXPONENT SYMBOL + +# ₸ 〒 〶 + (‎ ₸ ‎) 20B8 TENGE SIGN +← (‎ 〒 ‎) 3012 POSTAL MARK +← (‎ 〶 ‎) 3036 CIRCLED POSTAL MARK # →〒→ + +# ⃩ ꙯ + (‎ ⃩ ‎) 20E9 COMBINING WIDE BRIDGE ABOVE +← (‎ ꙯ ‎) A66F COMBINING CYRILLIC VZMET + +# ℗ Ⓟ + (‎ ℗ ‎) 2117 SOUND RECORDING COPYRIGHT +← (‎ Ⓟ ‎) 24C5 CIRCLED LATIN CAPITAL LETTER P + +# ꓨ ⅁ + (‎ ⅁ ‎) 2141 TURNED SANS-SERIF CAPITAL G +← (‎ ꓨ ‎) A4E8 LISU LETTER HHA + +# ꓶ 𐐑 𖼦 ⅂ 𝈕 𝈫 + (‎ ⅂ ‎) 2142 TURNED SANS-SERIF CAPITAL L +← (‎ ꓶ ‎) A4F6 LISU LETTER UH +← (‎ 𐐑 ‎) 10411 DESERET CAPITAL LETTER PEE +← (‎ 𖼦 ‎) 16F26 MIAO LETTER HA +← (‎ 𝈕 ‎) 1D215 GREEK VOCAL NOTATION SYMBOL-22 +← (‎ 𝈫 ‎) 1D22B GREEK INSTRUMENTAL NOTATION SYMBOL-24 # →𝈕→ + +# 𖼀 ⅃ + (‎ ⅃ ‎) 2143 REVERSED SANS-SERIF CAPITAL L +← (‎ 𖼀 ‎) 16F00 MIAO LETTER PA + +# ⅄ 𝈛 + (‎ ⅄ ‎) 2144 TURNED SANS-SERIF CAPITAL Y +← (‎ 𝈛 ‎) 1D21B GREEK VOCAL NOTATION SYMBOL-53 + +# ↞ ⯬ + (‎ ↞ ‎) 219E LEFTWARDS TWO HEADED ARROW +← (‎ ⯬ ‎) 2BEC LEFTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS + +# ↟ ⯭ + (‎ ↟ ‎) 219F UPWARDS TWO HEADED ARROW +← (‎ ⯭ ‎) 2BED UPWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS + +# ↠ ⯮ + (‎ ↠ ‎) 21A0 RIGHTWARDS TWO HEADED ARROW +← (‎ ⯮ ‎) 2BEE RIGHTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS + +# ↡ ⯯ + (‎ ↡ ‎) 21A1 DOWNWARDS TWO HEADED ARROW +← (‎ ⯯ ‎) 2BEF DOWNWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS + +# ↲ ↵ + (‎ ↲ ‎) 21B2 DOWNWARDS ARROW WITH TIP LEFTWARDS +← (‎ ↵ ‎) 21B5 DOWNWARDS ARROW WITH CORNER LEFTWARDS + +# 🄎 ↺ + (‎ ↺ ‎) 21BA ANTICLOCKWISE OPEN CIRCLE ARROW +← (‎ 🄎 ‎) 1F10E CIRCLED ANTICLOCKWISE ARROW + +# ⇃ᛚ ⇃↾ ⥯ + (‎ ⇃ᛚ ‎) 21C3 16DA DOWNWARDS HARPOON WITH BARB LEFTWARDS, RUNIC LETTER LAUKAZ LAGU LOGR L +← (‎ ⇃↾ ‎) 21C3 21BE DOWNWARDS HARPOON WITH BARB LEFTWARDS, UPWARDS HARPOON WITH BARB RIGHTWARDS +← (‎ ⥯ ‎) 296F DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT # →⇃↾→ + +# ⇃⇂ ⥥ + (‎ ⇃⇂ ‎) 21C3 21C2 DOWNWARDS HARPOON WITH BARB LEFTWARDS, DOWNWARDS HARPOON WITH BARB RIGHTWARDS +← (‎ ⥥ ‎) 2965 DOWNWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT + +# ∂ 𞣌 𝛛 𝜕 𝝏 𝞉 𝟃 + (‎ ∂ ‎) 2202 PARTIAL DIFFERENTIAL +← (‎ 𞣌 ‎) 1E8CC MENDE KIKAKUI DIGIT SIX +← (‎ 𝛛 ‎) 1D6DB MATHEMATICAL BOLD PARTIAL DIFFERENTIAL +← (‎ 𝜕 ‎) 1D715 MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL +← (‎ 𝝏 ‎) 1D74F MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL +← (‎ 𝞉 ‎) 1D789 MATHEMATICAL SANS-SERIF BOLD PARTIAL DIFFERENTIAL +← (‎ 𝟃 ‎) 1D7C3 MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL + +# ∅ ⌀ + (‎ ∅ ‎) 2205 EMPTY SET +← (‎ ⌀ ‎) 2300 DIAMETER SIGN + +# ∇ 𑢨 𝛁 𝛻 𝜵 𝝯 𝞩 + (‎ ∇ ‎) 2207 NABLA +← (‎ 𑢨 ‎) 118A8 WARANG CITI CAPITAL LETTER E +← (‎ 𝛁 ‎) 1D6C1 MATHEMATICAL BOLD NABLA +← (‎ 𝛻 ‎) 1D6FB MATHEMATICAL ITALIC NABLA +← (‎ 𝜵 ‎) 1D735 MATHEMATICAL BOLD ITALIC NABLA +← (‎ 𝝯 ‎) 1D76F MATHEMATICAL SANS-SERIF BOLD NABLA +← (‎ 𝞩 ‎) 1D7A9 MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA + +# ∇̈ ⍢ + (‎ ∇̈ ‎) 2207 0308 NABLA, COMBINING DIAERESIS +← (‎ ⍢ ‎) 2362 APL FUNCTIONAL SYMBOL DEL DIAERESIS + +# ∇̴ ⍫ + (‎ ∇̴ ‎) 2207 0334 NABLA, COMBINING TILDE OVERLAY +← (‎ ⍫ ‎) 236B APL FUNCTIONAL SYMBOL DEL TILDE + +# ∎ █ ■ + (‎ ∎ ‎) 220E END OF PROOF +← (‎ █ ‎) 2588 FULL BLOCK # →■→ +← (‎ ■ ‎) 25A0 BLACK SQUARE + +# ∐ ⨿ + (‎ ∐ ‎) 2210 N-ARY COPRODUCT +← (‎ ⨿ ‎) 2A3F AMALGAMATION OR COPRODUCT + +# ∠ 𞣈 + (‎ ∠ ‎) 2220 ANGLE +← (‎ 𞣈 ‎) 1E8C8 MENDE KIKAKUI DIGIT TWO + +# ∧ ⋀ + (‎ ∧ ‎) 2227 LOGICAL AND +← (‎ ⋀ ‎) 22C0 N-ARY LOGICAL AND + +# ∮∮ ∯ + (‎ ∮∮ ‎) 222E 222E CONTOUR INTEGRAL, CONTOUR INTEGRAL +← (‎ ∯ ‎) 222F SURFACE INTEGRAL + +# ∮∮∮ ∰ + (‎ ∮∮∮ ‎) 222E 222E 222E CONTOUR INTEGRAL, CONTOUR INTEGRAL, CONTOUR INTEGRAL +← (‎ ∰ ‎) 2230 VOLUME INTEGRAL + +# ∴ ⸫ + (‎ ∴ ‎) 2234 THEREFORE +← (‎ ⸫ ‎) 2E2B ONE DOT OVER TWO DOTS PUNCTUATION + +# ∵ ⸪ + (‎ ∵ ‎) 2235 BECAUSE +← (‎ ⸪ ‎) 2E2A TWO DOTS OVER ONE DOT PUNCTUATION + +# ∷ ⸬ + (‎ ∷ ‎) 2237 PROPORTION +← (‎ ⸬ ‎) 2E2C SQUARED FOUR DOT PUNCTUATION + +# ≈ 𑇞 + (‎ ≈ ‎) 2248 ALMOST EQUAL TO +← (‎ 𑇞 ‎) 111DE SHARADA SECTION MARK-1 + +# ≏ ♎ 🝞 + (‎ ≏ ‎) 224F DIFFERENCE BETWEEN +← (‎ ♎ ‎) 264E LIBRA +← (‎ 🝞 ‎) 1F75E ALCHEMICAL SYMBOL FOR SUBLIMATION # →♎→ + +# ≡ ≣ + (‎ ≡ ‎) 2261 IDENTICAL TO +← (‎ ≣ ‎) 2263 STRICTLY EQUIVALENT TO + +# ⊍ ⨃ + (‎ ⊍ ‎) 228D MULTISET MULTIPLICATION +← (‎ ⨃ ‎) 2A03 N-ARY UNION OPERATOR WITH DOT + +# ⊎ ⨄ + (‎ ⊎ ‎) 228E MULTISET UNION +← (‎ ⨄ ‎) 2A04 N-ARY UNION OPERATOR WITH PLUS + +# ⊏ 𝈸 + (‎ ⊏ ‎) 228F SQUARE IMAGE OF +← (‎ 𝈸 ‎) 1D238 GREEK INSTRUMENTAL NOTATION SYMBOL-43 + +# ⊐ 𝈹 + (‎ ⊐ ‎) 2290 SQUARE ORIGINAL OF +← (‎ 𝈹 ‎) 1D239 GREEK INSTRUMENTAL NOTATION SYMBOL-45 + +# ⊓ ⨅ + (‎ ⊓ ‎) 2293 SQUARE CAP +← (‎ ⨅ ‎) 2A05 N-ARY SQUARE INTERSECTION OPERATOR + +# ⊔ ⨆ + (‎ ⊔ ‎) 2294 SQUARE CUP +← (‎ ⨆ ‎) 2A06 N-ARY SQUARE UNION OPERATOR + +# 𐊨 ⊕ ⨁ 🜨 Ꚛ + (‎ ⊕ ‎) 2295 CIRCLED PLUS +← (‎ 𐊨 ‎) 102A8 CARIAN LETTER Q +← (‎ ⨁ ‎) 2A01 N-ARY CIRCLED PLUS OPERATOR +← (‎ 🜨 ‎) 1F728 ALCHEMICAL SYMBOL FOR VERDIGRIS +← (‎ Ꚛ ‎) A69A CYRILLIC CAPITAL LETTER CROSSED O + +# ⊗ ⨂ + (‎ ⊗ ‎) 2297 CIRCLED TIMES +← (‎ ⨂ ‎) 2A02 N-ARY CIRCLED TIMES OPERATOR + +# ⊛ ⍟ + (‎ ⊛ ‎) 229B CIRCLED ASTERISK OPERATOR +← (‎ ⍟ ‎) 235F APL FUNCTIONAL SYMBOL CIRCLE STAR + +# ⊠ 🝱 + (‎ ⊠ ‎) 22A0 SQUARED TIMES +← (‎ 🝱 ‎) 1F771 ALCHEMICAL SYMBOL FOR MONTH + +# ⊡ 🝕 + (‎ ⊡ ‎) 22A1 SQUARED DOT OPERATOR +← (‎ 🝕 ‎) 1F755 ALCHEMICAL SYMBOL FOR URINE + +# ꓕ ⊥ ⟂ 𝈜 Ʇ + (‎ ⊥ ‎) 22A5 UP TACK +← (‎ ꓕ ‎) A4D5 LISU LETTER THA +← (‎ ⟂ ‎) 27C2 PERPENDICULAR +← (‎ 𝈜 ‎) 1D21C GREEK VOCAL NOTATION SYMBOL-54 # →Ʇ→→ꓕ→ +← (‎ Ʇ ‎) A7B1 LATIN CAPITAL LETTER TURNED T # →ꓕ→ + +# ⊲ ◁ + (‎ ⊲ ‎) 22B2 NORMAL SUBGROUP OF +← (‎ ◁ ‎) 25C1 WHITE LEFT-POINTING TRIANGLE + +# ⊳ ▷ + (‎ ⊳ ‎) 22B3 CONTAINS AS NORMAL SUBGROUP +← (‎ ▷ ‎) 25B7 WHITE RIGHT-POINTING TRIANGLE + +# ⋆̈ ⍣ + (‎ ⋆̈ ‎) 22C6 0308 STAR OPERATOR, COMBINING DIAERESIS +← (‎ ⍣ ‎) 2363 APL FUNCTIONAL SYMBOL STAR DIAERESIS + +# ⌇ ︴ + (‎ ⌇ ‎) 2307 WAVY LINE +← (‎ ︴ ‎) FE34 PRESENTATION FORM FOR VERTICAL WAVY LOW LINE + +# ⌒ ◠ + (‎ ⌒ ‎) 2312 ARC +← (‎ ◠ ‎) 25E0 UPPER HALF CIRCLE + +# ⌙ ⨽ + (‎ ⌙ ‎) 2319 TURNED NOT SIGN +← (‎ ⨽ ‎) 2A3D RIGHTHAND INTERIOR PRODUCT + +# ⌤ ⌥ + (‎ ⌤ ‎) 2324 UP ARROWHEAD BETWEEN TWO HORIZONTAL BARS +← (‎ ⌥ ‎) 2325 OPTION KEY + +# ❬ く 𡿨 ⟨ 〈 ㇛ 〈 + (‎ 〈 ‎) 2329 LEFT-POINTING ANGLE BRACKET +← (‎ ❬ ‎) 276C MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT # →〈→ +← (‎ く ‎) 304F HIRAGANA LETTER KU # →㇛→→⟨→ +← (‎ 𡿨 ‎) 21FE8 CJK UNIFIED IDEOGRAPH-21FE8 # →㇛→→⟨→ +← (‎ ⟨ ‎) 27E8 MATHEMATICAL LEFT ANGLE BRACKET +← (‎ 〈 ‎) 3008 LEFT ANGLE BRACKET +← (‎ ㇛ ‎) 31DB CJK STROKE PD # →⟨→ + +# ❭ ⟩ 〉 〉 + (‎ 〉 ‎) 232A RIGHT-POINTING ANGLE BRACKET +← (‎ ❭ ‎) 276D MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT # →〉→ +← (‎ ⟩ ‎) 27E9 MATHEMATICAL RIGHT ANGLE BRACKET +← (‎ 〉 ‎) 3009 RIGHT ANGLE BRACKET + +# ⌻ ⧇ + (‎ ⌻ ‎) 233B APL FUNCTIONAL SYMBOL QUAD JOT +← (‎ ⧇ ‎) 29C7 SQUARED SMALL CIRCLE + +# ⌾ ◎ ⦾ + (‎ ⌾ ‎) 233E APL FUNCTIONAL SYMBOL CIRCLE JOT +← (‎ ◎ ‎) 25CE BULLSEYE # →⦾→ +← (‎ ⦾ ‎) 29BE CIRCLED WHITE BULLET + +# 〼 ⍁ ⧄ + (‎ ⍁ ‎) 2341 APL FUNCTIONAL SYMBOL QUAD SLASH +← (‎ 〼 ‎) 303C MASU MARK # →⧄→ +← (‎ ⧄ ‎) 29C4 SQUARED RISING DIAGONAL SLASH + +# ⍂ ⧅ + (‎ ⍂ ‎) 2342 APL FUNCTIONAL SYMBOL QUAD BACKSLASH +← (‎ ⧅ ‎) 29C5 SQUARED FALLING DIAGONAL SLASH + +# ⍉ ⦰ + (‎ ⍉ ‎) 2349 APL FUNCTIONAL SYMBOL CIRCLE BACKSLASH +← (‎ ⦰ ‎) 29B0 REVERSED EMPTY SET + +# ⍋ ⏃ + (‎ ⍋ ‎) 234B APL FUNCTIONAL SYMBOL DELTA STILE +← (‎ ⏃ ‎) 23C3 DENTISTRY SYMBOL LIGHT VERTICAL WITH TRIANGLE + +# ⍎ ⏂ + (‎ ⍎ ‎) 234E APL FUNCTIONAL SYMBOL DOWN TACK JOT +← (‎ ⏂ ‎) 23C2 DENTISTRY SYMBOL LIGHT UP AND HORIZONTAL WITH CIRCLE + +# ⍕ ⏁ + (‎ ⍕ ‎) 2355 APL FUNCTIONAL SYMBOL UP TACK JOT +← (‎ ⏁ ‎) 23C1 DENTISTRY SYMBOL LIGHT DOWN AND HORIZONTAL WITH CIRCLE + +# ⍭ ⏆ + (‎ ⍭ ‎) 236D APL FUNCTIONAL SYMBOL STILE TILDE +← (‎ ⏆ ‎) 23C6 DENTISTRY SYMBOL LIGHT VERTICAL AND WAVE + +# ⎈ ☸ + (‎ ⎈ ‎) 2388 HELM SYMBOL +← (‎ ☸ ‎) 2638 WHEEL OF DHARMA + +# ⏜ ︵ + (‎ ⏜ ‎) 23DC TOP PARENTHESIS +← (‎ ︵ ‎) FE35 PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS + +# ⏝ ︶ + (‎ ⏝ ‎) 23DD BOTTOM PARENTHESIS +← (‎ ︶ ‎) FE36 PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS + +# ⏞ ︷ + (‎ ⏞ ‎) 23DE TOP CURLY BRACKET +← (‎ ︷ ‎) FE37 PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET + +# ⏟ ︸ + (‎ ⏟ ‎) 23DF BOTTOM CURLY BRACKET +← (‎ ︸ ‎) FE38 PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET + +# ⏠ ︹ + (‎ ⏠ ‎) 23E0 TOP TORTOISE SHELL BRACKET +← (‎ ︹ ‎) FE39 PRESENTATION FORM FOR VERTICAL LEFT TORTOISE SHELL BRACKET + +# ⏡ ︺ + (‎ ⏡ ‎) 23E1 BOTTOM TORTOISE SHELL BRACKET +← (‎ ︺ ‎) FE3A PRESENTATION FORM FOR VERTICAL RIGHT TORTOISE SHELL BRACKET + +# ⏥ ▱ + (‎ ⏥ ‎) 23E5 FLATNESS +← (‎ ▱ ‎) 25B1 WHITE PARALLELOGRAM + +# ⏻ ⏼ + (‎ ⏻ ‎) 23FB POWER SYMBOL +← (‎ ⏼ ‎) 23FC POWER ON-OFF SYMBOL + +# ☾ 🌘 ⏾ + (‎ ⏾ ‎) 23FE POWER SLEEP SYMBOL +← (‎ ☾ ‎) 263E LAST QUARTER MOON +← (‎ 🌘 ‎) 1F318 WANING CRESCENT MOON SYMBOL # →☾→ + +# ➀ ① + (‎ ① ‎) 2460 CIRCLED DIGIT ONE +← (‎ ➀ ‎) 2780 DINGBAT CIRCLED SANS-SERIF DIGIT ONE + +# ➁ ② + (‎ ② ‎) 2461 CIRCLED DIGIT TWO +← (‎ ➁ ‎) 2781 DINGBAT CIRCLED SANS-SERIF DIGIT TWO + +# ➂ ③ + (‎ ③ ‎) 2462 CIRCLED DIGIT THREE +← (‎ ➂ ‎) 2782 DINGBAT CIRCLED SANS-SERIF DIGIT THREE + +# ➃ ④ + (‎ ④ ‎) 2463 CIRCLED DIGIT FOUR +← (‎ ➃ ‎) 2783 DINGBAT CIRCLED SANS-SERIF DIGIT FOUR + +# ➄ ⑤ + (‎ ⑤ ‎) 2464 CIRCLED DIGIT FIVE +← (‎ ➄ ‎) 2784 DINGBAT CIRCLED SANS-SERIF DIGIT FIVE + +# ➅ ⑥ + (‎ ⑥ ‎) 2465 CIRCLED DIGIT SIX +← (‎ ➅ ‎) 2785 DINGBAT CIRCLED SANS-SERIF DIGIT SIX + +# ➆ ⑦ + (‎ ⑦ ‎) 2466 CIRCLED DIGIT SEVEN +← (‎ ➆ ‎) 2786 DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN + +# ➇ ⑧ + (‎ ⑧ ‎) 2467 CIRCLED DIGIT EIGHT +← (‎ ➇ ‎) 2787 DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT + +# ➈ ⑨ + (‎ ⑨ ‎) 2468 CIRCLED DIGIT NINE +← (‎ ➈ ‎) 2788 DINGBAT CIRCLED SANS-SERIF DIGIT NINE + +# ➉ ⑩ + (‎ ⑩ ‎) 2469 CIRCLED NUMBER TEN +← (‎ ➉ ‎) 2789 DINGBAT CIRCLED SANS-SERIF NUMBER TEN + +# Ⓘ ⓛ + (‎ Ⓘ ‎) 24BE CIRCLED LATIN CAPITAL LETTER I +← (‎ ⓛ ‎) 24DB CIRCLED LATIN SMALL LETTER L + +# 🄍 ⓪ + (‎ ⓪ ‎) 24EA CIRCLED DIGIT ZERO +← (‎ 🄍 ‎) 1F10D CIRCLED ZERO WITH SLASH + +# │ ┃ ︱ | + (‎ │ ‎) 2502 BOX DRAWINGS LIGHT VERTICAL +← (‎ ┃ ‎) 2503 BOX DRAWINGS HEAVY VERTICAL +← (‎ ︱ ‎) FE31 PRESENTATION FORM FOR VERTICAL EM DASH # →|→ +← (‎ | ‎) FF5C FULLWIDTH VERTICAL LINE + +# ┌ ┏ + (‎ ┌ ‎) 250C BOX DRAWINGS LIGHT DOWN AND RIGHT +← (‎ ┏ ‎) 250F BOX DRAWINGS HEAVY DOWN AND RIGHT + +# ├ ┣ + (‎ ├ ‎) 251C BOX DRAWINGS LIGHT VERTICAL AND RIGHT +← (‎ ┣ ‎) 2523 BOX DRAWINGS HEAVY VERTICAL AND RIGHT + +# ▌ ▐ + (‎ ▌ ‎) 258C LEFT HALF BLOCK +← (‎ ▐ ‎) 2590 RIGHT HALF BLOCK + +# ▖ ▗ + (‎ ▖ ‎) 2596 QUADRANT LOWER LEFT +← (‎ ▗ ‎) 2597 QUADRANT LOWER RIGHT + +# ▘ ▝ + (‎ ▘ ‎) 2598 QUADRANT UPPER LEFT +← (‎ ▝ ‎) 259D QUADRANT UPPER RIGHT + +# □ ☐ + (‎ □ ‎) 25A1 WHITE SQUARE +← (‎ ☐ ‎) 2610 BALLOT BOX + +# ▪ ■ + (‎ ▪ ‎) 25AA BLACK SMALL SQUARE +← (‎ ■ ‎) FFED HALFWIDTH BLACK SQUARE + +# ▶ ▸ ► + (‎ ▶ ‎) 25B6 BLACK RIGHT-POINTING TRIANGLE +← (‎ ▸ ‎) 25B8 BLACK RIGHT-POINTING SMALL TRIANGLE # →►→ +← (‎ ► ‎) 25BA BLACK RIGHT-POINTING POINTER + +# 𐊼 ▽ 𝈔 🜄 + (‎ ▽ ‎) 25BD WHITE DOWN-POINTING TRIANGLE +← (‎ 𐊼 ‎) 102BC CARIAN LETTER K +← (‎ 𝈔 ‎) 1D214 GREEK VOCAL NOTATION SYMBOL-21 +← (‎ 🜄 ‎) 1F704 ALCHEMICAL SYMBOL FOR WATER + +# 𐦞 𓋹 ☥ + (‎ ☥ ‎) 2625 ANKH +← (‎ 𐦞 ‎) 1099E MEROITIC HIEROGLYPHIC SYMBOL VIDJ +← (‎ 𓋹 ‎) 132F9 EGYPTIAN HIEROGLYPH S034 + +# ☧ ⳩ + (‎ ☧ ‎) 2627 CHI RHO +← (‎ ⳩ ‎) 2CE9 COPTIC SYMBOL KHI RO + +# ☩ 🜊 + (‎ ☩ ‎) 2629 CROSS OF JERUSALEM +← (‎ 🜊 ‎) 1F70A ALCHEMICAL SYMBOL FOR VINEGAR + +# Ⲷ ☰ + (‎ ☰ ‎) 2630 TRIGRAM FOR HEAVEN +← (‎ Ⲷ ‎) 2CB6 COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE + +# ☽ 🌒 🌙 + (‎ ☽ ‎) 263D FIRST QUARTER MOON +← (‎ 🌒 ‎) 1F312 WAXING CRESCENT MOON SYMBOL +← (‎ 🌙 ‎) 1F319 CRESCENT MOON + +# 𝅘𝅥 ♩ + (‎ ♩ ‎) 2669 QUARTER NOTE +← (‎ 𝅘𝅥 ‎) 1D158 1D165 MUSICAL SYMBOL NOTEHEAD BLACK, MUSICAL SYMBOL COMBINING STEM + +# 𝅘𝅥𝅮 ♪ + (‎ ♪ ‎) 266A EIGHTH NOTE +← (‎ 𝅘𝅥𝅮 ‎) 1D158 1D165 1D16E MUSICAL SYMBOL NOTEHEAD BLACK, MUSICAL SYMBOL COMBINING STEM, MUSICAL SYMBOL COMBINING FLAG-1 + +# ⟦ 〚 + (‎ ⟦ ‎) 27E6 MATHEMATICAL LEFT WHITE SQUARE BRACKET +← (‎ 〚 ‎) 301A LEFT WHITE SQUARE BRACKET + +# ⟧ 〛 + (‎ ⟧ ‎) 27E7 MATHEMATICAL RIGHT WHITE SQUARE BRACKET +← (‎ 〛 ‎) 301B RIGHT WHITE SQUARE BRACKET + +# ⦚ ⧙ + (‎ ⦚ ‎) 299A VERTICAL ZIGZAG LINE +← (‎ ⧙ ‎) 29D9 RIGHT WIGGLY FENCE + +# 𐋀 ⧖ + (‎ ⧖ ‎) 29D6 WHITE HOURGLASS +← (‎ 𐋀 ‎) 102C0 CARIAN LETTER G + +# ⧟ 🜺 + (‎ ⧟ ‎) 29DF DOUBLE-ENDED MULTIMAP +← (‎ 🜺 ‎) 1F73A ALCHEMICAL SYMBOL FOR ARSENIC + +# ⨟ ⨾ + (‎ ⨟ ‎) 2A1F Z NOTATION SCHEMA COMPOSITION +← (‎ ⨾ ‎) 2A3E Z NOTATION RELATIONAL COMPOSITION + +# ⰿ ꦒ + (‎ ⰿ ‎) 2C3F GLAGOLITIC SMALL LETTER MYSLITE +← (‎ ꦒ ‎) A992 JAVANESE LETTER GA + +# Ɒ 𐐟 + (‎ Ɒ ‎) 2C70 LATIN CAPITAL LETTER TURNED ALPHA +← (‎ 𐐟 ‎) 1041F DESERET CAPITAL LETTER ESH + +# ⱶ ꮀ + (‎ ⱶ ‎) 2C76 LATIN SMALL LETTER HALF H +← (‎ ꮀ ‎) AB80 CHEROKEE SMALL LETTER HO + +# ⳨ 𐆠 + (‎ ⳨ ‎) 2CE8 COPTIC SYMBOL TAU RO +← (‎ 𐆠 ‎) 101A0 GREEK SYMBOL TAU RHO + +# ⵀ 𐊸 + (‎ ⵀ ‎) 2D40 TIFINAGH LETTER YAH +← (‎ 𐊸 ‎) 102B8 CARIAN LETTER SS + +# 乛 ⺂ ㇖ + (‎ ⺂ ‎) 2E82 CJK RADICAL SECOND ONE +← (‎ 乛 ‎) 4E5B CJK UNIFIED IDEOGRAPH-4E5B # →㇖→ +← (‎ ㇖ ‎) 31D6 CJK STROKE HG + +# 乚 ⺃ ㇟ + (‎ ⺃ ‎) 2E83 CJK RADICAL SECOND TWO +← (‎ 乚 ‎) 4E5A CJK UNIFIED IDEOGRAPH-4E5A +← (‎ ㇟ ‎) 31DF CJK STROKE SWG + +# 亻 イ ⺅ + (‎ ⺅ ‎) 2E85 CJK RADICAL PERSON +← (‎ 亻 ‎) 4EBB CJK UNIFIED IDEOGRAPH-4EBB +← (‎ イ ‎) 30A4 KATAKANA LETTER I + +# 刂 ⺉ + (‎ ⺉ ‎) 2E89 CJK RADICAL KNIFE TWO +← (‎ 刂 ‎) 5202 CJK UNIFIED IDEOGRAPH-5202 + +# 㔾 ⺋ + (‎ ⺋ ‎) 2E8B CJK RADICAL SEAL +← (‎ 㔾 ‎) 353E CJK UNIFIED IDEOGRAPH-353E + +# 兀 ⺎ 兀 + (‎ ⺎ ‎) 2E8E CJK RADICAL LAME ONE +← (‎ 兀 ‎) 5140 CJK UNIFIED IDEOGRAPH-5140 +← (‎ 兀 ‎) FA0C CJK COMPATIBILITY IDEOGRAPH-FA0C + +# 尣 ⺏ + (‎ ⺏ ‎) 2E8F CJK RADICAL LAME TWO +← (‎ 尣 ‎) 5C23 CJK UNIFIED IDEOGRAPH-5C23 + +# 尢 ⺐ 尢 ⼪ + (‎ ⺐ ‎) 2E90 CJK RADICAL LAME THREE +← (‎ 尢 ‎) 5C22 CJK UNIFIED IDEOGRAPH-5C22 +← (‎ 尢 ‎) 2F875 CJK COMPATIBILITY IDEOGRAPH-2F875 # →尢→ +← (‎ ⼪ ‎) 2F2A KANGXI RADICAL LAME + +# 巳 ⺒ + (‎ ⺒ ‎) 2E92 CJK RADICAL SNAKE +← (‎ 巳 ‎) 5DF3 CJK UNIFIED IDEOGRAPH-5DF3 + +# 幺 ⺓ ⼳ + (‎ ⺓ ‎) 2E93 CJK RADICAL THREAD +← (‎ 幺 ‎) 5E7A CJK UNIFIED IDEOGRAPH-5E7A +← (‎ ⼳ ‎) 2F33 KANGXI RADICAL SHORT THREAD + +# 彑 ⺔ + (‎ ⺔ ‎) 2E94 CJK RADICAL SNOUT ONE +← (‎ 彑 ‎) 5F51 CJK UNIFIED IDEOGRAPH-5F51 + +# 忄 ⺖ + (‎ ⺖ ‎) 2E96 CJK RADICAL HEART ONE +← (‎ 忄 ‎) 5FC4 CJK UNIFIED IDEOGRAPH-5FC4 + +# 㣺 ⺗ + (‎ ⺗ ‎) 2E97 CJK RADICAL HEART TWO +← (‎ 㣺 ‎) 38FA CJK UNIFIED IDEOGRAPH-38FA + +# 扌 ⺘ + (‎ ⺘ ‎) 2E98 CJK RADICAL HAND +← (‎ 扌 ‎) 624C CJK UNIFIED IDEOGRAPH-624C + +# 攵 ⺙ + (‎ ⺙ ‎) 2E99 CJK RADICAL RAP +← (‎ 攵 ‎) 6535 CJK UNIFIED IDEOGRAPH-6535 + +# 旡 ⺛ + (‎ ⺛ ‎) 2E9B CJK RADICAL CHOKE +← (‎ 旡 ‎) 65E1 CJK UNIFIED IDEOGRAPH-65E1 + +# 歺 ⺞ + (‎ ⺞ ‎) 2E9E CJK RADICAL DEATH +← (‎ 歺 ‎) 6B7A CJK UNIFIED IDEOGRAPH-6B7A + +# 母 ⺟ + (‎ ⺟ ‎) 2E9F CJK RADICAL MOTHER +← (‎ 母 ‎) 6BCD CJK UNIFIED IDEOGRAPH-6BCD + +# 民 ⺠ + (‎ ⺠ ‎) 2EA0 CJK RADICAL CIVILIAN +← (‎ 民 ‎) 6C11 CJK UNIFIED IDEOGRAPH-6C11 + +# 氵 ⺡ + (‎ ⺡ ‎) 2EA1 CJK RADICAL WATER ONE +← (‎ 氵 ‎) 6C35 CJK UNIFIED IDEOGRAPH-6C35 + +# 氺 ⺢ + (‎ ⺢ ‎) 2EA2 CJK RADICAL WATER TWO +← (‎ 氺 ‎) 6C3A CJK UNIFIED IDEOGRAPH-6C3A + +# 灬 ⺣ + (‎ ⺣ ‎) 2EA3 CJK RADICAL FIRE +← (‎ 灬 ‎) 706C CJK UNIFIED IDEOGRAPH-706C + +# 爫 ⺤ 爫 + (‎ ⺤ ‎) 2EA4 CJK RADICAL PAW ONE +← (‎ 爫 ‎) 722B CJK UNIFIED IDEOGRAPH-722B +← (‎ 爫 ‎) FA49 CJK COMPATIBILITY IDEOGRAPH-FA49 + +# 丬 ⺦ + (‎ ⺦ ‎) 2EA6 CJK RADICAL SIMPLIFIED HALF TREE TRUNK +← (‎ 丬 ‎) 4E2C CJK UNIFIED IDEOGRAPH-4E2C + +# 犭 ⺨ + (‎ ⺨ ‎) 2EA8 CJK RADICAL DOG +← (‎ 犭 ‎) 72AD CJK UNIFIED IDEOGRAPH-72AD + +# 罒 ⺫ ⺲ + (‎ ⺫ ‎) 2EAB CJK RADICAL EYE +← (‎ 罒 ‎) 7F52 CJK UNIFIED IDEOGRAPH-7F52 +← (‎ ⺲ ‎) 2EB2 CJK RADICAL NET TWO + +# 礻 ⺭ + (‎ ⺭ ‎) 2EAD CJK RADICAL SPIRIT TWO +← (‎ 礻 ‎) 793B CJK UNIFIED IDEOGRAPH-793B + +# 糹 ⺯ + (‎ ⺯ ‎) 2EAF CJK RADICAL SILK +← (‎ 糹 ‎) 7CF9 CJK UNIFIED IDEOGRAPH-7CF9 + +# 罓 ⺱ + (‎ ⺱ ‎) 2EB1 CJK RADICAL NET ONE +← (‎ 罓 ‎) 7F53 CJK UNIFIED IDEOGRAPH-7F53 + +# 耂 ⺹ + (‎ ⺹ ‎) 2EB9 CJK RADICAL OLD +← (‎ 耂 ‎) 8002 CJK UNIFIED IDEOGRAPH-8002 + +# 肀 ⺺ + (‎ ⺺ ‎) 2EBA CJK RADICAL BRUSH ONE +← (‎ 肀 ‎) 8080 CJK UNIFIED IDEOGRAPH-8080 + +# 艹 ⺾ ⺿ ⻀ 艹 艹 + (‎ ⺾ ‎) 2EBE CJK RADICAL GRASS ONE +← (‎ 艹 ‎) 8279 CJK UNIFIED IDEOGRAPH-8279 +← (‎ ⺿ ‎) 2EBF CJK RADICAL GRASS TWO # →艹→→艹→ +← (‎ ⻀ ‎) 2EC0 CJK RADICAL GRASS THREE # →艹→→艹→ +← (‎ 艹 ‎) FA5D CJK COMPATIBILITY IDEOGRAPH-FA5D # →艹→ +← (‎ 艹 ‎) FA5E CJK COMPATIBILITY IDEOGRAPH-FA5E # →艹→ + +# 虎 ⻁ + (‎ ⻁ ‎) 2EC1 CJK RADICAL TIGER +← (‎ 虎 ‎) 864E CJK UNIFIED IDEOGRAPH-864E + +# 衤 ⻂ + (‎ ⻂ ‎) 2EC2 CJK RADICAL CLOTHES +← (‎ 衤 ‎) 8864 CJK UNIFIED IDEOGRAPH-8864 + +# 覀 ⻃ + (‎ ⻃ ‎) 2EC3 CJK RADICAL WEST ONE +← (‎ 覀 ‎) 8980 CJK UNIFIED IDEOGRAPH-8980 + +# 西 ⻄ + (‎ ⻄ ‎) 2EC4 CJK RADICAL WEST TWO +← (‎ 西 ‎) 897F CJK UNIFIED IDEOGRAPH-897F + +# 见 ⻅ + (‎ ⻅ ‎) 2EC5 CJK RADICAL C-SIMPLIFIED SEE +← (‎ 见 ‎) 89C1 CJK UNIFIED IDEOGRAPH-89C1 + +# 讠 ⻈ + (‎ ⻈ ‎) 2EC8 CJK RADICAL C-SIMPLIFIED SPEECH +← (‎ 讠 ‎) 8BA0 CJK UNIFIED IDEOGRAPH-8BA0 + +# 贝 ⻉ + (‎ ⻉ ‎) 2EC9 CJK RADICAL C-SIMPLIFIED SHELL +← (‎ 贝 ‎) 8D1D CJK UNIFIED IDEOGRAPH-8D1D + +# 车 ⻋ + (‎ ⻋ ‎) 2ECB CJK RADICAL C-SIMPLIFIED CART +← (‎ 车 ‎) 8F66 CJK UNIFIED IDEOGRAPH-8F66 + +# 辶 ⻌ ⻍ 辶 + (‎ ⻌ ‎) 2ECC CJK RADICAL SIMPLIFIED WALK +← (‎ 辶 ‎) 8FB6 CJK UNIFIED IDEOGRAPH-8FB6 +← (‎ ⻍ ‎) 2ECD CJK RADICAL WALK ONE # →辶→ +← (‎ 辶 ‎) FA66 CJK COMPATIBILITY IDEOGRAPH-FA66 + +# 阝 ⻏ ⻖ + (‎ ⻏ ‎) 2ECF CJK RADICAL CITY +← (‎ 阝 ‎) 961D CJK UNIFIED IDEOGRAPH-961D +← (‎ ⻖ ‎) 2ED6 CJK RADICAL MOUND TWO + +# 钅 ⻐ + (‎ ⻐ ‎) 2ED0 CJK RADICAL C-SIMPLIFIED GOLD +← (‎ 钅 ‎) 9485 CJK UNIFIED IDEOGRAPH-9485 + +# 長 ⻑ ⾧ + (‎ ⻑ ‎) 2ED1 CJK RADICAL LONG ONE +← (‎ 長 ‎) 9577 CJK UNIFIED IDEOGRAPH-9577 +← (‎ ⾧ ‎) 2FA7 KANGXI RADICAL LONG + +# 镸 ⻒ + (‎ ⻒ ‎) 2ED2 CJK RADICAL LONG TWO +← (‎ 镸 ‎) 9578 CJK UNIFIED IDEOGRAPH-9578 + +# 长 ⻓ + (‎ ⻓ ‎) 2ED3 CJK RADICAL C-SIMPLIFIED LONG +← (‎ 长 ‎) 957F CJK UNIFIED IDEOGRAPH-957F + +# 门 ⻔ + (‎ ⻔ ‎) 2ED4 CJK RADICAL C-SIMPLIFIED GATE +← (‎ 门 ‎) 95E8 CJK UNIFIED IDEOGRAPH-95E8 + +# 青 ⻘ + (‎ ⻘ ‎) 2ED8 CJK RADICAL BLUE +← (‎ 青 ‎) 9752 CJK UNIFIED IDEOGRAPH-9752 + +# 韦 ⻙ + (‎ ⻙ ‎) 2ED9 CJK RADICAL C-SIMPLIFIED TANNED LEATHER +← (‎ 韦 ‎) 97E6 CJK UNIFIED IDEOGRAPH-97E6 + +# 页 ⻚ + (‎ ⻚ ‎) 2EDA CJK RADICAL C-SIMPLIFIED LEAF +← (‎ 页 ‎) 9875 CJK UNIFIED IDEOGRAPH-9875 + +# 风 ⻛ + (‎ ⻛ ‎) 2EDB CJK RADICAL C-SIMPLIFIED WIND +← (‎ 风 ‎) 98CE CJK UNIFIED IDEOGRAPH-98CE + +# 飞 ⻜ + (‎ ⻜ ‎) 2EDC CJK RADICAL C-SIMPLIFIED FLY +← (‎ 飞 ‎) 98DE CJK UNIFIED IDEOGRAPH-98DE + +# 食 ⻝ ⾷ + (‎ ⻝ ‎) 2EDD CJK RADICAL EAT ONE +← (‎ 食 ‎) 98DF CJK UNIFIED IDEOGRAPH-98DF +← (‎ ⾷ ‎) 2FB7 KANGXI RADICAL EAT + +# 飠 ⻟ + (‎ ⻟ ‎) 2EDF CJK RADICAL EAT THREE +← (‎ 飠 ‎) 98E0 CJK UNIFIED IDEOGRAPH-98E0 + +# 饣 ⻠ + (‎ ⻠ ‎) 2EE0 CJK RADICAL C-SIMPLIFIED EAT +← (‎ 饣 ‎) 9963 CJK UNIFIED IDEOGRAPH-9963 + +# 马 ⻢ + (‎ ⻢ ‎) 2EE2 CJK RADICAL C-SIMPLIFIED HORSE +← (‎ 马 ‎) 9A6C CJK UNIFIED IDEOGRAPH-9A6C + +# 鬼 ⻤ ⿁ + (‎ ⻤ ‎) 2EE4 CJK RADICAL GHOST +← (‎ 鬼 ‎) 9B3C CJK UNIFIED IDEOGRAPH-9B3C +← (‎ ⿁ ‎) 2FC1 KANGXI RADICAL GHOST + +# 鱼 ⻥ + (‎ ⻥ ‎) 2EE5 CJK RADICAL C-SIMPLIFIED FISH +← (‎ 鱼 ‎) 9C7C CJK UNIFIED IDEOGRAPH-9C7C + +# 麦 ⻨ + (‎ ⻨ ‎) 2EE8 CJK RADICAL SIMPLIFIED WHEAT +← (‎ 麦 ‎) 9EA6 CJK UNIFIED IDEOGRAPH-9EA6 + +# 黄 ⻩ + (‎ ⻩ ‎) 2EE9 CJK RADICAL SIMPLIFIED YELLOW +← (‎ 黄 ‎) 9EC4 CJK UNIFIED IDEOGRAPH-9EC4 + +# 斉 ⻫ + (‎ ⻫ ‎) 2EEB CJK RADICAL J-SIMPLIFIED EVEN +← (‎ 斉 ‎) 6589 CJK UNIFIED IDEOGRAPH-6589 + +# 齐 ⻬ + (‎ ⻬ ‎) 2EEC CJK RADICAL C-SIMPLIFIED EVEN +← (‎ 齐 ‎) 9F50 CJK UNIFIED IDEOGRAPH-9F50 + +# 歯 ⻭ + (‎ ⻭ ‎) 2EED CJK RADICAL J-SIMPLIFIED TOOTH +← (‎ 歯 ‎) 6B6F CJK UNIFIED IDEOGRAPH-6B6F + +# 齿 ⻮ + (‎ ⻮ ‎) 2EEE CJK RADICAL C-SIMPLIFIED TOOTH +← (‎ 齿 ‎) 9F7F CJK UNIFIED IDEOGRAPH-9F7F + +# 竜 ⻯ + (‎ ⻯ ‎) 2EEF CJK RADICAL J-SIMPLIFIED DRAGON +← (‎ 竜 ‎) 7ADC CJK UNIFIED IDEOGRAPH-7ADC + +# 龙 ⻰ + (‎ ⻰ ‎) 2EF0 CJK RADICAL C-SIMPLIFIED DRAGON +← (‎ 龙 ‎) 9F99 CJK UNIFIED IDEOGRAPH-9F99 + +# 亀 ⻲ + (‎ ⻲ ‎) 2EF2 CJK RADICAL J-SIMPLIFIED TURTLE +← (‎ 亀 ‎) 4E80 CJK UNIFIED IDEOGRAPH-4E80 + +# 龟 ⻳ + (‎ ⻳ ‎) 2EF3 CJK RADICAL C-SIMPLIFIED TURTLE +← (‎ 龟 ‎) 9F9F CJK UNIFIED IDEOGRAPH-9F9F + +# 乙 ㇠ ⼄ + (‎ ⼄ ‎) 2F04 KANGXI RADICAL SECOND +← (‎ 乙 ‎) 4E59 CJK UNIFIED IDEOGRAPH-4E59 +← (‎ ㇠ ‎) 31E0 CJK STROKE HXWG + +# 亅 ㇚ ⼅ + (‎ ⼅ ‎) 2F05 KANGXI RADICAL HOOK +← (‎ 亅 ‎) 4E85 CJK UNIFIED IDEOGRAPH-4E85 +← (‎ ㇚ ‎) 31DA CJK STROKE SG + +# 二 ニ ⼆ + (‎ ⼆ ‎) 2F06 KANGXI RADICAL TWO +← (‎ 二 ‎) 4E8C CJK UNIFIED IDEOGRAPH-4E8C +← (‎ ニ ‎) 30CB KATAKANA LETTER NI # →二→ + +# 亠 ⼇ + (‎ ⼇ ‎) 2F07 KANGXI RADICAL LID +← (‎ 亠 ‎) 4EA0 CJK UNIFIED IDEOGRAPH-4EA0 + +# 人 ⼈ + (‎ ⼈ ‎) 2F08 KANGXI RADICAL MAN +← (‎ 人 ‎) 4EBA CJK UNIFIED IDEOGRAPH-4EBA + +# 儿 ⼉ + (‎ ⼉ ‎) 2F09 KANGXI RADICAL LEGS +← (‎ 儿 ‎) 513F CJK UNIFIED IDEOGRAPH-513F + +# 入 ⼊ + (‎ ⼊ ‎) 2F0A KANGXI RADICAL ENTER +← (‎ 入 ‎) 5165 CJK UNIFIED IDEOGRAPH-5165 + +# 八 ハ ⼋ + (‎ ⼋ ‎) 2F0B KANGXI RADICAL EIGHT +← (‎ 八 ‎) 516B CJK UNIFIED IDEOGRAPH-516B +← (‎ ハ ‎) 30CF KATAKANA LETTER HA # →八→ + +# 冂 ⼌ + (‎ ⼌ ‎) 2F0C KANGXI RADICAL DOWN BOX +← (‎ 冂 ‎) 5182 CJK UNIFIED IDEOGRAPH-5182 + +# 冖 ⼍ + (‎ ⼍ ‎) 2F0D KANGXI RADICAL COVER +← (‎ 冖 ‎) 5196 CJK UNIFIED IDEOGRAPH-5196 + +# 冫 ⼎ + (‎ ⼎ ‎) 2F0E KANGXI RADICAL ICE +← (‎ 冫 ‎) 51AB CJK UNIFIED IDEOGRAPH-51AB + +# 几 ⼏ + (‎ ⼏ ‎) 2F0F KANGXI RADICAL TABLE +← (‎ 几 ‎) 51E0 CJK UNIFIED IDEOGRAPH-51E0 + +# 凵 凵 ⼐ + (‎ ⼐ ‎) 2F10 KANGXI RADICAL OPEN BOX +← (‎ 凵 ‎) 51F5 CJK UNIFIED IDEOGRAPH-51F5 +← (‎ 凵 ‎) 2F81D CJK COMPATIBILITY IDEOGRAPH-2F81D # →凵→ + +# 刀 ⼑ + (‎ ⼑ ‎) 2F11 KANGXI RADICAL KNIFE +← (‎ 刀 ‎) 5200 CJK UNIFIED IDEOGRAPH-5200 + +# 力 カ 力 ⼒ + (‎ ⼒ ‎) 2F12 KANGXI RADICAL POWER +← (‎ 力 ‎) 529B CJK UNIFIED IDEOGRAPH-529B +← (‎ カ ‎) 30AB KATAKANA LETTER KA +← (‎ 力 ‎) F98A CJK COMPATIBILITY IDEOGRAPH-F98A # →力→ + +# 勹 ⼓ + (‎ ⼓ ‎) 2F13 KANGXI RADICAL WRAP +← (‎ 勹 ‎) 52F9 CJK UNIFIED IDEOGRAPH-52F9 + +# 匕 ⼔ + (‎ ⼔ ‎) 2F14 KANGXI RADICAL SPOON +← (‎ 匕 ‎) 5315 CJK UNIFIED IDEOGRAPH-5315 + +# 匚 ⼕ + (‎ ⼕ ‎) 2F15 KANGXI RADICAL RIGHT OPEN BOX +← (‎ 匚 ‎) 531A CJK UNIFIED IDEOGRAPH-531A + +# 匸 ⼖ + (‎ ⼖ ‎) 2F16 KANGXI RADICAL HIDING ENCLOSURE +← (‎ 匸 ‎) 5338 CJK UNIFIED IDEOGRAPH-5338 + +# 十 〸 ⼗ + (‎ ⼗ ‎) 2F17 KANGXI RADICAL TEN +← (‎ 十 ‎) 5341 CJK UNIFIED IDEOGRAPH-5341 +← (‎ 〸 ‎) 3038 HANGZHOU NUMERAL TEN # →十→ + +# 卜 ト ⼘ + (‎ ⼘ ‎) 2F18 KANGXI RADICAL DIVINATION +← (‎ 卜 ‎) 535C CJK UNIFIED IDEOGRAPH-535C +← (‎ ト ‎) 30C8 KATAKANA LETTER TO + +# 卩 ⼙ + (‎ ⼙ ‎) 2F19 KANGXI RADICAL SEAL +← (‎ 卩 ‎) 5369 CJK UNIFIED IDEOGRAPH-5369 + +# 厂 ⼚ + (‎ ⼚ ‎) 2F1A KANGXI RADICAL CLIFF +← (‎ 厂 ‎) 5382 CJK UNIFIED IDEOGRAPH-5382 + +# 厶 ⼛ + (‎ ⼛ ‎) 2F1B KANGXI RADICAL PRIVATE +← (‎ 厶 ‎) 53B6 CJK UNIFIED IDEOGRAPH-53B6 + +# 又 ⼜ + (‎ ⼜ ‎) 2F1C KANGXI RADICAL AGAIN +← (‎ 又 ‎) 53C8 CJK UNIFIED IDEOGRAPH-53C8 + +# 口 ロ 囗 ⼝ ⼞ + (‎ ⼝ ‎) 2F1D KANGXI RADICAL MOUTH +← (‎ 口 ‎) 53E3 CJK UNIFIED IDEOGRAPH-53E3 +← (‎ ロ ‎) 30ED KATAKANA LETTER RO # →⼞→ +← (‎ 囗 ‎) 56D7 CJK UNIFIED IDEOGRAPH-56D7 # →⼞→ +← (‎ ⼞ ‎) 2F1E KANGXI RADICAL ENCLOSURE + +# 土 士 ⼟ ⼠ + (‎ ⼟ ‎) 2F1F KANGXI RADICAL EARTH +← (‎ 土 ‎) 571F CJK UNIFIED IDEOGRAPH-571F +← (‎ 士 ‎) 58EB CJK UNIFIED IDEOGRAPH-58EB # →⼠→ +← (‎ ⼠ ‎) 2F20 KANGXI RADICAL SCHOLAR + +# 夂 ⼡ + (‎ ⼡ ‎) 2F21 KANGXI RADICAL GO +← (‎ 夂 ‎) 5902 CJK UNIFIED IDEOGRAPH-5902 + +# 夊 ⼢ + (‎ ⼢ ‎) 2F22 KANGXI RADICAL GO SLOWLY +← (‎ 夊 ‎) 590A CJK UNIFIED IDEOGRAPH-590A + +# 夕 タ ⼣ + (‎ ⼣ ‎) 2F23 KANGXI RADICAL EVENING +← (‎ 夕 ‎) 5915 CJK UNIFIED IDEOGRAPH-5915 +← (‎ タ ‎) 30BF KATAKANA LETTER TA + +# 大 ⼤ + (‎ ⼤ ‎) 2F24 KANGXI RADICAL BIG +← (‎ 大 ‎) 5927 CJK UNIFIED IDEOGRAPH-5927 + +# 女 女 ⼥ + (‎ ⼥ ‎) 2F25 KANGXI RADICAL WOMAN +← (‎ 女 ‎) 5973 CJK UNIFIED IDEOGRAPH-5973 +← (‎ 女 ‎) F981 CJK COMPATIBILITY IDEOGRAPH-F981 # →女→ + +# 子 ⼦ + (‎ ⼦ ‎) 2F26 KANGXI RADICAL CHILD +← (‎ 子 ‎) 5B50 CJK UNIFIED IDEOGRAPH-5B50 + +# 宀 ⼧ + (‎ ⼧ ‎) 2F27 KANGXI RADICAL ROOF +← (‎ 宀 ‎) 5B80 CJK UNIFIED IDEOGRAPH-5B80 + +# 寸 ⼨ + (‎ ⼨ ‎) 2F28 KANGXI RADICAL INCH +← (‎ 寸 ‎) 5BF8 CJK UNIFIED IDEOGRAPH-5BF8 + +# 小 ⼩ + (‎ ⼩ ‎) 2F29 KANGXI RADICAL SMALL +← (‎ 小 ‎) 5C0F CJK UNIFIED IDEOGRAPH-5C0F + +# 尸 ⼫ + (‎ ⼫ ‎) 2F2B KANGXI RADICAL CORPSE +← (‎ 尸 ‎) 5C38 CJK UNIFIED IDEOGRAPH-5C38 + +# 屮 屮 屮 ⼬ + (‎ ⼬ ‎) 2F2C KANGXI RADICAL SPROUT +← (‎ 屮 ‎) 5C6E CJK UNIFIED IDEOGRAPH-5C6E +← (‎ 屮 ‎) FA3C CJK COMPATIBILITY IDEOGRAPH-FA3C # →屮→ +← (‎ 屮 ‎) 2F878 CJK COMPATIBILITY IDEOGRAPH-2F878 # →屮→ + +# 山 ⼭ + (‎ ⼭ ‎) 2F2D KANGXI RADICAL MOUNTAIN +← (‎ 山 ‎) 5C71 CJK UNIFIED IDEOGRAPH-5C71 + +# 巛 ⼮ + (‎ ⼮ ‎) 2F2E KANGXI RADICAL RIVER +← (‎ 巛 ‎) 5DDB CJK UNIFIED IDEOGRAPH-5DDB + +# 工 エ ⼯ + (‎ ⼯ ‎) 2F2F KANGXI RADICAL WORK +← (‎ 工 ‎) 5DE5 CJK UNIFIED IDEOGRAPH-5DE5 +← (‎ エ ‎) 30A8 KATAKANA LETTER E + +# 己 ⼰ + (‎ ⼰ ‎) 2F30 KANGXI RADICAL ONESELF +← (‎ 己 ‎) 5DF1 CJK UNIFIED IDEOGRAPH-5DF1 + +# 巾 ⼱ + (‎ ⼱ ‎) 2F31 KANGXI RADICAL TURBAN +← (‎ 巾 ‎) 5DFE CJK UNIFIED IDEOGRAPH-5DFE + +# 干 ⼲ + (‎ ⼲ ‎) 2F32 KANGXI RADICAL DRY +← (‎ 干 ‎) 5E72 CJK UNIFIED IDEOGRAPH-5E72 + +# 广 ⼴ + (‎ ⼴ ‎) 2F34 KANGXI RADICAL DOTTED CLIFF +← (‎ 广 ‎) 5E7F CJK UNIFIED IDEOGRAPH-5E7F + +# 廴 ⼵ + (‎ ⼵ ‎) 2F35 KANGXI RADICAL LONG STRIDE +← (‎ 廴 ‎) 5EF4 CJK UNIFIED IDEOGRAPH-5EF4 + +# 廾 廾 ⼶ + (‎ ⼶ ‎) 2F36 KANGXI RADICAL TWO HANDS +← (‎ 廾 ‎) 5EFE CJK UNIFIED IDEOGRAPH-5EFE +← (‎ 廾 ‎) 2F890 CJK COMPATIBILITY IDEOGRAPH-2F890 # →廾→ + +# 弋 ⼷ + (‎ ⼷ ‎) 2F37 KANGXI RADICAL SHOOT +← (‎ 弋 ‎) 5F0B CJK UNIFIED IDEOGRAPH-5F0B + +# 弓 ⼸ + (‎ ⼸ ‎) 2F38 KANGXI RADICAL BOW +← (‎ 弓 ‎) 5F13 CJK UNIFIED IDEOGRAPH-5F13 + +# 彐 ⼹ + (‎ ⼹ ‎) 2F39 KANGXI RADICAL SNOUT +← (‎ 彐 ‎) 5F50 CJK UNIFIED IDEOGRAPH-5F50 + +# 彡 ⼺ + (‎ ⼺ ‎) 2F3A KANGXI RADICAL BRISTLE +← (‎ 彡 ‎) 5F61 CJK UNIFIED IDEOGRAPH-5F61 + +# 彳 ⼻ + (‎ ⼻ ‎) 2F3B KANGXI RADICAL STEP +← (‎ 彳 ‎) 5F73 CJK UNIFIED IDEOGRAPH-5F73 + +# 心 ⼼ + (‎ ⼼ ‎) 2F3C KANGXI RADICAL HEART +← (‎ 心 ‎) 5FC3 CJK UNIFIED IDEOGRAPH-5FC3 + +# 戈 ⼽ + (‎ ⼽ ‎) 2F3D KANGXI RADICAL HALBERD +← (‎ 戈 ‎) 6208 CJK UNIFIED IDEOGRAPH-6208 + +# 戶 戸 ⼾ + (‎ ⼾ ‎) 2F3E KANGXI RADICAL DOOR +← (‎ 戶 ‎) 6236 CJK UNIFIED IDEOGRAPH-6236 +← (‎ 戸 ‎) 6238 CJK UNIFIED IDEOGRAPH-6238 + +# 手 ⼿ + (‎ ⼿ ‎) 2F3F KANGXI RADICAL HAND +← (‎ 手 ‎) 624B CJK UNIFIED IDEOGRAPH-624B + +# 支 ⽀ + (‎ ⽀ ‎) 2F40 KANGXI RADICAL BRANCH +← (‎ 支 ‎) 652F CJK UNIFIED IDEOGRAPH-652F + +# 攴 ⽁ + (‎ ⽁ ‎) 2F41 KANGXI RADICAL RAP +← (‎ 攴 ‎) 6534 CJK UNIFIED IDEOGRAPH-6534 + +# 文 ⽂ + (‎ ⽂ ‎) 2F42 KANGXI RADICAL SCRIPT +← (‎ 文 ‎) 6587 CJK UNIFIED IDEOGRAPH-6587 + +# 斗 ⽃ + (‎ ⽃ ‎) 2F43 KANGXI RADICAL DIPPER +← (‎ 斗 ‎) 6597 CJK UNIFIED IDEOGRAPH-6597 + +# 斤 ⽄ + (‎ ⽄ ‎) 2F44 KANGXI RADICAL AXE +← (‎ 斤 ‎) 65A4 CJK UNIFIED IDEOGRAPH-65A4 + +# 方 ⽅ + (‎ ⽅ ‎) 2F45 KANGXI RADICAL SQUARE +← (‎ 方 ‎) 65B9 CJK UNIFIED IDEOGRAPH-65B9 + +# 无 ⽆ + (‎ ⽆ ‎) 2F46 KANGXI RADICAL NOT +← (‎ 无 ‎) 65E0 CJK UNIFIED IDEOGRAPH-65E0 + +# 日 ⽇ + (‎ ⽇ ‎) 2F47 KANGXI RADICAL SUN +← (‎ 日 ‎) 65E5 CJK UNIFIED IDEOGRAPH-65E5 + +# 曰 ⽈ + (‎ ⽈ ‎) 2F48 KANGXI RADICAL SAY +← (‎ 曰 ‎) 66F0 CJK UNIFIED IDEOGRAPH-66F0 + +# 月 ⽉ + (‎ ⽉ ‎) 2F49 KANGXI RADICAL MOON +← (‎ 月 ‎) 6708 CJK UNIFIED IDEOGRAPH-6708 + +# 木 ⽊ + (‎ ⽊ ‎) 2F4A KANGXI RADICAL TREE +← (‎ 木 ‎) 6728 CJK UNIFIED IDEOGRAPH-6728 + +# 欠 ⽋ + (‎ ⽋ ‎) 2F4B KANGXI RADICAL LACK +← (‎ 欠 ‎) 6B20 CJK UNIFIED IDEOGRAPH-6B20 + +# 止 ⽌ + (‎ ⽌ ‎) 2F4C KANGXI RADICAL STOP +← (‎ 止 ‎) 6B62 CJK UNIFIED IDEOGRAPH-6B62 + +# 歹 歹 ⽍ + (‎ ⽍ ‎) 2F4D KANGXI RADICAL DEATH +← (‎ 歹 ‎) 6B79 CJK UNIFIED IDEOGRAPH-6B79 +← (‎ 歹 ‎) FA95 CJK COMPATIBILITY IDEOGRAPH-FA95 # →歹→ + +# 殳 ⽎ + (‎ ⽎ ‎) 2F4E KANGXI RADICAL WEAPON +← (‎ 殳 ‎) 6BB3 CJK UNIFIED IDEOGRAPH-6BB3 + +# 毋 ⽏ + (‎ ⽏ ‎) 2F4F KANGXI RADICAL DO NOT +← (‎ 毋 ‎) 6BCB CJK UNIFIED IDEOGRAPH-6BCB + +# 比 ⽐ + (‎ ⽐ ‎) 2F50 KANGXI RADICAL COMPARE +← (‎ 比 ‎) 6BD4 CJK UNIFIED IDEOGRAPH-6BD4 + +# 毛 ⽑ + (‎ ⽑ ‎) 2F51 KANGXI RADICAL FUR +← (‎ 毛 ‎) 6BDB CJK UNIFIED IDEOGRAPH-6BDB + +# 氏 ⽒ + (‎ ⽒ ‎) 2F52 KANGXI RADICAL CLAN +← (‎ 氏 ‎) 6C0F CJK UNIFIED IDEOGRAPH-6C0F + +# 气 ⽓ + (‎ ⽓ ‎) 2F53 KANGXI RADICAL STEAM +← (‎ 气 ‎) 6C14 CJK UNIFIED IDEOGRAPH-6C14 + +# 水 ⽔ + (‎ ⽔ ‎) 2F54 KANGXI RADICAL WATER +← (‎ 水 ‎) 6C34 CJK UNIFIED IDEOGRAPH-6C34 + +# 火 ⽕ + (‎ ⽕ ‎) 2F55 KANGXI RADICAL FIRE +← (‎ 火 ‎) 706B CJK UNIFIED IDEOGRAPH-706B + +# 爪 ⽖ + (‎ ⽖ ‎) 2F56 KANGXI RADICAL CLAW +← (‎ 爪 ‎) 722A CJK UNIFIED IDEOGRAPH-722A + +# 父 ⽗ + (‎ ⽗ ‎) 2F57 KANGXI RADICAL FATHER +← (‎ 父 ‎) 7236 CJK UNIFIED IDEOGRAPH-7236 + +# 爻 ⽘ + (‎ ⽘ ‎) 2F58 KANGXI RADICAL DOUBLE X +← (‎ 爻 ‎) 723B CJK UNIFIED IDEOGRAPH-723B + +# 爿 ⽙ + (‎ ⽙ ‎) 2F59 KANGXI RADICAL HALF TREE TRUNK +← (‎ 爿 ‎) 723F CJK UNIFIED IDEOGRAPH-723F + +# 片 ⽚ + (‎ ⽚ ‎) 2F5A KANGXI RADICAL SLICE +← (‎ 片 ‎) 7247 CJK UNIFIED IDEOGRAPH-7247 + +# 牙 ⽛ + (‎ ⽛ ‎) 2F5B KANGXI RADICAL FANG +← (‎ 牙 ‎) 7259 CJK UNIFIED IDEOGRAPH-7259 + +# 牛 ⽜ + (‎ ⽜ ‎) 2F5C KANGXI RADICAL COW +← (‎ 牛 ‎) 725B CJK UNIFIED IDEOGRAPH-725B + +# 犬 ⽝ + (‎ ⽝ ‎) 2F5D KANGXI RADICAL DOG +← (‎ 犬 ‎) 72AC CJK UNIFIED IDEOGRAPH-72AC + +# 玄 ⽞ + (‎ ⽞ ‎) 2F5E KANGXI RADICAL PROFOUND +← (‎ 玄 ‎) 7384 CJK UNIFIED IDEOGRAPH-7384 + +# 玉 ⽟ + (‎ ⽟ ‎) 2F5F KANGXI RADICAL JADE +← (‎ 玉 ‎) 7389 CJK UNIFIED IDEOGRAPH-7389 + +# 瓜 ⽠ + (‎ ⽠ ‎) 2F60 KANGXI RADICAL MELON +← (‎ 瓜 ‎) 74DC CJK UNIFIED IDEOGRAPH-74DC + +# 瓦 ⽡ + (‎ ⽡ ‎) 2F61 KANGXI RADICAL TILE +← (‎ 瓦 ‎) 74E6 CJK UNIFIED IDEOGRAPH-74E6 + +# 甘 ⽢ + (‎ ⽢ ‎) 2F62 KANGXI RADICAL SWEET +← (‎ 甘 ‎) 7518 CJK UNIFIED IDEOGRAPH-7518 + +# 生 ⽣ + (‎ ⽣ ‎) 2F63 KANGXI RADICAL LIFE +← (‎ 生 ‎) 751F CJK UNIFIED IDEOGRAPH-751F + +# 用 ⽤ + (‎ ⽤ ‎) 2F64 KANGXI RADICAL USE +← (‎ 用 ‎) 7528 CJK UNIFIED IDEOGRAPH-7528 + +# 田 ⽥ + (‎ ⽥ ‎) 2F65 KANGXI RADICAL FIELD +← (‎ 田 ‎) 7530 CJK UNIFIED IDEOGRAPH-7530 + +# 疋 ⽦ + (‎ ⽦ ‎) 2F66 KANGXI RADICAL BOLT OF CLOTH +← (‎ 疋 ‎) 758B CJK UNIFIED IDEOGRAPH-758B + +# 疒 ⽧ + (‎ ⽧ ‎) 2F67 KANGXI RADICAL SICKNESS +← (‎ 疒 ‎) 7592 CJK UNIFIED IDEOGRAPH-7592 + +# 癶 ⽨ + (‎ ⽨ ‎) 2F68 KANGXI RADICAL DOTTED TENT +← (‎ 癶 ‎) 7676 CJK UNIFIED IDEOGRAPH-7676 + +# 白 ⽩ + (‎ ⽩ ‎) 2F69 KANGXI RADICAL WHITE +← (‎ 白 ‎) 767D CJK UNIFIED IDEOGRAPH-767D + +# 皮 ⽪ + (‎ ⽪ ‎) 2F6A KANGXI RADICAL SKIN +← (‎ 皮 ‎) 76AE CJK UNIFIED IDEOGRAPH-76AE + +# 皿 ⽫ + (‎ ⽫ ‎) 2F6B KANGXI RADICAL DISH +← (‎ 皿 ‎) 76BF CJK UNIFIED IDEOGRAPH-76BF + +# 目 ⽬ + (‎ ⽬ ‎) 2F6C KANGXI RADICAL EYE +← (‎ 目 ‎) 76EE CJK UNIFIED IDEOGRAPH-76EE + +# 矛 ⽭ + (‎ ⽭ ‎) 2F6D KANGXI RADICAL SPEAR +← (‎ 矛 ‎) 77DB CJK UNIFIED IDEOGRAPH-77DB + +# 矢 ⽮ + (‎ ⽮ ‎) 2F6E KANGXI RADICAL ARROW +← (‎ 矢 ‎) 77E2 CJK UNIFIED IDEOGRAPH-77E2 + +# 石 ⽯ + (‎ ⽯ ‎) 2F6F KANGXI RADICAL STONE +← (‎ 石 ‎) 77F3 CJK UNIFIED IDEOGRAPH-77F3 + +# 示 ⽰ + (‎ ⽰ ‎) 2F70 KANGXI RADICAL SPIRIT +← (‎ 示 ‎) 793A CJK UNIFIED IDEOGRAPH-793A + +# 禸 ⽱ + (‎ ⽱ ‎) 2F71 KANGXI RADICAL TRACK +← (‎ 禸 ‎) 79B8 CJK UNIFIED IDEOGRAPH-79B8 + +# 禾 ⽲ + (‎ ⽲ ‎) 2F72 KANGXI RADICAL GRAIN +← (‎ 禾 ‎) 79BE CJK UNIFIED IDEOGRAPH-79BE + +# 穴 ⽳ + (‎ ⽳ ‎) 2F73 KANGXI RADICAL CAVE +← (‎ 穴 ‎) 7A74 CJK UNIFIED IDEOGRAPH-7A74 + +# 立 立 ⽴ + (‎ ⽴ ‎) 2F74 KANGXI RADICAL STAND +← (‎ 立 ‎) 7ACB CJK UNIFIED IDEOGRAPH-7ACB +← (‎ 立 ‎) F9F7 CJK COMPATIBILITY IDEOGRAPH-F9F7 # →立→ + +# 竹 ⽵ + (‎ ⽵ ‎) 2F75 KANGXI RADICAL BAMBOO +← (‎ 竹 ‎) 7AF9 CJK UNIFIED IDEOGRAPH-7AF9 + +# 米 ⽶ + (‎ ⽶ ‎) 2F76 KANGXI RADICAL RICE +← (‎ 米 ‎) 7C73 CJK UNIFIED IDEOGRAPH-7C73 + +# 糸 ⽷ + (‎ ⽷ ‎) 2F77 KANGXI RADICAL SILK +← (‎ 糸 ‎) 7CF8 CJK UNIFIED IDEOGRAPH-7CF8 + +# 缶 ⽸ + (‎ ⽸ ‎) 2F78 KANGXI RADICAL JAR +← (‎ 缶 ‎) 7F36 CJK UNIFIED IDEOGRAPH-7F36 + +# 网 ⽹ + (‎ ⽹ ‎) 2F79 KANGXI RADICAL NET +← (‎ 网 ‎) 7F51 CJK UNIFIED IDEOGRAPH-7F51 + +# 羊 ⽺ + (‎ ⽺ ‎) 2F7A KANGXI RADICAL SHEEP +← (‎ 羊 ‎) 7F8A CJK UNIFIED IDEOGRAPH-7F8A + +# 羽 羽 ⽻ + (‎ ⽻ ‎) 2F7B KANGXI RADICAL FEATHER +← (‎ 羽 ‎) 7FBD CJK UNIFIED IDEOGRAPH-7FBD +← (‎ 羽 ‎) FA1E CJK COMPATIBILITY IDEOGRAPH-FA1E # →羽→ + +# 老 老 ⽼ + (‎ ⽼ ‎) 2F7C KANGXI RADICAL OLD +← (‎ 老 ‎) 8001 CJK UNIFIED IDEOGRAPH-8001 +← (‎ 老 ‎) F934 CJK COMPATIBILITY IDEOGRAPH-F934 # →老→ + +# 而 ⽽ + (‎ ⽽ ‎) 2F7D KANGXI RADICAL AND +← (‎ 而 ‎) 800C CJK UNIFIED IDEOGRAPH-800C + +# 耒 ⽾ + (‎ ⽾ ‎) 2F7E KANGXI RADICAL PLOW +← (‎ 耒 ‎) 8012 CJK UNIFIED IDEOGRAPH-8012 + +# 耳 ⽿ + (‎ ⽿ ‎) 2F7F KANGXI RADICAL EAR +← (‎ 耳 ‎) 8033 CJK UNIFIED IDEOGRAPH-8033 + +# 聿 ⾀ + (‎ ⾀ ‎) 2F80 KANGXI RADICAL BRUSH +← (‎ 聿 ‎) 807F CJK UNIFIED IDEOGRAPH-807F + +# 肉 ⾁ + (‎ ⾁ ‎) 2F81 KANGXI RADICAL MEAT +← (‎ 肉 ‎) 8089 CJK UNIFIED IDEOGRAPH-8089 + +# 臣 ⾂ + (‎ ⾂ ‎) 2F82 KANGXI RADICAL MINISTER +← (‎ 臣 ‎) 81E3 CJK UNIFIED IDEOGRAPH-81E3 + +# 自 ⾃ + (‎ ⾃ ‎) 2F83 KANGXI RADICAL SELF +← (‎ 自 ‎) 81EA CJK UNIFIED IDEOGRAPH-81EA + +# 至 ⾄ + (‎ ⾄ ‎) 2F84 KANGXI RADICAL ARRIVE +← (‎ 至 ‎) 81F3 CJK UNIFIED IDEOGRAPH-81F3 + +# 臼 ⾅ + (‎ ⾅ ‎) 2F85 KANGXI RADICAL MORTAR +← (‎ 臼 ‎) 81FC CJK UNIFIED IDEOGRAPH-81FC + +# 舌 ⾆ + (‎ ⾆ ‎) 2F86 KANGXI RADICAL TONGUE +← (‎ 舌 ‎) 820C CJK UNIFIED IDEOGRAPH-820C + +# 舛 ⾇ + (‎ ⾇ ‎) 2F87 KANGXI RADICAL OPPOSE +← (‎ 舛 ‎) 821B CJK UNIFIED IDEOGRAPH-821B + +# 舟 ⾈ + (‎ ⾈ ‎) 2F88 KANGXI RADICAL BOAT +← (‎ 舟 ‎) 821F CJK UNIFIED IDEOGRAPH-821F + +# 艮 ⾉ + (‎ ⾉ ‎) 2F89 KANGXI RADICAL STOPPING +← (‎ 艮 ‎) 826E CJK UNIFIED IDEOGRAPH-826E + +# 色 ⾊ + (‎ ⾊ ‎) 2F8A KANGXI RADICAL COLOR +← (‎ 色 ‎) 8272 CJK UNIFIED IDEOGRAPH-8272 + +# 艸 ⾋ + (‎ ⾋ ‎) 2F8B KANGXI RADICAL GRASS +← (‎ 艸 ‎) 8278 CJK UNIFIED IDEOGRAPH-8278 + +# 虍 ⾌ + (‎ ⾌ ‎) 2F8C KANGXI RADICAL TIGER +← (‎ 虍 ‎) 864D CJK UNIFIED IDEOGRAPH-864D + +# 虫 ⾍ + (‎ ⾍ ‎) 2F8D KANGXI RADICAL INSECT +← (‎ 虫 ‎) 866B CJK UNIFIED IDEOGRAPH-866B + +# 血 ⾎ + (‎ ⾎ ‎) 2F8E KANGXI RADICAL BLOOD +← (‎ 血 ‎) 8840 CJK UNIFIED IDEOGRAPH-8840 + +# 行 行 ⾏ + (‎ ⾏ ‎) 2F8F KANGXI RADICAL WALK ENCLOSURE +← (‎ 行 ‎) 884C CJK UNIFIED IDEOGRAPH-884C +← (‎ 行 ‎) FA08 CJK COMPATIBILITY IDEOGRAPH-FA08 # →行→ + +# 衣 衣 ⾐ + (‎ ⾐ ‎) 2F90 KANGXI RADICAL CLOTHES +← (‎ 衣 ‎) 8863 CJK UNIFIED IDEOGRAPH-8863 +← (‎ 衣 ‎) 2F9C4 CJK COMPATIBILITY IDEOGRAPH-2F9C4 # →衣→ + +# 襾 ⾑ + (‎ ⾑ ‎) 2F91 KANGXI RADICAL WEST +← (‎ 襾 ‎) 897E CJK UNIFIED IDEOGRAPH-897E + +# 見 見 ⾒ + (‎ ⾒ ‎) 2F92 KANGXI RADICAL SEE +← (‎ 見 ‎) 898B CJK UNIFIED IDEOGRAPH-898B +← (‎ 見 ‎) FA0A CJK COMPATIBILITY IDEOGRAPH-FA0A # →見→ + +# 角 ⾓ + (‎ ⾓ ‎) 2F93 KANGXI RADICAL HORN +← (‎ 角 ‎) 89D2 CJK UNIFIED IDEOGRAPH-89D2 + +# 言 ⾔ + (‎ ⾔ ‎) 2F94 KANGXI RADICAL SPEECH +← (‎ 言 ‎) 8A00 CJK UNIFIED IDEOGRAPH-8A00 + +# 谷 ⾕ + (‎ ⾕ ‎) 2F95 KANGXI RADICAL VALLEY +← (‎ 谷 ‎) 8C37 CJK UNIFIED IDEOGRAPH-8C37 + +# 豆 ⾖ + (‎ ⾖ ‎) 2F96 KANGXI RADICAL BEAN +← (‎ 豆 ‎) 8C46 CJK UNIFIED IDEOGRAPH-8C46 + +# 豕 豕 ⾗ + (‎ ⾗ ‎) 2F97 KANGXI RADICAL PIG +← (‎ 豕 ‎) 8C55 CJK UNIFIED IDEOGRAPH-8C55 +← (‎ 豕 ‎) 2F9D2 CJK COMPATIBILITY IDEOGRAPH-2F9D2 # →豕→ + +# 豸 ⾘ + (‎ ⾘ ‎) 2F98 KANGXI RADICAL BADGER +← (‎ 豸 ‎) 8C78 CJK UNIFIED IDEOGRAPH-8C78 + +# 貝 ⾙ + (‎ ⾙ ‎) 2F99 KANGXI RADICAL SHELL +← (‎ 貝 ‎) 8C9D CJK UNIFIED IDEOGRAPH-8C9D + +# 赤 ⾚ + (‎ ⾚ ‎) 2F9A KANGXI RADICAL RED +← (‎ 赤 ‎) 8D64 CJK UNIFIED IDEOGRAPH-8D64 + +# 走 ⾛ + (‎ ⾛ ‎) 2F9B KANGXI RADICAL RUN +← (‎ 走 ‎) 8D70 CJK UNIFIED IDEOGRAPH-8D70 + +# 足 ⾜ + (‎ ⾜ ‎) 2F9C KANGXI RADICAL FOOT +← (‎ 足 ‎) 8DB3 CJK UNIFIED IDEOGRAPH-8DB3 + +# 身 ⾝ + (‎ ⾝ ‎) 2F9D KANGXI RADICAL BODY +← (‎ 身 ‎) 8EAB CJK UNIFIED IDEOGRAPH-8EAB + +# 車 車 ⾞ + (‎ ⾞ ‎) 2F9E KANGXI RADICAL CART +← (‎ 車 ‎) 8ECA CJK UNIFIED IDEOGRAPH-8ECA +← (‎ 車 ‎) F902 CJK COMPATIBILITY IDEOGRAPH-F902 # →車→ + +# 辛 ⾟ + (‎ ⾟ ‎) 2F9F KANGXI RADICAL BITTER +← (‎ 辛 ‎) 8F9B CJK UNIFIED IDEOGRAPH-8F9B + +# 辰 辰 ⾠ + (‎ ⾠ ‎) 2FA0 KANGXI RADICAL MORNING +← (‎ 辰 ‎) 8FB0 CJK UNIFIED IDEOGRAPH-8FB0 +← (‎ 辰 ‎) F971 CJK COMPATIBILITY IDEOGRAPH-F971 # →辰→ + +# 辵 ⾡ + (‎ ⾡ ‎) 2FA1 KANGXI RADICAL WALK +← (‎ 辵 ‎) 8FB5 CJK UNIFIED IDEOGRAPH-8FB5 + +# 邑 ⾢ + (‎ ⾢ ‎) 2FA2 KANGXI RADICAL CITY +← (‎ 邑 ‎) 9091 CJK UNIFIED IDEOGRAPH-9091 + +# 酉 ⾣ + (‎ ⾣ ‎) 2FA3 KANGXI RADICAL WINE +← (‎ 酉 ‎) 9149 CJK UNIFIED IDEOGRAPH-9149 + +# 釆 ⾤ + (‎ ⾤ ‎) 2FA4 KANGXI RADICAL DISTINGUISH +← (‎ 釆 ‎) 91C6 CJK UNIFIED IDEOGRAPH-91C6 + +# 里 里 ⾥ + (‎ ⾥ ‎) 2FA5 KANGXI RADICAL VILLAGE +← (‎ 里 ‎) 91CC CJK UNIFIED IDEOGRAPH-91CC +← (‎ 里 ‎) F9E9 CJK COMPATIBILITY IDEOGRAPH-F9E9 # →里→ + +# 金 金 ⾦ + (‎ ⾦ ‎) 2FA6 KANGXI RADICAL GOLD +← (‎ 金 ‎) 91D1 CJK UNIFIED IDEOGRAPH-91D1 +← (‎ 金 ‎) F90A CJK COMPATIBILITY IDEOGRAPH-F90A # →金→ + +# 門 ⾨ + (‎ ⾨ ‎) 2FA8 KANGXI RADICAL GATE +← (‎ 門 ‎) 9580 CJK UNIFIED IDEOGRAPH-9580 + +# 阜 ⾩ + (‎ ⾩ ‎) 2FA9 KANGXI RADICAL MOUND +← (‎ 阜 ‎) 961C CJK UNIFIED IDEOGRAPH-961C + +# 隶 ⾪ + (‎ ⾪ ‎) 2FAA KANGXI RADICAL SLAVE +← (‎ 隶 ‎) 96B6 CJK UNIFIED IDEOGRAPH-96B6 + +# 隹 ⾫ + (‎ ⾫ ‎) 2FAB KANGXI RADICAL SHORT TAILED BIRD +← (‎ 隹 ‎) 96B9 CJK UNIFIED IDEOGRAPH-96B9 + +# 雨 ⾬ + (‎ ⾬ ‎) 2FAC KANGXI RADICAL RAIN +← (‎ 雨 ‎) 96E8 CJK UNIFIED IDEOGRAPH-96E8 + +# 靑 ⾭ + (‎ ⾭ ‎) 2FAD KANGXI RADICAL BLUE +← (‎ 靑 ‎) 9751 CJK UNIFIED IDEOGRAPH-9751 + +# 非 ⾮ + (‎ ⾮ ‎) 2FAE KANGXI RADICAL WRONG +← (‎ 非 ‎) 975E CJK UNIFIED IDEOGRAPH-975E + +# 面 ⾯ + (‎ ⾯ ‎) 2FAF KANGXI RADICAL FACE +← (‎ 面 ‎) 9762 CJK UNIFIED IDEOGRAPH-9762 + +# 革 ⾰ + (‎ ⾰ ‎) 2FB0 KANGXI RADICAL LEATHER +← (‎ 革 ‎) 9769 CJK UNIFIED IDEOGRAPH-9769 + +# 韋 ⾱ + (‎ ⾱ ‎) 2FB1 KANGXI RADICAL TANNED LEATHER +← (‎ 韋 ‎) 97CB CJK UNIFIED IDEOGRAPH-97CB + +# 韭 ⾲ + (‎ ⾲ ‎) 2FB2 KANGXI RADICAL LEEK +← (‎ 韭 ‎) 97ED CJK UNIFIED IDEOGRAPH-97ED + +# 音 ⾳ + (‎ ⾳ ‎) 2FB3 KANGXI RADICAL SOUND +← (‎ 音 ‎) 97F3 CJK UNIFIED IDEOGRAPH-97F3 + +# 頁 ⾴ + (‎ ⾴ ‎) 2FB4 KANGXI RADICAL LEAF +← (‎ 頁 ‎) 9801 CJK UNIFIED IDEOGRAPH-9801 + +# 風 ⾵ + (‎ ⾵ ‎) 2FB5 KANGXI RADICAL WIND +← (‎ 風 ‎) 98A8 CJK UNIFIED IDEOGRAPH-98A8 + +# 飛 ⾶ + (‎ ⾶ ‎) 2FB6 KANGXI RADICAL FLY +← (‎ 飛 ‎) 98DB CJK UNIFIED IDEOGRAPH-98DB + +# 首 ⾸ + (‎ ⾸ ‎) 2FB8 KANGXI RADICAL HEAD +← (‎ 首 ‎) 9996 CJK UNIFIED IDEOGRAPH-9996 + +# 香 ⾹ + (‎ ⾹ ‎) 2FB9 KANGXI RADICAL FRAGRANT +← (‎ 香 ‎) 9999 CJK UNIFIED IDEOGRAPH-9999 + +# 馬 ⾺ + (‎ ⾺ ‎) 2FBA KANGXI RADICAL HORSE +← (‎ 馬 ‎) 99AC CJK UNIFIED IDEOGRAPH-99AC + +# 骨 ⾻ + (‎ ⾻ ‎) 2FBB KANGXI RADICAL BONE +← (‎ 骨 ‎) 9AA8 CJK UNIFIED IDEOGRAPH-9AA8 + +# 高 ⾼ + (‎ ⾼ ‎) 2FBC KANGXI RADICAL TALL +← (‎ 高 ‎) 9AD8 CJK UNIFIED IDEOGRAPH-9AD8 + +# 髟 ⾽ + (‎ ⾽ ‎) 2FBD KANGXI RADICAL HAIR +← (‎ 髟 ‎) 9ADF CJK UNIFIED IDEOGRAPH-9ADF + +# 鬥 ⾾ + (‎ ⾾ ‎) 2FBE KANGXI RADICAL FIGHT +← (‎ 鬥 ‎) 9B25 CJK UNIFIED IDEOGRAPH-9B25 + +# 鬯 ⾿ + (‎ ⾿ ‎) 2FBF KANGXI RADICAL SACRIFICIAL WINE +← (‎ 鬯 ‎) 9B2F CJK UNIFIED IDEOGRAPH-9B2F + +# 鬲 ⿀ + (‎ ⿀ ‎) 2FC0 KANGXI RADICAL CAULDRON +← (‎ 鬲 ‎) 9B32 CJK UNIFIED IDEOGRAPH-9B32 + +# 魚 ⿂ + (‎ ⿂ ‎) 2FC2 KANGXI RADICAL FISH +← (‎ 魚 ‎) 9B5A CJK UNIFIED IDEOGRAPH-9B5A + +# 鳥 ⿃ + (‎ ⿃ ‎) 2FC3 KANGXI RADICAL BIRD +← (‎ 鳥 ‎) 9CE5 CJK UNIFIED IDEOGRAPH-9CE5 + +# 鹵 ⿄ + (‎ ⿄ ‎) 2FC4 KANGXI RADICAL SALT +← (‎ 鹵 ‎) 9E75 CJK UNIFIED IDEOGRAPH-9E75 + +# 鹿 鹿 ⿅ + (‎ ⿅ ‎) 2FC5 KANGXI RADICAL DEER +← (‎ 鹿 ‎) 9E7F CJK UNIFIED IDEOGRAPH-9E7F +← (‎ 鹿 ‎) F940 CJK COMPATIBILITY IDEOGRAPH-F940 # →鹿→ + +# 麥 ⿆ + (‎ ⿆ ‎) 2FC6 KANGXI RADICAL WHEAT +← (‎ 麥 ‎) 9EA5 CJK UNIFIED IDEOGRAPH-9EA5 + +# 麻 麻 ⿇ + (‎ ⿇ ‎) 2FC7 KANGXI RADICAL HEMP +← (‎ 麻 ‎) 9EBB CJK UNIFIED IDEOGRAPH-9EBB +← (‎ 麻 ‎) 2FA15 CJK COMPATIBILITY IDEOGRAPH-2FA15 # →麻→ + +# 黃 ⿈ + (‎ ⿈ ‎) 2FC8 KANGXI RADICAL YELLOW +← (‎ 黃 ‎) 9EC3 CJK UNIFIED IDEOGRAPH-9EC3 + +# 黍 ⿉ + (‎ ⿉ ‎) 2FC9 KANGXI RADICAL MILLET +← (‎ 黍 ‎) 9ECD CJK UNIFIED IDEOGRAPH-9ECD + +# 黑 黒 ⿊ + (‎ ⿊ ‎) 2FCA KANGXI RADICAL BLACK +← (‎ 黑 ‎) 9ED1 CJK UNIFIED IDEOGRAPH-9ED1 +← (‎ 黒 ‎) 9ED2 CJK UNIFIED IDEOGRAPH-9ED2 + +# 黹 黹 ⿋ + (‎ ⿋ ‎) 2FCB KANGXI RADICAL EMBROIDERY +← (‎ 黹 ‎) 9EF9 CJK UNIFIED IDEOGRAPH-9EF9 +← (‎ 黹 ‎) 2FA17 CJK COMPATIBILITY IDEOGRAPH-2FA17 # →黹→ + +# 黽 ⿌ + (‎ ⿌ ‎) 2FCC KANGXI RADICAL FROG +← (‎ 黽 ‎) 9EFD CJK UNIFIED IDEOGRAPH-9EFD + +# 鼎 ⿍ + (‎ ⿍ ‎) 2FCD KANGXI RADICAL TRIPOD +← (‎ 鼎 ‎) 9F0E CJK UNIFIED IDEOGRAPH-9F0E + +# 鼓 ⿎ + (‎ ⿎ ‎) 2FCE KANGXI RADICAL DRUM +← (‎ 鼓 ‎) 9F13 CJK UNIFIED IDEOGRAPH-9F13 + +# 鼠 ⿏ + (‎ ⿏ ‎) 2FCF KANGXI RADICAL RAT +← (‎ 鼠 ‎) 9F20 CJK UNIFIED IDEOGRAPH-9F20 + +# 鼻 鼻 ⿐ + (‎ ⿐ ‎) 2FD0 KANGXI RADICAL NOSE +← (‎ 鼻 ‎) 9F3B CJK UNIFIED IDEOGRAPH-9F3B +← (‎ 鼻 ‎) 2FA1C CJK COMPATIBILITY IDEOGRAPH-2FA1C # →鼻→ + +# 齊 ⿑ + (‎ ⿑ ‎) 2FD1 KANGXI RADICAL EVEN +← (‎ 齊 ‎) 9F4A CJK UNIFIED IDEOGRAPH-9F4A + +# 齒 ⿒ + (‎ ⿒ ‎) 2FD2 KANGXI RADICAL TOOTH +← (‎ 齒 ‎) 9F52 CJK UNIFIED IDEOGRAPH-9F52 + +# 龍 龍 ⿓ + (‎ ⿓ ‎) 2FD3 KANGXI RADICAL DRAGON +← (‎ 龍 ‎) 9F8D CJK UNIFIED IDEOGRAPH-9F8D +← (‎ 龍 ‎) F9C4 CJK COMPATIBILITY IDEOGRAPH-F9C4 # →龍→ + +# 龜 龜 龜 龜 ⿔ + (‎ ⿔ ‎) 2FD4 KANGXI RADICAL TURTLE +← (‎ 龜 ‎) 9F9C CJK UNIFIED IDEOGRAPH-9F9C +← (‎ 龜 ‎) F907 CJK COMPATIBILITY IDEOGRAPH-F907 # →龜→ +← (‎ 龜 ‎) F908 CJK COMPATIBILITY IDEOGRAPH-F908 # →龜→ +← (‎ 龜 ‎) FACE CJK COMPATIBILITY IDEOGRAPH-FACE # →龜→ + +# 龠 ⿕ + (‎ ⿕ ‎) 2FD5 KANGXI RADICAL FLUTE +← (‎ 龠 ‎) 9FA0 CJK UNIFIED IDEOGRAPH-9FA0 + +# 〜 ~ + (‎ 〜 ‎) 301C WAVE DASH +← (‎ ~ ‎) FF5E FULLWIDTH TILDE + +# 卄 〹 + (‎ 〹 ‎) 3039 HANGZHOU NUMERAL TWENTY +← (‎ 卄 ‎) 5344 CJK UNIFIED IDEOGRAPH-5344 + +# 卅 〺 + (‎ 〺 ‎) 303A HANGZHOU NUMERAL THIRTY +← (‎ 卅 ‎) 5345 CJK UNIFIED IDEOGRAPH-5345 + +# へ ヘ + (‎ へ ‎) 3078 HIRAGANA LETTER HE +← (‎ ヘ ‎) 30D8 KATAKANA LETTER HE + +# ゙ ゛ + (‎ ゛ ‎) 309B KATAKANA-HIRAGANA VOICED SOUND MARK +← (‎ ゙ ‎) FF9E HALFWIDTH KATAKANA VOICED SOUND MARK + +# ゚ ゜ + (‎ ゜ ‎) 309C KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK +← (‎ ゚ ‎) FF9F HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK + +# ㏄ ⃝ 🅭 + (‎ ㏄ ⃝ ‎) 33C4 0009 20DD SQUARE CC, , COMBINING ENCLOSING CIRCLE +← (‎ 🅭 ‎) 1F16D CIRCLED CC + +# 㒞 㒞 + (‎ 㒞 ‎) 349E CJK UNIFIED IDEOGRAPH-349E +← (‎ 㒞 ‎) 2F80C CJK COMPATIBILITY IDEOGRAPH-2F80C + +# 㒹 㒹 + (‎ 㒹 ‎) 34B9 CJK UNIFIED IDEOGRAPH-34B9 +← (‎ 㒹 ‎) 2F813 CJK COMPATIBILITY IDEOGRAPH-2F813 + +# 㒻 㒻 + (‎ 㒻 ‎) 34BB CJK UNIFIED IDEOGRAPH-34BB +← (‎ 㒻 ‎) 2F9CA CJK COMPATIBILITY IDEOGRAPH-2F9CA + +# 㓟 㓟 + (‎ 㓟 ‎) 34DF CJK UNIFIED IDEOGRAPH-34DF +← (‎ 㓟 ‎) 2F81F CJK COMPATIBILITY IDEOGRAPH-2F81F + +# 㔕 㔕 + (‎ 㔕 ‎) 3515 CJK UNIFIED IDEOGRAPH-3515 +← (‎ 㔕 ‎) 2F824 CJK COMPATIBILITY IDEOGRAPH-2F824 + +# 㖈 䎛 + (‎ 㖈 ‎) 3588 CJK UNIFIED IDEOGRAPH-3588 +← (‎ 䎛 ‎) 439B CJK UNIFIED IDEOGRAPH-439B + +# 㘽 㦳 + (‎ 㘽 ‎) 363D CJK UNIFIED IDEOGRAPH-363D +← (‎ 㦳 ‎) 39B3 CJK UNIFIED IDEOGRAPH-39B3 + +# 㛮 㛮 + (‎ 㛮 ‎) 36EE CJK UNIFIED IDEOGRAPH-36EE +← (‎ 㛮 ‎) 2F867 CJK COMPATIBILITY IDEOGRAPH-2F867 + +# 㛼 㛼 + (‎ 㛼 ‎) 36FC CJK UNIFIED IDEOGRAPH-36FC +← (‎ 㛼 ‎) 2F868 CJK COMPATIBILITY IDEOGRAPH-2F868 + +# 㞁 㞁 + (‎ 㞁 ‎) 3781 CJK UNIFIED IDEOGRAPH-3781 +← (‎ 㞁 ‎) 2F876 CJK COMPATIBILITY IDEOGRAPH-2F876 + +# 㠯 㠯 + (‎ 㠯 ‎) 382F CJK UNIFIED IDEOGRAPH-382F +← (‎ 㠯 ‎) 2F883 CJK COMPATIBILITY IDEOGRAPH-2F883 + +# 㡢 㡢 + (‎ 㡢 ‎) 3862 CJK UNIFIED IDEOGRAPH-3862 +← (‎ 㡢 ‎) 2F888 CJK COMPATIBILITY IDEOGRAPH-2F888 + +# 㡼 㡼 + (‎ 㡼 ‎) 387C CJK UNIFIED IDEOGRAPH-387C +← (‎ 㡼 ‎) 2F88A CJK COMPATIBILITY IDEOGRAPH-2F88A + +# 㣇 㣇 + (‎ 㣇 ‎) 38C7 CJK UNIFIED IDEOGRAPH-38C7 +← (‎ 㣇 ‎) 2F896 CJK COMPATIBILITY IDEOGRAPH-2F896 + +# 㣣 㣣 + (‎ 㣣 ‎) 38E3 CJK UNIFIED IDEOGRAPH-38E3 +← (‎ 㣣 ‎) 2F89B CJK COMPATIBILITY IDEOGRAPH-2F89B + +# 㤜 㤜 + (‎ 㤜 ‎) 391C CJK UNIFIED IDEOGRAPH-391C +← (‎ 㤜 ‎) 2F8A2 CJK COMPATIBILITY IDEOGRAPH-2F8A2 + +# 㤺 㤺 + (‎ 㤺 ‎) 393A CJK UNIFIED IDEOGRAPH-393A +← (‎ 㤺 ‎) 2F8A1 CJK COMPATIBILITY IDEOGRAPH-2F8A1 + +# 㨮 㨮 + (‎ 㨮 ‎) 3A2E CJK UNIFIED IDEOGRAPH-3A2E +← (‎ 㨮 ‎) 2F8C2 CJK COMPATIBILITY IDEOGRAPH-2F8C2 + +# 㩁 搉 + (‎ 㩁 ‎) 3A41 CJK UNIFIED IDEOGRAPH-3A41 +← (‎ 搉 ‎) 6409 CJK UNIFIED IDEOGRAPH-6409 + +# 㩬 㩬 + (‎ 㩬 ‎) 3A6C CJK UNIFIED IDEOGRAPH-3A6C +← (‎ 㩬 ‎) 2F8C7 CJK COMPATIBILITY IDEOGRAPH-2F8C7 + +# 㫚 曶 + (‎ 㫚 ‎) 3ADA CJK UNIFIED IDEOGRAPH-3ADA +← (‎ 曶 ‎) 66F6 CJK UNIFIED IDEOGRAPH-66F6 + +# 㫤 㫤 + (‎ 㫤 ‎) 3AE4 CJK UNIFIED IDEOGRAPH-3AE4 +← (‎ 㫤 ‎) 2F8D1 CJK COMPATIBILITY IDEOGRAPH-2F8D1 + +# 㬈 㬈 + (‎ 㬈 ‎) 3B08 CJK UNIFIED IDEOGRAPH-3B08 +← (‎ 㬈 ‎) 2F8D0 CJK COMPATIBILITY IDEOGRAPH-2F8D0 + +# 㬙 㬙 + (‎ 㬙 ‎) 3B19 CJK UNIFIED IDEOGRAPH-3B19 +← (‎ 㬙 ‎) 2F8CE CJK COMPATIBILITY IDEOGRAPH-2F8CE + +# 㬵 胶 + (‎ 㬵 ‎) 3B35 CJK UNIFIED IDEOGRAPH-3B35 +← (‎ 胶 ‎) 80F6 CJK UNIFIED IDEOGRAPH-80F6 + +# 㬺 幐 + (‎ 㬺 ‎) 3B3A CJK UNIFIED IDEOGRAPH-3B3A +← (‎ 幐 ‎) 5E50 CJK UNIFIED IDEOGRAPH-5E50 + +# 㬻 䐠 + (‎ 㬻 ‎) 3B3B CJK UNIFIED IDEOGRAPH-3B3B +← (‎ 䐠 ‎) 4420 CJK UNIFIED IDEOGRAPH-4420 + +# 㭉 㭉 + (‎ 㭉 ‎) 3B49 CJK UNIFIED IDEOGRAPH-3B49 +← (‎ 㭉 ‎) 2F8DE CJK COMPATIBILITY IDEOGRAPH-2F8DE + +# 㮝 㮝 㮝 + (‎ 㮝 ‎) 3B9D CJK UNIFIED IDEOGRAPH-3B9D +← (‎ 㮝 ‎) FAD2 CJK COMPATIBILITY IDEOGRAPH-FAD2 +← (‎ 㮝 ‎) 2F8E7 CJK COMPATIBILITY IDEOGRAPH-2F8E7 + +# 㮣 槩 + (‎ 㮣 ‎) 3BA3 CJK UNIFIED IDEOGRAPH-3BA3 +← (‎ 槩 ‎) 69E9 CJK UNIFIED IDEOGRAPH-69E9 + +# 㰘 㰘 + (‎ 㰘 ‎) 3C18 CJK UNIFIED IDEOGRAPH-3C18 +← (‎ 㰘 ‎) 2F8EE CJK COMPATIBILITY IDEOGRAPH-2F8EE + +# 㱎 㱎 + (‎ 㱎 ‎) 3C4E CJK UNIFIED IDEOGRAPH-3C4E +← (‎ 㱎 ‎) 2F8F2 CJK COMPATIBILITY IDEOGRAPH-2F8F2 + +# 㴳 㴳 + (‎ 㴳 ‎) 3D33 CJK UNIFIED IDEOGRAPH-3D33 +← (‎ 㴳 ‎) 2F90A CJK COMPATIBILITY IDEOGRAPH-2F90A + +# 㶖 㶖 + (‎ 㶖 ‎) 3D96 CJK UNIFIED IDEOGRAPH-3D96 +← (‎ 㶖 ‎) 2F916 CJK COMPATIBILITY IDEOGRAPH-2F916 + +# 㺬 㺬 + (‎ 㺬 ‎) 3EAC CJK UNIFIED IDEOGRAPH-3EAC +← (‎ 㺬 ‎) 2F92A CJK COMPATIBILITY IDEOGRAPH-2F92A + +# 㺸 㺸 㺸 + (‎ 㺸 ‎) 3EB8 CJK UNIFIED IDEOGRAPH-3EB8 +← (‎ 㺸 ‎) 2F92C CJK COMPATIBILITY IDEOGRAPH-2F92C +← (‎ 㺸 ‎) 2F92D CJK COMPATIBILITY IDEOGRAPH-2F92D + +# 㼛 㼛 + (‎ 㼛 ‎) 3F1B CJK UNIFIED IDEOGRAPH-3F1B +← (‎ 㼛 ‎) 2F933 CJK COMPATIBILITY IDEOGRAPH-2F933 + +# 㿼 㿼 + (‎ 㿼 ‎) 3FFC CJK UNIFIED IDEOGRAPH-3FFC +← (‎ 㿼 ‎) 2F93E CJK COMPATIBILITY IDEOGRAPH-2F93E + +# 䀈 䀈 + (‎ 䀈 ‎) 4008 CJK UNIFIED IDEOGRAPH-4008 +← (‎ 䀈 ‎) 2F93F CJK COMPATIBILITY IDEOGRAPH-2F93F + +# 䀘 䀘 + (‎ 䀘 ‎) 4018 CJK UNIFIED IDEOGRAPH-4018 +← (‎ 䀘 ‎) FAD3 CJK COMPATIBILITY IDEOGRAPH-FAD3 + +# 䀹 鿃 䀹 䀹 + (‎ 䀹 ‎) 4039 CJK UNIFIED IDEOGRAPH-4039 +← (‎ 鿃 ‎) 9FC3 CJK UNIFIED IDEOGRAPH-9FC3 # →䀹→ +← (‎ 䀹 ‎) FAD4 CJK COMPATIBILITY IDEOGRAPH-FAD4 +← (‎ 䀹 ‎) 2F949 CJK COMPATIBILITY IDEOGRAPH-2F949 + +# 䀿 晣 + (‎ 䀿 ‎) 403F CJK UNIFIED IDEOGRAPH-403F +← (‎ 晣 ‎) 6663 CJK UNIFIED IDEOGRAPH-6663 + +# 䁆 䁆 + (‎ 䁆 ‎) 4046 CJK UNIFIED IDEOGRAPH-4046 +← (‎ 䁆 ‎) 2F94B CJK COMPATIBILITY IDEOGRAPH-2F94B + +# 䂖 䂖 + (‎ 䂖 ‎) 4096 CJK UNIFIED IDEOGRAPH-4096 +← (‎ 䂖 ‎) 2F94C CJK COMPATIBILITY IDEOGRAPH-2F94C + +# 䃣 䃣 + (‎ 䃣 ‎) 40E3 CJK UNIFIED IDEOGRAPH-40E3 +← (‎ 䃣 ‎) 2F951 CJK COMPATIBILITY IDEOGRAPH-2F951 + +# 䄯 䄯 + (‎ 䄯 ‎) 412F CJK UNIFIED IDEOGRAPH-412F +← (‎ 䄯 ‎) 2F958 CJK COMPATIBILITY IDEOGRAPH-2F958 + +# 䈂 䈂 + (‎ 䈂 ‎) 4202 CJK UNIFIED IDEOGRAPH-4202 +← (‎ 䈂 ‎) 2F960 CJK COMPATIBILITY IDEOGRAPH-2F960 + +# 䈧 䈧 + (‎ 䈧 ‎) 4227 CJK UNIFIED IDEOGRAPH-4227 +← (‎ 䈧 ‎) 2F964 CJK COMPATIBILITY IDEOGRAPH-2F964 + +# 䊠 䊠 + (‎ 䊠 ‎) 42A0 CJK UNIFIED IDEOGRAPH-42A0 +← (‎ 䊠 ‎) 2F967 CJK COMPATIBILITY IDEOGRAPH-2F967 + +# 䌁 䌁 + (‎ 䌁 ‎) 4301 CJK UNIFIED IDEOGRAPH-4301 +← (‎ 䌁 ‎) 2F96D CJK COMPATIBILITY IDEOGRAPH-2F96D + +# 䌴 䌴 + (‎ 䌴 ‎) 4334 CJK UNIFIED IDEOGRAPH-4334 +← (‎ 䌴 ‎) 2F971 CJK COMPATIBILITY IDEOGRAPH-2F971 + +# 䍙 䍙 + (‎ 䍙 ‎) 4359 CJK UNIFIED IDEOGRAPH-4359 +← (‎ 䍙 ‎) 2F974 CJK COMPATIBILITY IDEOGRAPH-2F974 + +# 䏕 䏕 + (‎ 䏕 ‎) 43D5 CJK UNIFIED IDEOGRAPH-43D5 +← (‎ 䏕 ‎) 2F981 CJK COMPATIBILITY IDEOGRAPH-2F981 + +# 䏙 䏙 + (‎ 䏙 ‎) 43D9 CJK UNIFIED IDEOGRAPH-43D9 +← (‎ 䏙 ‎) 2F8D7 CJK COMPATIBILITY IDEOGRAPH-2F8D7 + +# 䐋 䐋 + (‎ 䐋 ‎) 440B CJK UNIFIED IDEOGRAPH-440B +← (‎ 䐋 ‎) 2F984 CJK COMPATIBILITY IDEOGRAPH-2F984 + +# 䑃 朦 + (‎ 䑃 ‎) 4443 CJK UNIFIED IDEOGRAPH-4443 +← (‎ 朦 ‎) 6726 CJK UNIFIED IDEOGRAPH-6726 + +# 䑫 䑫 + (‎ 䑫 ‎) 446B CJK UNIFIED IDEOGRAPH-446B +← (‎ 䑫 ‎) 2F98E CJK COMPATIBILITY IDEOGRAPH-2F98E + +# 䔫 䔫 + (‎ 䔫 ‎) 452B CJK UNIFIED IDEOGRAPH-452B +← (‎ 䔫 ‎) 2F9A7 CJK COMPATIBILITY IDEOGRAPH-2F9A7 + +# 䕝 䕝 + (‎ 䕝 ‎) 455D CJK UNIFIED IDEOGRAPH-455D +← (‎ 䕝 ‎) 2F9AE CJK COMPATIBILITY IDEOGRAPH-2F9AE + +# 䕡 䕡 + (‎ 䕡 ‎) 4561 CJK UNIFIED IDEOGRAPH-4561 +← (‎ 䕡 ‎) 2F9AF CJK COMPATIBILITY IDEOGRAPH-2F9AF + +# 䕫 䕫 + (‎ 䕫 ‎) 456B CJK UNIFIED IDEOGRAPH-456B +← (‎ 䕫 ‎) 2F9B2 CJK COMPATIBILITY IDEOGRAPH-2F9B2 + +# 䗗 䗗 + (‎ 䗗 ‎) 45D7 CJK UNIFIED IDEOGRAPH-45D7 +← (‎ 䗗 ‎) 2F9BF CJK COMPATIBILITY IDEOGRAPH-2F9BF + +# 䗹 䗹 + (‎ 䗹 ‎) 45F9 CJK UNIFIED IDEOGRAPH-45F9 +← (‎ 䗹 ‎) 2F9C2 CJK COMPATIBILITY IDEOGRAPH-2F9C2 + +# 䘵 䘵 + (‎ 䘵 ‎) 4635 CJK UNIFIED IDEOGRAPH-4635 +← (‎ 䘵 ‎) 2F9C8 CJK COMPATIBILITY IDEOGRAPH-2F9C8 + +# 䚶 訞 + (‎ 䚶 ‎) 46B6 CJK UNIFIED IDEOGRAPH-46B6 +← (‎ 訞 ‎) 8A1E CJK UNIFIED IDEOGRAPH-8A1E + +# 䚾 䚾 + (‎ 䚾 ‎) 46BE CJK UNIFIED IDEOGRAPH-46BE +← (‎ 䚾 ‎) 2F9CD CJK COMPATIBILITY IDEOGRAPH-2F9CD + +# 䛇 䛇 + (‎ 䛇 ‎) 46C7 CJK UNIFIED IDEOGRAPH-46C7 +← (‎ 䛇 ‎) 2F9CE CJK COMPATIBILITY IDEOGRAPH-2F9CE + +# 䦕 䦕 + (‎ 䦕 ‎) 4995 CJK UNIFIED IDEOGRAPH-4995 +← (‎ 䦕 ‎) 2F9EF CJK COMPATIBILITY IDEOGRAPH-2F9EF + +# 䧦 䧦 + (‎ 䧦 ‎) 49E6 CJK UNIFIED IDEOGRAPH-49E6 +← (‎ 䧦 ‎) 2F9F2 CJK COMPATIBILITY IDEOGRAPH-2F9F2 + +# 䩮 䩮 + (‎ 䩮 ‎) 4A6E CJK UNIFIED IDEOGRAPH-4A6E +← (‎ 䩮 ‎) 2F9F8 CJK COMPATIBILITY IDEOGRAPH-2F9F8 + +# 䩶 䩶 + (‎ 䩶 ‎) 4A76 CJK UNIFIED IDEOGRAPH-4A76 +← (‎ 䩶 ‎) 2F9F9 CJK COMPATIBILITY IDEOGRAPH-2F9F9 + +# 䪲 䪲 + (‎ 䪲 ‎) 4AB2 CJK UNIFIED IDEOGRAPH-4AB2 +← (‎ 䪲 ‎) 2F9FC CJK COMPATIBILITY IDEOGRAPH-2F9FC + +# 䬳 䬳 + (‎ 䬳 ‎) 4B33 CJK UNIFIED IDEOGRAPH-4B33 +← (‎ 䬳 ‎) 2FA03 CJK COMPATIBILITY IDEOGRAPH-2FA03 + +# 䯎 䯎 + (‎ 䯎 ‎) 4BCE CJK UNIFIED IDEOGRAPH-4BCE +← (‎ 䯎 ‎) 2FA08 CJK COMPATIBILITY IDEOGRAPH-2FA08 + +# 䳎 䳎 + (‎ 䳎 ‎) 4CCE CJK UNIFIED IDEOGRAPH-4CCE +← (‎ 䳎 ‎) 2FA0D CJK COMPATIBILITY IDEOGRAPH-2FA0D + +# 䳭 䳭 + (‎ 䳭 ‎) 4CED CJK UNIFIED IDEOGRAPH-4CED +← (‎ 䳭 ‎) 2FA0E CJK COMPATIBILITY IDEOGRAPH-2FA0E + +# 䳸 䳸 + (‎ 䳸 ‎) 4CF8 CJK UNIFIED IDEOGRAPH-4CF8 +← (‎ 䳸 ‎) 2FA11 CJK COMPATIBILITY IDEOGRAPH-2FA11 + +# 䵖 䵖 + (‎ 䵖 ‎) 4D56 CJK UNIFIED IDEOGRAPH-4D56 +← (‎ 䵖 ‎) 2FA16 CJK COMPATIBILITY IDEOGRAPH-2FA16 + +# 不 不 + (‎ 不 ‎) 4E0D CJK UNIFIED IDEOGRAPH-4E0D +← (‎ 不 ‎) F967 CJK COMPATIBILITY IDEOGRAPH-F967 + +# 並 並 + (‎ 並 ‎) 4E26 CJK UNIFIED IDEOGRAPH-4E26 +← (‎ 並 ‎) FA70 CJK COMPATIBILITY IDEOGRAPH-FA70 + +# 串 串 + (‎ 串 ‎) 4E32 CJK UNIFIED IDEOGRAPH-4E32 +← (‎ 串 ‎) F905 CJK COMPATIBILITY IDEOGRAPH-F905 + +# 丸 丸 + (‎ 丸 ‎) 4E38 CJK UNIFIED IDEOGRAPH-4E38 +← (‎ 丸 ‎) 2F801 CJK COMPATIBILITY IDEOGRAPH-2F801 + +# 丹 丹 + (‎ 丹 ‎) 4E39 CJK UNIFIED IDEOGRAPH-4E39 +← (‎ 丹 ‎) F95E CJK COMPATIBILITY IDEOGRAPH-F95E + +# 丽 丽 + (‎ 丽 ‎) 4E3D CJK UNIFIED IDEOGRAPH-4E3D +← (‎ 丽 ‎) 2F800 CJK COMPATIBILITY IDEOGRAPH-2F800 + +# 乁 乁 + (‎ 乁 ‎) 4E41 CJK UNIFIED IDEOGRAPH-4E41 +← (‎ 乁 ‎) 2F802 CJK COMPATIBILITY IDEOGRAPH-2F802 + +# 亂 亂 + (‎ 亂 ‎) 4E82 CJK UNIFIED IDEOGRAPH-4E82 +← (‎ 亂 ‎) F91B CJK COMPATIBILITY IDEOGRAPH-F91B + +# 了 了 + (‎ 了 ‎) 4E86 CJK UNIFIED IDEOGRAPH-4E86 +← (‎ 了 ‎) F9BA CJK COMPATIBILITY IDEOGRAPH-F9BA + +# 亮 亮 + (‎ 亮 ‎) 4EAE CJK UNIFIED IDEOGRAPH-4EAE +← (‎ 亮 ‎) F977 CJK COMPATIBILITY IDEOGRAPH-F977 + +# 什 什 + (‎ 什 ‎) 4EC0 CJK UNIFIED IDEOGRAPH-4EC0 +← (‎ 什 ‎) F9FD CJK COMPATIBILITY IDEOGRAPH-F9FD + +# 仌 仌 + (‎ 仌 ‎) 4ECC CJK UNIFIED IDEOGRAPH-4ECC +← (‎ 仌 ‎) 2F819 CJK COMPATIBILITY IDEOGRAPH-2F819 + +# 令 令 + (‎ 令 ‎) 4EE4 CJK UNIFIED IDEOGRAPH-4EE4 +← (‎ 令 ‎) F9A8 CJK COMPATIBILITY IDEOGRAPH-F9A8 + +# 你 你 + (‎ 你 ‎) 4F60 CJK UNIFIED IDEOGRAPH-4F60 +← (‎ 你 ‎) 2F804 CJK COMPATIBILITY IDEOGRAPH-2F804 + +# 併 倂 倂 + (‎ 併 ‎) 4F75 CJK UNIFIED IDEOGRAPH-4F75 +← (‎ 倂 ‎) 5002 CJK UNIFIED IDEOGRAPH-5002 +← (‎ 倂 ‎) 2F807 CJK COMPATIBILITY IDEOGRAPH-2F807 # →倂→ + +# 侀 侀 + (‎ 侀 ‎) 4F80 CJK UNIFIED IDEOGRAPH-4F80 +← (‎ 侀 ‎) FA73 CJK COMPATIBILITY IDEOGRAPH-FA73 + +# 來 來 + (‎ 來 ‎) 4F86 CJK UNIFIED IDEOGRAPH-4F86 +← (‎ 來 ‎) F92D CJK COMPATIBILITY IDEOGRAPH-F92D + +# 例 例 + (‎ 例 ‎) 4F8B CJK UNIFIED IDEOGRAPH-4F8B +← (‎ 例 ‎) F9B5 CJK COMPATIBILITY IDEOGRAPH-F9B5 + +# 侮 侮 侮 + (‎ 侮 ‎) 4FAE CJK UNIFIED IDEOGRAPH-4FAE +← (‎ 侮 ‎) FA30 CJK COMPATIBILITY IDEOGRAPH-FA30 +← (‎ 侮 ‎) 2F805 CJK COMPATIBILITY IDEOGRAPH-2F805 + +# 侻 侻 + (‎ 侻 ‎) 4FBB CJK UNIFIED IDEOGRAPH-4FBB +← (‎ 侻 ‎) 2F806 CJK COMPATIBILITY IDEOGRAPH-2F806 + +# 便 便 + (‎ 便 ‎) 4FBF CJK UNIFIED IDEOGRAPH-4FBF +← (‎ 便 ‎) F965 CJK COMPATIBILITY IDEOGRAPH-F965 + +# 値 值 + (‎ 値 ‎) 5024 CJK UNIFIED IDEOGRAPH-5024 +← (‎ 值 ‎) 503C CJK UNIFIED IDEOGRAPH-503C + +# 倫 倫 + (‎ 倫 ‎) 502B CJK UNIFIED IDEOGRAPH-502B +← (‎ 倫 ‎) F9D4 CJK COMPATIBILITY IDEOGRAPH-F9D4 + +# 偺 偺 + (‎ 偺 ‎) 507A CJK UNIFIED IDEOGRAPH-507A +← (‎ 偺 ‎) 2F808 CJK COMPATIBILITY IDEOGRAPH-2F808 + +# 備 備 + (‎ 備 ‎) 5099 CJK UNIFIED IDEOGRAPH-5099 +← (‎ 備 ‎) 2F809 CJK COMPATIBILITY IDEOGRAPH-2F809 + +# 像 像 + (‎ 像 ‎) 50CF CJK UNIFIED IDEOGRAPH-50CF +← (‎ 像 ‎) 2F80B CJK COMPATIBILITY IDEOGRAPH-2F80B + +# 僚 僚 + (‎ 僚 ‎) 50DA CJK UNIFIED IDEOGRAPH-50DA +← (‎ 僚 ‎) F9BB CJK COMPATIBILITY IDEOGRAPH-F9BB + +# 僧 僧 僧 + (‎ 僧 ‎) 50E7 CJK UNIFIED IDEOGRAPH-50E7 +← (‎ 僧 ‎) FA31 CJK COMPATIBILITY IDEOGRAPH-FA31 +← (‎ 僧 ‎) 2F80A CJK COMPATIBILITY IDEOGRAPH-2F80A + +# 充 充 + (‎ 充 ‎) 5145 CJK UNIFIED IDEOGRAPH-5145 +← (‎ 充 ‎) FA74 CJK COMPATIBILITY IDEOGRAPH-FA74 + +# 免 免 免 + (‎ 免 ‎) 514D CJK UNIFIED IDEOGRAPH-514D +← (‎ 免 ‎) FA32 CJK COMPATIBILITY IDEOGRAPH-FA32 +← (‎ 免 ‎) 2F80E CJK COMPATIBILITY IDEOGRAPH-2F80E + +# 兔 兔 + (‎ 兔 ‎) 5154 CJK UNIFIED IDEOGRAPH-5154 +← (‎ 兔 ‎) 2F80F CJK COMPATIBILITY IDEOGRAPH-2F80F + +# 兤 兤 + (‎ 兤 ‎) 5164 CJK UNIFIED IDEOGRAPH-5164 +← (‎ 兤 ‎) 2F810 CJK COMPATIBILITY IDEOGRAPH-2F810 + +# 內 內 + (‎ 內 ‎) 5167 CJK UNIFIED IDEOGRAPH-5167 +← (‎ 內 ‎) 2F814 CJK COMPATIBILITY IDEOGRAPH-2F814 + +# 全 全 + (‎ 全 ‎) 5168 CJK UNIFIED IDEOGRAPH-5168 +← (‎ 全 ‎) FA72 CJK COMPATIBILITY IDEOGRAPH-FA72 + +# 兩 兩 + (‎ 兩 ‎) 5169 CJK UNIFIED IDEOGRAPH-5169 +← (‎ 兩 ‎) F978 CJK COMPATIBILITY IDEOGRAPH-F978 + +# 六 六 + (‎ 六 ‎) 516D CJK UNIFIED IDEOGRAPH-516D +← (‎ 六 ‎) F9D1 CJK COMPATIBILITY IDEOGRAPH-F9D1 + +# 具 具 + (‎ 具 ‎) 5177 CJK UNIFIED IDEOGRAPH-5177 +← (‎ 具 ‎) 2F811 CJK COMPATIBILITY IDEOGRAPH-2F811 + +# 冀 冀 + (‎ 冀 ‎) 5180 CJK UNIFIED IDEOGRAPH-5180 +← (‎ 冀 ‎) FA75 CJK COMPATIBILITY IDEOGRAPH-FA75 + +# 再 再 + (‎ 再 ‎) 518D CJK UNIFIED IDEOGRAPH-518D +← (‎ 再 ‎) 2F815 CJK COMPATIBILITY IDEOGRAPH-2F815 + +# 冒 冒 + (‎ 冒 ‎) 5192 CJK UNIFIED IDEOGRAPH-5192 +← (‎ 冒 ‎) 2F8D2 CJK COMPATIBILITY IDEOGRAPH-2F8D2 + +# 冕 冕 + (‎ 冕 ‎) 5195 CJK UNIFIED IDEOGRAPH-5195 +← (‎ 冕 ‎) 2F8D3 CJK COMPATIBILITY IDEOGRAPH-2F8D3 + +# 冗 冗 + (‎ 冗 ‎) 5197 CJK UNIFIED IDEOGRAPH-5197 +← (‎ 冗 ‎) 2F817 CJK COMPATIBILITY IDEOGRAPH-2F817 + +# 冤 冤 + (‎ 冤 ‎) 51A4 CJK UNIFIED IDEOGRAPH-51A4 +← (‎ 冤 ‎) 2F818 CJK COMPATIBILITY IDEOGRAPH-2F818 + +# 冬 冬 + (‎ 冬 ‎) 51AC CJK UNIFIED IDEOGRAPH-51AC +← (‎ 冬 ‎) 2F81A CJK COMPATIBILITY IDEOGRAPH-2F81A + +# 况 况 况 + (‎ 况 ‎) 51B5 CJK UNIFIED IDEOGRAPH-51B5 +← (‎ 况 ‎) FA71 CJK COMPATIBILITY IDEOGRAPH-FA71 +← (‎ 况 ‎) 2F81B CJK COMPATIBILITY IDEOGRAPH-2F81B + +# 冷 冷 + (‎ 冷 ‎) 51B7 CJK UNIFIED IDEOGRAPH-51B7 +← (‎ 冷 ‎) F92E CJK COMPATIBILITY IDEOGRAPH-F92E + +# 凉 凉 + (‎ 凉 ‎) 51C9 CJK UNIFIED IDEOGRAPH-51C9 +← (‎ 凉 ‎) F979 CJK COMPATIBILITY IDEOGRAPH-F979 + +# 凌 凌 + (‎ 凌 ‎) 51CC CJK UNIFIED IDEOGRAPH-51CC +← (‎ 凌 ‎) F955 CJK COMPATIBILITY IDEOGRAPH-F955 + +# 凜 凜 + (‎ 凜 ‎) 51DC CJK UNIFIED IDEOGRAPH-51DC +← (‎ 凜 ‎) F954 CJK COMPATIBILITY IDEOGRAPH-F954 + +# 凞 凞 + (‎ 凞 ‎) 51DE CJK UNIFIED IDEOGRAPH-51DE +← (‎ 凞 ‎) FA15 CJK COMPATIBILITY IDEOGRAPH-FA15 + +# 刃 刃 + (‎ 刃 ‎) 5203 CJK UNIFIED IDEOGRAPH-5203 +← (‎ 刃 ‎) 2F81E CJK COMPATIBILITY IDEOGRAPH-2F81E + +# 切 切 切 + (‎ 切 ‎) 5207 CJK UNIFIED IDEOGRAPH-5207 +← (‎ 切 ‎) FA00 CJK COMPATIBILITY IDEOGRAPH-FA00 +← (‎ 切 ‎) 2F850 CJK COMPATIBILITY IDEOGRAPH-2F850 + +# 列 列 + (‎ 列 ‎) 5217 CJK UNIFIED IDEOGRAPH-5217 +← (‎ 列 ‎) F99C CJK COMPATIBILITY IDEOGRAPH-F99C + +# 利 利 + (‎ 利 ‎) 5229 CJK UNIFIED IDEOGRAPH-5229 +← (‎ 利 ‎) F9DD CJK COMPATIBILITY IDEOGRAPH-F9DD + +# 刺 刺 + (‎ 刺 ‎) 523A CJK UNIFIED IDEOGRAPH-523A +← (‎ 刺 ‎) F9FF CJK COMPATIBILITY IDEOGRAPH-F9FF + +# 刻 刻 + (‎ 刻 ‎) 523B CJK UNIFIED IDEOGRAPH-523B +← (‎ 刻 ‎) 2F820 CJK COMPATIBILITY IDEOGRAPH-2F820 + +# 剆 剆 + (‎ 剆 ‎) 5246 CJK UNIFIED IDEOGRAPH-5246 +← (‎ 剆 ‎) 2F821 CJK COMPATIBILITY IDEOGRAPH-2F821 + +# 割 割 + (‎ 割 ‎) 5272 CJK UNIFIED IDEOGRAPH-5272 +← (‎ 割 ‎) 2F822 CJK COMPATIBILITY IDEOGRAPH-2F822 + +# 剷 剷 + (‎ 剷 ‎) 5277 CJK UNIFIED IDEOGRAPH-5277 +← (‎ 剷 ‎) 2F823 CJK COMPATIBILITY IDEOGRAPH-2F823 + +# 劉 劉 + (‎ 劉 ‎) 5289 CJK UNIFIED IDEOGRAPH-5289 +← (‎ 劉 ‎) F9C7 CJK COMPATIBILITY IDEOGRAPH-F9C7 + +# 劣 劣 + (‎ 劣 ‎) 52A3 CJK UNIFIED IDEOGRAPH-52A3 +← (‎ 劣 ‎) F99D CJK COMPATIBILITY IDEOGRAPH-F99D + +# 劳 劳 + (‎ 劳 ‎) 52B3 CJK UNIFIED IDEOGRAPH-52B3 +← (‎ 劳 ‎) 2F992 CJK COMPATIBILITY IDEOGRAPH-2F992 + +# 勇 勇 勇 + (‎ 勇 ‎) 52C7 CJK UNIFIED IDEOGRAPH-52C7 +← (‎ 勇 ‎) FA76 CJK COMPATIBILITY IDEOGRAPH-FA76 +← (‎ 勇 ‎) 2F825 CJK COMPATIBILITY IDEOGRAPH-2F825 + +# 勉 勉 勉 + (‎ 勉 ‎) 52C9 CJK UNIFIED IDEOGRAPH-52C9 +← (‎ 勉 ‎) FA33 CJK COMPATIBILITY IDEOGRAPH-FA33 +← (‎ 勉 ‎) 2F826 CJK COMPATIBILITY IDEOGRAPH-2F826 + +# 勒 勒 + (‎ 勒 ‎) 52D2 CJK UNIFIED IDEOGRAPH-52D2 +← (‎ 勒 ‎) F952 CJK COMPATIBILITY IDEOGRAPH-F952 + +# 勞 勞 + (‎ 勞 ‎) 52DE CJK UNIFIED IDEOGRAPH-52DE +← (‎ 勞 ‎) F92F CJK COMPATIBILITY IDEOGRAPH-F92F + +# 勤 勤 勤 + (‎ 勤 ‎) 52E4 CJK UNIFIED IDEOGRAPH-52E4 +← (‎ 勤 ‎) FA34 CJK COMPATIBILITY IDEOGRAPH-FA34 +← (‎ 勤 ‎) 2F827 CJK COMPATIBILITY IDEOGRAPH-2F827 + +# 勵 勵 + (‎ 勵 ‎) 52F5 CJK UNIFIED IDEOGRAPH-52F5 +← (‎ 勵 ‎) F97F CJK COMPATIBILITY IDEOGRAPH-F97F + +# 勺 勺 勺 + (‎ 勺 ‎) 52FA CJK UNIFIED IDEOGRAPH-52FA +← (‎ 勺 ‎) FA77 CJK COMPATIBILITY IDEOGRAPH-FA77 +← (‎ 勺 ‎) 2F828 CJK COMPATIBILITY IDEOGRAPH-2F828 + +# 包 包 + (‎ 包 ‎) 5305 CJK UNIFIED IDEOGRAPH-5305 +← (‎ 包 ‎) 2F829 CJK COMPATIBILITY IDEOGRAPH-2F829 + +# 匆 匆 + (‎ 匆 ‎) 5306 CJK UNIFIED IDEOGRAPH-5306 +← (‎ 匆 ‎) 2F82A CJK COMPATIBILITY IDEOGRAPH-2F82A + +# 北 北 北 + (‎ 北 ‎) 5317 CJK UNIFIED IDEOGRAPH-5317 +← (‎ 北 ‎) F963 CJK COMPATIBILITY IDEOGRAPH-F963 +← (‎ 北 ‎) 2F82B CJK COMPATIBILITY IDEOGRAPH-2F82B + +# 匿 匿 + (‎ 匿 ‎) 533F CJK UNIFIED IDEOGRAPH-533F +← (‎ 匿 ‎) F9EB CJK COMPATIBILITY IDEOGRAPH-F9EB + +# 卉 卉 + (‎ 卉 ‎) 5349 CJK UNIFIED IDEOGRAPH-5349 +← (‎ 卉 ‎) 2F82C CJK COMPATIBILITY IDEOGRAPH-2F82C + +# 卑 卑 卑 + (‎ 卑 ‎) 5351 CJK UNIFIED IDEOGRAPH-5351 +← (‎ 卑 ‎) FA35 CJK COMPATIBILITY IDEOGRAPH-FA35 +← (‎ 卑 ‎) 2F82D CJK COMPATIBILITY IDEOGRAPH-2F82D + +# 博 博 + (‎ 博 ‎) 535A CJK UNIFIED IDEOGRAPH-535A +← (‎ 博 ‎) 2F82E CJK COMPATIBILITY IDEOGRAPH-2F82E + +# 即 即 + (‎ 即 ‎) 5373 CJK UNIFIED IDEOGRAPH-5373 +← (‎ 即 ‎) 2F82F CJK COMPATIBILITY IDEOGRAPH-2F82F + +# 卵 卵 + (‎ 卵 ‎) 5375 CJK UNIFIED IDEOGRAPH-5375 +← (‎ 卵 ‎) F91C CJK COMPATIBILITY IDEOGRAPH-F91C + +# 卽 卽 + (‎ 卽 ‎) 537D CJK UNIFIED IDEOGRAPH-537D +← (‎ 卽 ‎) 2F830 CJK COMPATIBILITY IDEOGRAPH-2F830 + +# 卿 卿 卿 卿 + (‎ 卿 ‎) 537F CJK UNIFIED IDEOGRAPH-537F +← (‎ 卿 ‎) 2F831 CJK COMPATIBILITY IDEOGRAPH-2F831 +← (‎ 卿 ‎) 2F832 CJK COMPATIBILITY IDEOGRAPH-2F832 +← (‎ 卿 ‎) 2F833 CJK COMPATIBILITY IDEOGRAPH-2F833 + +# 參 參 + (‎ 參 ‎) 53C3 CJK UNIFIED IDEOGRAPH-53C3 +← (‎ 參 ‎) F96B CJK COMPATIBILITY IDEOGRAPH-F96B + +# 及 及 + (‎ 及 ‎) 53CA CJK UNIFIED IDEOGRAPH-53CA +← (‎ 及 ‎) 2F836 CJK COMPATIBILITY IDEOGRAPH-2F836 + +# 叟 叟 + (‎ 叟 ‎) 53DF CJK UNIFIED IDEOGRAPH-53DF +← (‎ 叟 ‎) 2F837 CJK COMPATIBILITY IDEOGRAPH-2F837 + +# 句 句 + (‎ 句 ‎) 53E5 CJK UNIFIED IDEOGRAPH-53E5 +← (‎ 句 ‎) F906 CJK COMPATIBILITY IDEOGRAPH-F906 + +# 叫 叫 + (‎ 叫 ‎) 53EB CJK UNIFIED IDEOGRAPH-53EB +← (‎ 叫 ‎) 2F839 CJK COMPATIBILITY IDEOGRAPH-2F839 + +# 叱 叱 + (‎ 叱 ‎) 53F1 CJK UNIFIED IDEOGRAPH-53F1 +← (‎ 叱 ‎) 2F83A CJK COMPATIBILITY IDEOGRAPH-2F83A + +# 吆 吆 + (‎ 吆 ‎) 5406 CJK UNIFIED IDEOGRAPH-5406 +← (‎ 吆 ‎) 2F83B CJK COMPATIBILITY IDEOGRAPH-2F83B + +# 吏 吏 + (‎ 吏 ‎) 540F CJK UNIFIED IDEOGRAPH-540F +← (‎ 吏 ‎) F9DE CJK COMPATIBILITY IDEOGRAPH-F9DE + +# 吝 吝 + (‎ 吝 ‎) 541D CJK UNIFIED IDEOGRAPH-541D +← (‎ 吝 ‎) F9ED CJK COMPATIBILITY IDEOGRAPH-F9ED + +# 吸 吸 + (‎ 吸 ‎) 5438 CJK UNIFIED IDEOGRAPH-5438 +← (‎ 吸 ‎) 2F83D CJK COMPATIBILITY IDEOGRAPH-2F83D + +# 呂 呂 + (‎ 呂 ‎) 5442 CJK UNIFIED IDEOGRAPH-5442 +← (‎ 呂 ‎) F980 CJK COMPATIBILITY IDEOGRAPH-F980 + +# 呈 呈 + (‎ 呈 ‎) 5448 CJK UNIFIED IDEOGRAPH-5448 +← (‎ 呈 ‎) 2F83E CJK COMPATIBILITY IDEOGRAPH-2F83E + +# 周 周 + (‎ 周 ‎) 5468 CJK UNIFIED IDEOGRAPH-5468 +← (‎ 周 ‎) 2F83F CJK COMPATIBILITY IDEOGRAPH-2F83F + +# 咞 咞 + (‎ 咞 ‎) 549E CJK UNIFIED IDEOGRAPH-549E +← (‎ 咞 ‎) 2F83C CJK COMPATIBILITY IDEOGRAPH-2F83C + +# 咢 咢 + (‎ 咢 ‎) 54A2 CJK UNIFIED IDEOGRAPH-54A2 +← (‎ 咢 ‎) 2F840 CJK COMPATIBILITY IDEOGRAPH-2F840 + +# 咽 咽 + (‎ 咽 ‎) 54BD CJK UNIFIED IDEOGRAPH-54BD +← (‎ 咽 ‎) F99E CJK COMPATIBILITY IDEOGRAPH-F99E + +# 哶 哶 + (‎ 哶 ‎) 54F6 CJK UNIFIED IDEOGRAPH-54F6 +← (‎ 哶 ‎) 2F841 CJK COMPATIBILITY IDEOGRAPH-2F841 + +# 唐 唐 + (‎ 唐 ‎) 5510 CJK UNIFIED IDEOGRAPH-5510 +← (‎ 唐 ‎) 2F842 CJK COMPATIBILITY IDEOGRAPH-2F842 + +# 啓 啟 啓 + (‎ 啓 ‎) 5553 CJK UNIFIED IDEOGRAPH-5553 +← (‎ 啟 ‎) 555F CJK UNIFIED IDEOGRAPH-555F +← (‎ 啓 ‎) 2F843 CJK COMPATIBILITY IDEOGRAPH-2F843 + +# 啕 啕 + (‎ 啕 ‎) 5555 CJK UNIFIED IDEOGRAPH-5555 +← (‎ 啕 ‎) FA79 CJK COMPATIBILITY IDEOGRAPH-FA79 + +# 啣 啣 + (‎ 啣 ‎) 5563 CJK UNIFIED IDEOGRAPH-5563 +← (‎ 啣 ‎) 2F844 CJK COMPATIBILITY IDEOGRAPH-2F844 + +# 善 善 善 + (‎ 善 ‎) 5584 CJK UNIFIED IDEOGRAPH-5584 +← (‎ 善 ‎) 2F845 CJK COMPATIBILITY IDEOGRAPH-2F845 +← (‎ 善 ‎) 2F846 CJK COMPATIBILITY IDEOGRAPH-2F846 + +# 喇 喇 + (‎ 喇 ‎) 5587 CJK UNIFIED IDEOGRAPH-5587 +← (‎ 喇 ‎) F90B CJK COMPATIBILITY IDEOGRAPH-F90B + +# 喙 喙 喙 + (‎ 喙 ‎) 5599 CJK UNIFIED IDEOGRAPH-5599 +← (‎ 喙 ‎) FA7A CJK COMPATIBILITY IDEOGRAPH-FA7A +← (‎ 喙 ‎) 2F847 CJK COMPATIBILITY IDEOGRAPH-2F847 + +# 喝 喝 喝 + (‎ 喝 ‎) 559D CJK UNIFIED IDEOGRAPH-559D +← (‎ 喝 ‎) FA36 CJK COMPATIBILITY IDEOGRAPH-FA36 +← (‎ 喝 ‎) FA78 CJK COMPATIBILITY IDEOGRAPH-FA78 + +# 喫 喫 + (‎ 喫 ‎) 55AB CJK UNIFIED IDEOGRAPH-55AB +← (‎ 喫 ‎) 2F848 CJK COMPATIBILITY IDEOGRAPH-2F848 + +# 喳 喳 + (‎ 喳 ‎) 55B3 CJK UNIFIED IDEOGRAPH-55B3 +← (‎ 喳 ‎) 2F849 CJK COMPATIBILITY IDEOGRAPH-2F849 + +# 嗀 嗀 + (‎ 嗀 ‎) 55C0 CJK UNIFIED IDEOGRAPH-55C0 +← (‎ 嗀 ‎) FA0D CJK COMPATIBILITY IDEOGRAPH-FA0D + +# 嗂 嗂 + (‎ 嗂 ‎) 55C2 CJK UNIFIED IDEOGRAPH-55C2 +← (‎ 嗂 ‎) 2F84A CJK COMPATIBILITY IDEOGRAPH-2F84A + +# 嗢 嗢 + (‎ 嗢 ‎) 55E2 CJK UNIFIED IDEOGRAPH-55E2 +← (‎ 嗢 ‎) FA7B CJK COMPATIBILITY IDEOGRAPH-FA7B + +# 嘆 嘆 嘆 + (‎ 嘆 ‎) 5606 CJK UNIFIED IDEOGRAPH-5606 +← (‎ 嘆 ‎) FA37 CJK COMPATIBILITY IDEOGRAPH-FA37 +← (‎ 嘆 ‎) 2F84C CJK COMPATIBILITY IDEOGRAPH-2F84C + +# 噑 噑 + (‎ 噑 ‎) 5651 CJK UNIFIED IDEOGRAPH-5651 +← (‎ 噑 ‎) 2F84E CJK COMPATIBILITY IDEOGRAPH-2F84E + +# 器 器 + (‎ 器 ‎) 5668 CJK UNIFIED IDEOGRAPH-5668 +← (‎ 器 ‎) FA38 CJK COMPATIBILITY IDEOGRAPH-FA38 + +# 噴 噴 + (‎ 噴 ‎) 5674 CJK UNIFIED IDEOGRAPH-5674 +← (‎ 噴 ‎) 2F84F CJK COMPATIBILITY IDEOGRAPH-2F84F + +# 囹 囹 + (‎ 囹 ‎) 56F9 CJK UNIFIED IDEOGRAPH-56F9 +← (‎ 囹 ‎) F9A9 CJK COMPATIBILITY IDEOGRAPH-F9A9 + +# 圖 圖 + (‎ 圖 ‎) 5716 CJK UNIFIED IDEOGRAPH-5716 +← (‎ 圖 ‎) 2F84B CJK COMPATIBILITY IDEOGRAPH-2F84B + +# 圗 圗 + (‎ 圗 ‎) 5717 CJK UNIFIED IDEOGRAPH-5717 +← (‎ 圗 ‎) 2F84D CJK COMPATIBILITY IDEOGRAPH-2F84D + +# 型 型 + (‎ 型 ‎) 578B CJK UNIFIED IDEOGRAPH-578B +← (‎ 型 ‎) 2F855 CJK COMPATIBILITY IDEOGRAPH-2F855 + +# 城 城 + (‎ 城 ‎) 57CE CJK UNIFIED IDEOGRAPH-57CE +← (‎ 城 ‎) 2F852 CJK COMPATIBILITY IDEOGRAPH-2F852 + +# 埴 埴 + (‎ 埴 ‎) 57F4 CJK UNIFIED IDEOGRAPH-57F4 +← (‎ 埴 ‎) 2F853 CJK COMPATIBILITY IDEOGRAPH-2F853 + +# 堍 堍 + (‎ 堍 ‎) 580D CJK UNIFIED IDEOGRAPH-580D +← (‎ 堍 ‎) 2F854 CJK COMPATIBILITY IDEOGRAPH-2F854 + +# 報 報 + (‎ 報 ‎) 5831 CJK UNIFIED IDEOGRAPH-5831 +← (‎ 報 ‎) 2F857 CJK COMPATIBILITY IDEOGRAPH-2F857 + +# 堲 堲 + (‎ 堲 ‎) 5832 CJK UNIFIED IDEOGRAPH-5832 +← (‎ 堲 ‎) 2F856 CJK COMPATIBILITY IDEOGRAPH-2F856 + +# 塀 塀 + (‎ 塀 ‎) 5840 CJK UNIFIED IDEOGRAPH-5840 +← (‎ 塀 ‎) FA39 CJK COMPATIBILITY IDEOGRAPH-FA39 + +# 塚 塚 塚 + (‎ 塚 ‎) 585A CJK UNIFIED IDEOGRAPH-585A +← (‎ 塚 ‎) FA10 CJK COMPATIBILITY IDEOGRAPH-FA10 +← (‎ 塚 ‎) FA7C CJK COMPATIBILITY IDEOGRAPH-FA7C + +# 塞 塞 + (‎ 塞 ‎) 585E CJK UNIFIED IDEOGRAPH-585E +← (‎ 塞 ‎) F96C CJK COMPATIBILITY IDEOGRAPH-F96C + +# 塡 填 + (‎ 塡 ‎) 5861 CJK UNIFIED IDEOGRAPH-5861 +← (‎ 填 ‎) 586B CJK UNIFIED IDEOGRAPH-586B + +# 墨 墨 + (‎ 墨 ‎) 58A8 CJK UNIFIED IDEOGRAPH-58A8 +← (‎ 墨 ‎) FA3A CJK COMPATIBILITY IDEOGRAPH-FA3A + +# 墫 壿 + (‎ 墫 ‎) 58AB CJK UNIFIED IDEOGRAPH-58AB +← (‎ 壿 ‎) 58FF CJK UNIFIED IDEOGRAPH-58FF + +# 墬 墬 + (‎ 墬 ‎) 58AC CJK UNIFIED IDEOGRAPH-58AC +← (‎ 墬 ‎) 2F858 CJK COMPATIBILITY IDEOGRAPH-2F858 + +# 墳 墳 + (‎ 墳 ‎) 58B3 CJK UNIFIED IDEOGRAPH-58B3 +← (‎ 墳 ‎) FA7D CJK COMPATIBILITY IDEOGRAPH-FA7D + +# 壘 壘 + (‎ 壘 ‎) 58D8 CJK UNIFIED IDEOGRAPH-58D8 +← (‎ 壘 ‎) F94A CJK COMPATIBILITY IDEOGRAPH-F94A + +# 壟 壟 + (‎ 壟 ‎) 58DF CJK UNIFIED IDEOGRAPH-58DF +← (‎ 壟 ‎) F942 CJK COMPATIBILITY IDEOGRAPH-F942 + +# 壮 壮 + (‎ 壮 ‎) 58EE CJK UNIFIED IDEOGRAPH-58EE +← (‎ 壮 ‎) 2F851 CJK COMPATIBILITY IDEOGRAPH-2F851 + +# 売 売 + (‎ 売 ‎) 58F2 CJK UNIFIED IDEOGRAPH-58F2 +← (‎ 売 ‎) 2F85A CJK COMPATIBILITY IDEOGRAPH-2F85A + +# 壷 壷 + (‎ 壷 ‎) 58F7 CJK UNIFIED IDEOGRAPH-58F7 +← (‎ 壷 ‎) 2F85B CJK COMPATIBILITY IDEOGRAPH-2F85B + +# 夆 夆 + (‎ 夆 ‎) 5906 CJK UNIFIED IDEOGRAPH-5906 +← (‎ 夆 ‎) 2F85C CJK COMPATIBILITY IDEOGRAPH-2F85C + +# 多 多 + (‎ 多 ‎) 591A CJK UNIFIED IDEOGRAPH-591A +← (‎ 多 ‎) 2F85D CJK COMPATIBILITY IDEOGRAPH-2F85D + +# 夢 夢 + (‎ 夢 ‎) 5922 CJK UNIFIED IDEOGRAPH-5922 +← (‎ 夢 ‎) 2F85E CJK COMPATIBILITY IDEOGRAPH-2F85E + +# 奄 奄 + (‎ 奄 ‎) 5944 CJK UNIFIED IDEOGRAPH-5944 +← (‎ 奄 ‎) FA7E CJK COMPATIBILITY IDEOGRAPH-FA7E + +# 奈 奈 + (‎ 奈 ‎) 5948 CJK UNIFIED IDEOGRAPH-5948 +← (‎ 奈 ‎) F90C CJK COMPATIBILITY IDEOGRAPH-F90C + +# 契 契 + (‎ 契 ‎) 5951 CJK UNIFIED IDEOGRAPH-5951 +← (‎ 契 ‎) F909 CJK COMPATIBILITY IDEOGRAPH-F909 + +# 奔 奔 + (‎ 奔 ‎) 5954 CJK UNIFIED IDEOGRAPH-5954 +← (‎ 奔 ‎) FA7F CJK COMPATIBILITY IDEOGRAPH-FA7F + +# 奢 奢 + (‎ 奢 ‎) 5962 CJK UNIFIED IDEOGRAPH-5962 +← (‎ 奢 ‎) 2F85F CJK COMPATIBILITY IDEOGRAPH-2F85F + +# 姘 姘 + (‎ 姘 ‎) 59D8 CJK UNIFIED IDEOGRAPH-59D8 +← (‎ 姘 ‎) 2F865 CJK COMPATIBILITY IDEOGRAPH-2F865 + +# 姬 姬 + (‎ 姬 ‎) 59EC CJK UNIFIED IDEOGRAPH-59EC +← (‎ 姬 ‎) 2F862 CJK COMPATIBILITY IDEOGRAPH-2F862 + +# 娛 娛 + (‎ 娛 ‎) 5A1B CJK UNIFIED IDEOGRAPH-5A1B +← (‎ 娛 ‎) 2F863 CJK COMPATIBILITY IDEOGRAPH-2F863 + +# 娧 娧 + (‎ 娧 ‎) 5A27 CJK UNIFIED IDEOGRAPH-5A27 +← (‎ 娧 ‎) 2F864 CJK COMPATIBILITY IDEOGRAPH-2F864 + +# 婢 婢 + (‎ 婢 ‎) 5A62 CJK UNIFIED IDEOGRAPH-5A62 +← (‎ 婢 ‎) FA80 CJK COMPATIBILITY IDEOGRAPH-FA80 + +# 婦 婦 + (‎ 婦 ‎) 5A66 CJK UNIFIED IDEOGRAPH-5A66 +← (‎ 婦 ‎) 2F866 CJK COMPATIBILITY IDEOGRAPH-2F866 + +# 媯 嬀 + (‎ 媯 ‎) 5AAF CJK UNIFIED IDEOGRAPH-5AAF +← (‎ 嬀 ‎) 5B00 CJK UNIFIED IDEOGRAPH-5B00 + +# 媵 媵 + (‎ 媵 ‎) 5AB5 CJK UNIFIED IDEOGRAPH-5AB5 +← (‎ 媵 ‎) 2F986 CJK COMPATIBILITY IDEOGRAPH-2F986 + +# 嬈 嬈 + (‎ 嬈 ‎) 5B08 CJK UNIFIED IDEOGRAPH-5B08 +← (‎ 嬈 ‎) 2F869 CJK COMPATIBILITY IDEOGRAPH-2F869 + +# 嬨 嬨 + (‎ 嬨 ‎) 5B28 CJK UNIFIED IDEOGRAPH-5B28 +← (‎ 嬨 ‎) FA81 CJK COMPATIBILITY IDEOGRAPH-FA81 + +# 嬾 嬾 嬾 + (‎ 嬾 ‎) 5B3E CJK UNIFIED IDEOGRAPH-5B3E +← (‎ 嬾 ‎) 2F86A CJK COMPATIBILITY IDEOGRAPH-2F86A +← (‎ 嬾 ‎) 2F86B CJK COMPATIBILITY IDEOGRAPH-2F86B + +# 宅 宅 + (‎ 宅 ‎) 5B85 CJK UNIFIED IDEOGRAPH-5B85 +← (‎ 宅 ‎) FA04 CJK COMPATIBILITY IDEOGRAPH-FA04 + +# 寃 寃 + (‎ 寃 ‎) 5BC3 CJK UNIFIED IDEOGRAPH-5BC3 +← (‎ 寃 ‎) 2F86D CJK COMPATIBILITY IDEOGRAPH-2F86D + +# 寘 寘 + (‎ 寘 ‎) 5BD8 CJK UNIFIED IDEOGRAPH-5BD8 +← (‎ 寘 ‎) 2F86E CJK COMPATIBILITY IDEOGRAPH-2F86E + +# 寧 寧 寧 寧 + (‎ 寧 ‎) 5BE7 CJK UNIFIED IDEOGRAPH-5BE7 +← (‎ 寧 ‎) F95F CJK COMPATIBILITY IDEOGRAPH-F95F +← (‎ 寧 ‎) F9AA CJK COMPATIBILITY IDEOGRAPH-F9AA +← (‎ 寧 ‎) 2F86F CJK COMPATIBILITY IDEOGRAPH-2F86F + +# 寮 寮 + (‎ 寮 ‎) 5BEE CJK UNIFIED IDEOGRAPH-5BEE +← (‎ 寮 ‎) F9BC CJK COMPATIBILITY IDEOGRAPH-F9BC + +# 寳 寳 + (‎ 寳 ‎) 5BF3 CJK UNIFIED IDEOGRAPH-5BF3 +← (‎ 寳 ‎) 2F870 CJK COMPATIBILITY IDEOGRAPH-2F870 + +# 寿 寿 + (‎ 寿 ‎) 5BFF CJK UNIFIED IDEOGRAPH-5BFF +← (‎ 寿 ‎) 2F872 CJK COMPATIBILITY IDEOGRAPH-2F872 + +# 将 将 + (‎ 将 ‎) 5C06 CJK UNIFIED IDEOGRAPH-5C06 +← (‎ 将 ‎) 2F873 CJK COMPATIBILITY IDEOGRAPH-2F873 + +# 尿 尿 + (‎ 尿 ‎) 5C3F CJK UNIFIED IDEOGRAPH-5C3F +← (‎ 尿 ‎) F9BD CJK COMPATIBILITY IDEOGRAPH-F9BD + +# 屠 屠 + (‎ 屠 ‎) 5C60 CJK UNIFIED IDEOGRAPH-5C60 +← (‎ 屠 ‎) 2F877 CJK COMPATIBILITY IDEOGRAPH-2F877 + +# 屢 屢 + (‎ 屢 ‎) 5C62 CJK UNIFIED IDEOGRAPH-5C62 +← (‎ 屢 ‎) F94B CJK COMPATIBILITY IDEOGRAPH-F94B + +# 層 層 + (‎ 層 ‎) 5C64 CJK UNIFIED IDEOGRAPH-5C64 +← (‎ 層 ‎) FA3B CJK COMPATIBILITY IDEOGRAPH-FA3B + +# 履 履 + (‎ 履 ‎) 5C65 CJK UNIFIED IDEOGRAPH-5C65 +← (‎ 履 ‎) F9DF CJK COMPATIBILITY IDEOGRAPH-F9DF + +# 岍 岍 + (‎ 岍 ‎) 5C8D CJK UNIFIED IDEOGRAPH-5C8D +← (‎ 岍 ‎) 2F87A CJK COMPATIBILITY IDEOGRAPH-2F87A + +# 峀 峀 + (‎ 峀 ‎) 5CC0 CJK UNIFIED IDEOGRAPH-5CC0 +← (‎ 峀 ‎) 2F879 CJK COMPATIBILITY IDEOGRAPH-2F879 + +# 崙 崙 + (‎ 崙 ‎) 5D19 CJK UNIFIED IDEOGRAPH-5D19 +← (‎ 崙 ‎) F9D5 CJK COMPATIBILITY IDEOGRAPH-F9D5 + +# 嵃 嵃 + (‎ 嵃 ‎) 5D43 CJK UNIFIED IDEOGRAPH-5D43 +← (‎ 嵃 ‎) 2F87C CJK COMPATIBILITY IDEOGRAPH-2F87C + +# 嵐 嵐 + (‎ 嵐 ‎) 5D50 CJK UNIFIED IDEOGRAPH-5D50 +← (‎ 嵐 ‎) F921 CJK COMPATIBILITY IDEOGRAPH-F921 + +# 嵫 嵫 + (‎ 嵫 ‎) 5D6B CJK UNIFIED IDEOGRAPH-5D6B +← (‎ 嵫 ‎) 2F87F CJK COMPATIBILITY IDEOGRAPH-2F87F + +# 嵮 嵮 + (‎ 嵮 ‎) 5D6E CJK UNIFIED IDEOGRAPH-5D6E +← (‎ 嵮 ‎) 2F87E CJK COMPATIBILITY IDEOGRAPH-2F87E + +# 嵼 嵼 + (‎ 嵼 ‎) 5D7C CJK UNIFIED IDEOGRAPH-5D7C +← (‎ 嵼 ‎) 2F880 CJK COMPATIBILITY IDEOGRAPH-2F880 + +# 嶲 嶲 + (‎ 嶲 ‎) 5DB2 CJK UNIFIED IDEOGRAPH-5DB2 +← (‎ 嶲 ‎) 2F9F4 CJK COMPATIBILITY IDEOGRAPH-2F9F4 + +# 嶺 嶺 + (‎ 嶺 ‎) 5DBA CJK UNIFIED IDEOGRAPH-5DBA +← (‎ 嶺 ‎) F9AB CJK COMPATIBILITY IDEOGRAPH-F9AB + +# 巡 巡 + (‎ 巡 ‎) 5DE1 CJK UNIFIED IDEOGRAPH-5DE1 +← (‎ 巡 ‎) 2F881 CJK COMPATIBILITY IDEOGRAPH-2F881 + +# 巢 巢 + (‎ 巢 ‎) 5DE2 CJK UNIFIED IDEOGRAPH-5DE2 +← (‎ 巢 ‎) 2F882 CJK COMPATIBILITY IDEOGRAPH-2F882 + +# 巽 巽 + (‎ 巽 ‎) 5DFD CJK UNIFIED IDEOGRAPH-5DFD +← (‎ 巽 ‎) 2F884 CJK COMPATIBILITY IDEOGRAPH-2F884 + +# 帡 帲 + (‎ 帡 ‎) 5E21 CJK UNIFIED IDEOGRAPH-5E21 +← (‎ 帲 ‎) 5E32 CJK UNIFIED IDEOGRAPH-5E32 + +# 帨 帨 + (‎ 帨 ‎) 5E28 CJK UNIFIED IDEOGRAPH-5E28 +← (‎ 帨 ‎) 2F885 CJK COMPATIBILITY IDEOGRAPH-2F885 + +# 帽 帽 + (‎ 帽 ‎) 5E3D CJK UNIFIED IDEOGRAPH-5E3D +← (‎ 帽 ‎) 2F886 CJK COMPATIBILITY IDEOGRAPH-2F886 + +# 幩 幩 + (‎ 幩 ‎) 5E69 CJK UNIFIED IDEOGRAPH-5E69 +← (‎ 幩 ‎) 2F887 CJK COMPATIBILITY IDEOGRAPH-2F887 + +# 年 年 + (‎ 年 ‎) 5E74 CJK UNIFIED IDEOGRAPH-5E74 +← (‎ 年 ‎) F98E CJK COMPATIBILITY IDEOGRAPH-F98E + +# 度 度 + (‎ 度 ‎) 5EA6 CJK UNIFIED IDEOGRAPH-5EA6 +← (‎ 度 ‎) FA01 CJK COMPATIBILITY IDEOGRAPH-FA01 + +# 庰 庰 + (‎ 庰 ‎) 5EB0 CJK UNIFIED IDEOGRAPH-5EB0 +← (‎ 庰 ‎) 2F88B CJK COMPATIBILITY IDEOGRAPH-2F88B + +# 庳 庳 + (‎ 庳 ‎) 5EB3 CJK UNIFIED IDEOGRAPH-5EB3 +← (‎ 庳 ‎) 2F88C CJK COMPATIBILITY IDEOGRAPH-2F88C + +# 庶 庶 + (‎ 庶 ‎) 5EB6 CJK UNIFIED IDEOGRAPH-5EB6 +← (‎ 庶 ‎) 2F88D CJK COMPATIBILITY IDEOGRAPH-2F88D + +# 廉 廉 + (‎ 廉 ‎) 5EC9 CJK UNIFIED IDEOGRAPH-5EC9 +← (‎ 廉 ‎) F9A2 CJK COMPATIBILITY IDEOGRAPH-F9A2 + +# 廊 廊 廊 + (‎ 廊 ‎) 5ECA CJK UNIFIED IDEOGRAPH-5ECA +← (‎ 廊 ‎) F928 CJK COMPATIBILITY IDEOGRAPH-F928 +← (‎ 廊 ‎) 2F88E CJK COMPATIBILITY IDEOGRAPH-2F88E + +# 廒 廒 + (‎ 廒 ‎) 5ED2 CJK UNIFIED IDEOGRAPH-5ED2 +← (‎ 廒 ‎) FA82 CJK COMPATIBILITY IDEOGRAPH-FA82 + +# 廓 廓 + (‎ 廓 ‎) 5ED3 CJK UNIFIED IDEOGRAPH-5ED3 +← (‎ 廓 ‎) FA0B CJK COMPATIBILITY IDEOGRAPH-FA0B + +# 廙 廙 + (‎ 廙 ‎) 5ED9 CJK UNIFIED IDEOGRAPH-5ED9 +← (‎ 廙 ‎) FA83 CJK COMPATIBILITY IDEOGRAPH-FA83 + +# 廬 廬 + (‎ 廬 ‎) 5EEC CJK UNIFIED IDEOGRAPH-5EEC +← (‎ 廬 ‎) F982 CJK COMPATIBILITY IDEOGRAPH-F982 + +# 弄 弄 + (‎ 弄 ‎) 5F04 CJK UNIFIED IDEOGRAPH-5F04 +← (‎ 弄 ‎) F943 CJK COMPATIBILITY IDEOGRAPH-F943 + +# 弢 弢 弢 + (‎ 弢 ‎) 5F22 CJK UNIFIED IDEOGRAPH-5F22 +← (‎ 弢 ‎) 2F894 CJK COMPATIBILITY IDEOGRAPH-2F894 +← (‎ 弢 ‎) 2F895 CJK COMPATIBILITY IDEOGRAPH-2F895 + +# 当 当 + (‎ 当 ‎) 5F53 CJK UNIFIED IDEOGRAPH-5F53 +← (‎ 当 ‎) 2F874 CJK COMPATIBILITY IDEOGRAPH-2F874 + +# 形 形 + (‎ 形 ‎) 5F62 CJK UNIFIED IDEOGRAPH-5F62 +← (‎ 形 ‎) 2F899 CJK COMPATIBILITY IDEOGRAPH-2F899 + +# 彩 彩 + (‎ 彩 ‎) 5F69 CJK UNIFIED IDEOGRAPH-5F69 +← (‎ 彩 ‎) FA84 CJK COMPATIBILITY IDEOGRAPH-FA84 + +# 彫 彫 + (‎ 彫 ‎) 5F6B CJK UNIFIED IDEOGRAPH-5F6B +← (‎ 彫 ‎) 2F89A CJK COMPATIBILITY IDEOGRAPH-2F89A + +# 律 律 + (‎ 律 ‎) 5F8B CJK UNIFIED IDEOGRAPH-5F8B +← (‎ 律 ‎) F9D8 CJK COMPATIBILITY IDEOGRAPH-F9D8 + +# 徚 徚 + (‎ 徚 ‎) 5F9A CJK UNIFIED IDEOGRAPH-5F9A +← (‎ 徚 ‎) 2F89C CJK COMPATIBILITY IDEOGRAPH-2F89C + +# 復 復 + (‎ 復 ‎) 5FA9 CJK UNIFIED IDEOGRAPH-5FA9 +← (‎ 復 ‎) F966 CJK COMPATIBILITY IDEOGRAPH-F966 + +# 徭 徭 + (‎ 徭 ‎) 5FAD CJK UNIFIED IDEOGRAPH-5FAD +← (‎ 徭 ‎) FA85 CJK COMPATIBILITY IDEOGRAPH-FA85 + +# 忍 忍 + (‎ 忍 ‎) 5FCD CJK UNIFIED IDEOGRAPH-5FCD +← (‎ 忍 ‎) 2F89D CJK COMPATIBILITY IDEOGRAPH-2F89D + +# 志 志 + (‎ 志 ‎) 5FD7 CJK UNIFIED IDEOGRAPH-5FD7 +← (‎ 志 ‎) 2F89E CJK COMPATIBILITY IDEOGRAPH-2F89E + +# 念 念 + (‎ 念 ‎) 5FF5 CJK UNIFIED IDEOGRAPH-5FF5 +← (‎ 念 ‎) F9A3 CJK COMPATIBILITY IDEOGRAPH-F9A3 + +# 忹 忹 + (‎ 忹 ‎) 5FF9 CJK UNIFIED IDEOGRAPH-5FF9 +← (‎ 忹 ‎) 2F89F CJK COMPATIBILITY IDEOGRAPH-2F89F + +# 怒 怒 + (‎ 怒 ‎) 6012 CJK UNIFIED IDEOGRAPH-6012 +← (‎ 怒 ‎) F960 CJK COMPATIBILITY IDEOGRAPH-F960 + +# 怜 怜 + (‎ 怜 ‎) 601C CJK UNIFIED IDEOGRAPH-601C +← (‎ 怜 ‎) F9AC CJK COMPATIBILITY IDEOGRAPH-F9AC + +# 恵 恵 + (‎ 恵 ‎) 6075 CJK UNIFIED IDEOGRAPH-6075 +← (‎ 恵 ‎) FA6B CJK COMPATIBILITY IDEOGRAPH-FA6B + +# 悁 悁 + (‎ 悁 ‎) 6081 CJK UNIFIED IDEOGRAPH-6081 +← (‎ 悁 ‎) 2F8A0 CJK COMPATIBILITY IDEOGRAPH-2F8A0 + +# 悔 悔 悔 + (‎ 悔 ‎) 6094 CJK UNIFIED IDEOGRAPH-6094 +← (‎ 悔 ‎) FA3D CJK COMPATIBILITY IDEOGRAPH-FA3D +← (‎ 悔 ‎) 2F8A3 CJK COMPATIBILITY IDEOGRAPH-2F8A3 + +# 惇 惇 + (‎ 惇 ‎) 60C7 CJK UNIFIED IDEOGRAPH-60C7 +← (‎ 惇 ‎) 2F8A5 CJK COMPATIBILITY IDEOGRAPH-2F8A5 + +# 惘 惘 + (‎ 惘 ‎) 60D8 CJK UNIFIED IDEOGRAPH-60D8 +← (‎ 惘 ‎) FA86 CJK COMPATIBILITY IDEOGRAPH-FA86 + +# 惡 惡 + (‎ 惡 ‎) 60E1 CJK UNIFIED IDEOGRAPH-60E1 +← (‎ 惡 ‎) F9B9 CJK COMPATIBILITY IDEOGRAPH-F9B9 + +# 愈 愈 + (‎ 愈 ‎) 6108 CJK UNIFIED IDEOGRAPH-6108 +← (‎ 愈 ‎) FA88 CJK COMPATIBILITY IDEOGRAPH-FA88 + +# 慄 慄 + (‎ 慄 ‎) 6144 CJK UNIFIED IDEOGRAPH-6144 +← (‎ 慄 ‎) F9D9 CJK COMPATIBILITY IDEOGRAPH-F9D9 + +# 慈 慈 + (‎ 慈 ‎) 6148 CJK UNIFIED IDEOGRAPH-6148 +← (‎ 慈 ‎) 2F8A6 CJK COMPATIBILITY IDEOGRAPH-2F8A6 + +# 慌 慌 慌 + (‎ 慌 ‎) 614C CJK UNIFIED IDEOGRAPH-614C +← (‎ 慌 ‎) 2F8A7 CJK COMPATIBILITY IDEOGRAPH-2F8A7 +← (‎ 慌 ‎) 2F8A9 CJK COMPATIBILITY IDEOGRAPH-2F8A9 + +# 慎 慎 慎 + (‎ 慎 ‎) 614E CJK UNIFIED IDEOGRAPH-614E +← (‎ 慎 ‎) FA87 CJK COMPATIBILITY IDEOGRAPH-FA87 +← (‎ 慎 ‎) 2F8A8 CJK COMPATIBILITY IDEOGRAPH-2F8A8 + +# 慠 慠 + (‎ 慠 ‎) 6160 CJK UNIFIED IDEOGRAPH-6160 +← (‎ 慠 ‎) FA8A CJK COMPATIBILITY IDEOGRAPH-FA8A + +# 慨 慨 + (‎ 慨 ‎) 6168 CJK UNIFIED IDEOGRAPH-6168 +← (‎ 慨 ‎) FA3E CJK COMPATIBILITY IDEOGRAPH-FA3E + +# 慺 慺 + (‎ 慺 ‎) 617A CJK UNIFIED IDEOGRAPH-617A +← (‎ 慺 ‎) 2F8AA CJK COMPATIBILITY IDEOGRAPH-2F8AA + +# 憎 憎 憎 憎 + (‎ 憎 ‎) 618E CJK UNIFIED IDEOGRAPH-618E +← (‎ 憎 ‎) FA3F CJK COMPATIBILITY IDEOGRAPH-FA3F +← (‎ 憎 ‎) FA89 CJK COMPATIBILITY IDEOGRAPH-FA89 +← (‎ 憎 ‎) 2F8AB CJK COMPATIBILITY IDEOGRAPH-2F8AB + +# 憐 憐 + (‎ 憐 ‎) 6190 CJK UNIFIED IDEOGRAPH-6190 +← (‎ 憐 ‎) F98F CJK COMPATIBILITY IDEOGRAPH-F98F + +# 憤 憤 + (‎ 憤 ‎) 61A4 CJK UNIFIED IDEOGRAPH-61A4 +← (‎ 憤 ‎) 2F8AD CJK COMPATIBILITY IDEOGRAPH-2F8AD + +# 憯 憯 + (‎ 憯 ‎) 61AF CJK UNIFIED IDEOGRAPH-61AF +← (‎ 憯 ‎) 2F8AE CJK COMPATIBILITY IDEOGRAPH-2F8AE + +# 憲 憲 + (‎ 憲 ‎) 61B2 CJK UNIFIED IDEOGRAPH-61B2 +← (‎ 憲 ‎) 2F8AC CJK COMPATIBILITY IDEOGRAPH-2F8AC + +# 懞 懞 + (‎ 懞 ‎) 61DE CJK UNIFIED IDEOGRAPH-61DE +← (‎ 懞 ‎) 2F8AF CJK COMPATIBILITY IDEOGRAPH-2F8AF + +# 懲 懲 懲 懲 + (‎ 懲 ‎) 61F2 CJK UNIFIED IDEOGRAPH-61F2 +← (‎ 懲 ‎) FA40 CJK COMPATIBILITY IDEOGRAPH-FA40 +← (‎ 懲 ‎) FA8B CJK COMPATIBILITY IDEOGRAPH-FA8B +← (‎ 懲 ‎) 2F8B0 CJK COMPATIBILITY IDEOGRAPH-2F8B0 + +# 懶 懶 懶 + (‎ 懶 ‎) 61F6 CJK UNIFIED IDEOGRAPH-61F6 +← (‎ 懶 ‎) F90D CJK COMPATIBILITY IDEOGRAPH-F90D +← (‎ 懶 ‎) 2F8B1 CJK COMPATIBILITY IDEOGRAPH-2F8B1 + +# 戀 戀 + (‎ 戀 ‎) 6200 CJK UNIFIED IDEOGRAPH-6200 +← (‎ 戀 ‎) F990 CJK COMPATIBILITY IDEOGRAPH-F990 + +# 成 成 + (‎ 成 ‎) 6210 CJK UNIFIED IDEOGRAPH-6210 +← (‎ 成 ‎) 2F8B2 CJK COMPATIBILITY IDEOGRAPH-2F8B2 + +# 戛 戛 + (‎ 戛 ‎) 621B CJK UNIFIED IDEOGRAPH-621B +← (‎ 戛 ‎) 2F8B3 CJK COMPATIBILITY IDEOGRAPH-2F8B3 + +# 戮 戮 + (‎ 戮 ‎) 622E CJK UNIFIED IDEOGRAPH-622E +← (‎ 戮 ‎) F9D2 CJK COMPATIBILITY IDEOGRAPH-F9D2 + +# 戴 戴 + (‎ 戴 ‎) 6234 CJK UNIFIED IDEOGRAPH-6234 +← (‎ 戴 ‎) FA8C CJK COMPATIBILITY IDEOGRAPH-FA8C + +# 扝 扝 + (‎ 扝 ‎) 625D CJK UNIFIED IDEOGRAPH-625D +← (‎ 扝 ‎) 2F8B4 CJK COMPATIBILITY IDEOGRAPH-2F8B4 + +# 抱 抱 + (‎ 抱 ‎) 62B1 CJK UNIFIED IDEOGRAPH-62B1 +← (‎ 抱 ‎) 2F8B5 CJK COMPATIBILITY IDEOGRAPH-2F8B5 + +# 拉 拉 + (‎ 拉 ‎) 62C9 CJK UNIFIED IDEOGRAPH-62C9 +← (‎ 拉 ‎) F925 CJK COMPATIBILITY IDEOGRAPH-F925 + +# 拏 拏 + (‎ 拏 ‎) 62CF CJK UNIFIED IDEOGRAPH-62CF +← (‎ 拏 ‎) F95B CJK COMPATIBILITY IDEOGRAPH-F95B + +# 拓 拓 + (‎ 拓 ‎) 62D3 CJK UNIFIED IDEOGRAPH-62D3 +← (‎ 拓 ‎) FA02 CJK COMPATIBILITY IDEOGRAPH-FA02 + +# 拔 拔 + (‎ 拔 ‎) 62D4 CJK UNIFIED IDEOGRAPH-62D4 +← (‎ 拔 ‎) 2F8B6 CJK COMPATIBILITY IDEOGRAPH-2F8B6 + +# 拼 拼 + (‎ 拼 ‎) 62FC CJK UNIFIED IDEOGRAPH-62FC +← (‎ 拼 ‎) 2F8BA CJK COMPATIBILITY IDEOGRAPH-2F8BA + +# 拾 拾 + (‎ 拾 ‎) 62FE CJK UNIFIED IDEOGRAPH-62FE +← (‎ 拾 ‎) F973 CJK COMPATIBILITY IDEOGRAPH-F973 + +# 挽 挽 + (‎ 挽 ‎) 633D CJK UNIFIED IDEOGRAPH-633D +← (‎ 挽 ‎) 2F8B9 CJK COMPATIBILITY IDEOGRAPH-2F8B9 + +# 捐 捐 + (‎ 捐 ‎) 6350 CJK UNIFIED IDEOGRAPH-6350 +← (‎ 捐 ‎) 2F8B7 CJK COMPATIBILITY IDEOGRAPH-2F8B7 + +# 捨 捨 + (‎ 捨 ‎) 6368 CJK UNIFIED IDEOGRAPH-6368 +← (‎ 捨 ‎) 2F8BB CJK COMPATIBILITY IDEOGRAPH-2F8BB + +# 捻 捻 + (‎ 捻 ‎) 637B CJK UNIFIED IDEOGRAPH-637B +← (‎ 捻 ‎) F9A4 CJK COMPATIBILITY IDEOGRAPH-F9A4 + +# 掃 掃 + (‎ 掃 ‎) 6383 CJK UNIFIED IDEOGRAPH-6383 +← (‎ 掃 ‎) 2F8BC CJK COMPATIBILITY IDEOGRAPH-2F8BC + +# 掠 掠 + (‎ 掠 ‎) 63A0 CJK UNIFIED IDEOGRAPH-63A0 +← (‎ 掠 ‎) F975 CJK COMPATIBILITY IDEOGRAPH-F975 + +# 掩 掩 + (‎ 掩 ‎) 63A9 CJK UNIFIED IDEOGRAPH-63A9 +← (‎ 掩 ‎) 2F8C1 CJK COMPATIBILITY IDEOGRAPH-2F8C1 + +# 揄 揄 + (‎ 揄 ‎) 63C4 CJK UNIFIED IDEOGRAPH-63C4 +← (‎ 揄 ‎) FA8D CJK COMPATIBILITY IDEOGRAPH-FA8D + +# 揅 揅 + (‎ 揅 ‎) 63C5 CJK UNIFIED IDEOGRAPH-63C5 +← (‎ 揅 ‎) 2F8C0 CJK COMPATIBILITY IDEOGRAPH-2F8C0 + +# 揤 揤 + (‎ 揤 ‎) 63E4 CJK UNIFIED IDEOGRAPH-63E4 +← (‎ 揤 ‎) 2F8BD CJK COMPATIBILITY IDEOGRAPH-2F8BD + +# 搜 搜 + (‎ 搜 ‎) 641C CJK UNIFIED IDEOGRAPH-641C +← (‎ 搜 ‎) FA8E CJK COMPATIBILITY IDEOGRAPH-FA8E + +# 搢 搢 + (‎ 搢 ‎) 6422 CJK UNIFIED IDEOGRAPH-6422 +← (‎ 搢 ‎) 2F8BF CJK COMPATIBILITY IDEOGRAPH-2F8BF + +# 摒 摒 + (‎ 摒 ‎) 6452 CJK UNIFIED IDEOGRAPH-6452 +← (‎ 摒 ‎) FA8F CJK COMPATIBILITY IDEOGRAPH-FA8F + +# 摩 摩 + (‎ 摩 ‎) 6469 CJK UNIFIED IDEOGRAPH-6469 +← (‎ 摩 ‎) 2F8C3 CJK COMPATIBILITY IDEOGRAPH-2F8C3 + +# 摷 摷 + (‎ 摷 ‎) 6477 CJK UNIFIED IDEOGRAPH-6477 +← (‎ 摷 ‎) 2F8C6 CJK COMPATIBILITY IDEOGRAPH-2F8C6 + +# 摾 摾 + (‎ 摾 ‎) 647E CJK UNIFIED IDEOGRAPH-647E +← (‎ 摾 ‎) 2F8C4 CJK COMPATIBILITY IDEOGRAPH-2F8C4 + +# 撚 撚 + (‎ 撚 ‎) 649A CJK UNIFIED IDEOGRAPH-649A +← (‎ 撚 ‎) F991 CJK COMPATIBILITY IDEOGRAPH-F991 + +# 撝 撝 + (‎ 撝 ‎) 649D CJK UNIFIED IDEOGRAPH-649D +← (‎ 撝 ‎) 2F8C5 CJK COMPATIBILITY IDEOGRAPH-2F8C5 + +# 擄 擄 + (‎ 擄 ‎) 64C4 CJK UNIFIED IDEOGRAPH-64C4 +← (‎ 擄 ‎) F930 CJK COMPATIBILITY IDEOGRAPH-F930 + +# 敏 敏 敏 + (‎ 敏 ‎) 654F CJK UNIFIED IDEOGRAPH-654F +← (‎ 敏 ‎) FA41 CJK COMPATIBILITY IDEOGRAPH-FA41 +← (‎ 敏 ‎) 2F8C8 CJK COMPATIBILITY IDEOGRAPH-2F8C8 + +# 敖 敖 + (‎ 敖 ‎) 6556 CJK UNIFIED IDEOGRAPH-6556 +← (‎ 敖 ‎) FA90 CJK COMPATIBILITY IDEOGRAPH-FA90 + +# 敬 敬 + (‎ 敬 ‎) 656C CJK UNIFIED IDEOGRAPH-656C +← (‎ 敬 ‎) 2F8C9 CJK COMPATIBILITY IDEOGRAPH-2F8C9 + +# 數 數 + (‎ 數 ‎) 6578 CJK UNIFIED IDEOGRAPH-6578 +← (‎ 數 ‎) F969 CJK COMPATIBILITY IDEOGRAPH-F969 + +# 料 料 + (‎ 料 ‎) 6599 CJK UNIFIED IDEOGRAPH-6599 +← (‎ 料 ‎) F9BE CJK COMPATIBILITY IDEOGRAPH-F9BE + +# 旅 旅 + (‎ 旅 ‎) 65C5 CJK UNIFIED IDEOGRAPH-65C5 +← (‎ 旅 ‎) F983 CJK COMPATIBILITY IDEOGRAPH-F983 + +# 既 既 + (‎ 既 ‎) 65E2 CJK UNIFIED IDEOGRAPH-65E2 +← (‎ 既 ‎) FA42 CJK COMPATIBILITY IDEOGRAPH-FA42 + +# 旣 旣 + (‎ 旣 ‎) 65E3 CJK UNIFIED IDEOGRAPH-65E3 +← (‎ 旣 ‎) 2F8CB CJK COMPATIBILITY IDEOGRAPH-2F8CB + +# 易 易 + (‎ 易 ‎) 6613 CJK UNIFIED IDEOGRAPH-6613 +← (‎ 易 ‎) F9E0 CJK COMPATIBILITY IDEOGRAPH-F9E0 + +# 晉 晉 + (‎ 晉 ‎) 6649 CJK UNIFIED IDEOGRAPH-6649 +← (‎ 晉 ‎) 2F8CD CJK COMPATIBILITY IDEOGRAPH-2F8CD + +# 晚 晩 + (‎ 晚 ‎) 665A CJK UNIFIED IDEOGRAPH-665A +← (‎ 晩 ‎) 6669 CJK UNIFIED IDEOGRAPH-6669 + +# 晴 晴 晴 + (‎ 晴 ‎) 6674 CJK UNIFIED IDEOGRAPH-6674 +← (‎ 晴 ‎) FA12 CJK COMPATIBILITY IDEOGRAPH-FA12 +← (‎ 晴 ‎) FA91 CJK COMPATIBILITY IDEOGRAPH-FA91 + +# 暈 暈 + (‎ 暈 ‎) 6688 CJK UNIFIED IDEOGRAPH-6688 +← (‎ 暈 ‎) F9C5 CJK COMPATIBILITY IDEOGRAPH-F9C5 + +# 暑 暑 暑 + (‎ 暑 ‎) 6691 CJK UNIFIED IDEOGRAPH-6691 +← (‎ 暑 ‎) FA43 CJK COMPATIBILITY IDEOGRAPH-FA43 +← (‎ 暑 ‎) 2F8CF CJK COMPATIBILITY IDEOGRAPH-2F8CF + +# 暜 暜 + (‎ 暜 ‎) 669C CJK UNIFIED IDEOGRAPH-669C +← (‎ 暜 ‎) 2F8D5 CJK COMPATIBILITY IDEOGRAPH-2F8D5 + +# 暴 暴 + (‎ 暴 ‎) 66B4 CJK UNIFIED IDEOGRAPH-66B4 +← (‎ 暴 ‎) FA06 CJK COMPATIBILITY IDEOGRAPH-FA06 + +# 曆 曆 + (‎ 曆 ‎) 66C6 CJK UNIFIED IDEOGRAPH-66C6 +← (‎ 曆 ‎) F98B CJK COMPATIBILITY IDEOGRAPH-F98B + +# 更 更 + (‎ 更 ‎) 66F4 CJK UNIFIED IDEOGRAPH-66F4 +← (‎ 更 ‎) F901 CJK COMPATIBILITY IDEOGRAPH-F901 + +# 書 書 + (‎ 書 ‎) 66F8 CJK UNIFIED IDEOGRAPH-66F8 +← (‎ 書 ‎) 2F8CC CJK COMPATIBILITY IDEOGRAPH-2F8CC + +# 最 最 + (‎ 最 ‎) 6700 CJK UNIFIED IDEOGRAPH-6700 +← (‎ 最 ‎) 2F8D4 CJK COMPATIBILITY IDEOGRAPH-2F8D4 + +# 朌 肦 + (‎ 朌 ‎) 670C CJK UNIFIED IDEOGRAPH-670C +← (‎ 肦 ‎) 80A6 CJK UNIFIED IDEOGRAPH-80A6 + +# 朏 胐 + (‎ 朏 ‎) 670F CJK UNIFIED IDEOGRAPH-670F +← (‎ 胐 ‎) 80D0 CJK UNIFIED IDEOGRAPH-80D0 + +# 朐 胊 + (‎ 朐 ‎) 6710 CJK UNIFIED IDEOGRAPH-6710 +← (‎ 胊 ‎) 80CA CJK UNIFIED IDEOGRAPH-80CA + +# 朓 脁 + (‎ 朓 ‎) 6713 CJK UNIFIED IDEOGRAPH-6713 +← (‎ 脁 ‎) 8101 CJK UNIFIED IDEOGRAPH-8101 + +# 朗 朗 朗 朗 + (‎ 朗 ‎) 6717 CJK UNIFIED IDEOGRAPH-6717 +← (‎ 朗 ‎) F929 CJK COMPATIBILITY IDEOGRAPH-F929 +← (‎ 朗 ‎) FA92 CJK COMPATIBILITY IDEOGRAPH-FA92 +← (‎ 朗 ‎) 2F8D8 CJK COMPATIBILITY IDEOGRAPH-2F8D8 + +# 朘 脧 + (‎ 朘 ‎) 6718 CJK UNIFIED IDEOGRAPH-6718 +← (‎ 脧 ‎) 8127 CJK UNIFIED IDEOGRAPH-8127 + +# 望 望 望 + (‎ 望 ‎) 671B CJK UNIFIED IDEOGRAPH-671B +← (‎ 望 ‎) FA93 CJK COMPATIBILITY IDEOGRAPH-FA93 +← (‎ 望 ‎) 2F8D9 CJK COMPATIBILITY IDEOGRAPH-2F8D9 + +# 朡 朡 + (‎ 朡 ‎) 6721 CJK UNIFIED IDEOGRAPH-6721 +← (‎ 朡 ‎) 2F8DA CJK COMPATIBILITY IDEOGRAPH-2F8DA + +# 朣 膧 + (‎ 朣 ‎) 6723 CJK UNIFIED IDEOGRAPH-6723 +← (‎ 膧 ‎) 81A7 CJK UNIFIED IDEOGRAPH-81A7 + +# 李 李 + (‎ 李 ‎) 674E CJK UNIFIED IDEOGRAPH-674E +← (‎ 李 ‎) F9E1 CJK COMPATIBILITY IDEOGRAPH-F9E1 + +# 杓 杓 + (‎ 杓 ‎) 6753 CJK UNIFIED IDEOGRAPH-6753 +← (‎ 杓 ‎) 2F8DC CJK COMPATIBILITY IDEOGRAPH-2F8DC + +# 杖 杖 + (‎ 杖 ‎) 6756 CJK UNIFIED IDEOGRAPH-6756 +← (‎ 杖 ‎) FA94 CJK COMPATIBILITY IDEOGRAPH-FA94 + +# 杞 杞 + (‎ 杞 ‎) 675E CJK UNIFIED IDEOGRAPH-675E +← (‎ 杞 ‎) 2F8DB CJK COMPATIBILITY IDEOGRAPH-2F8DB + +# 杮 柿 + (‎ 杮 ‎) 676E CJK UNIFIED IDEOGRAPH-676E +← (‎ 柿 ‎) 67FF CJK UNIFIED IDEOGRAPH-67FF + +# 杻 杻 + (‎ 杻 ‎) 677B CJK UNIFIED IDEOGRAPH-677B +← (‎ 杻 ‎) F9C8 CJK COMPATIBILITY IDEOGRAPH-F9C8 + +# 枅 枅 + (‎ 枅 ‎) 6785 CJK UNIFIED IDEOGRAPH-6785 +← (‎ 枅 ‎) 2F8E0 CJK COMPATIBILITY IDEOGRAPH-2F8E0 + +# 林 林 + (‎ 林 ‎) 6797 CJK UNIFIED IDEOGRAPH-6797 +← (‎ 林 ‎) F9F4 CJK COMPATIBILITY IDEOGRAPH-F9F4 + +# 柳 柳 + (‎ 柳 ‎) 67F3 CJK UNIFIED IDEOGRAPH-67F3 +← (‎ 柳 ‎) F9C9 CJK COMPATIBILITY IDEOGRAPH-F9C9 + +# 柺 柺 + (‎ 柺 ‎) 67FA CJK UNIFIED IDEOGRAPH-67FA +← (‎ 柺 ‎) 2F8DF CJK COMPATIBILITY IDEOGRAPH-2F8DF + +# 栗 栗 + (‎ 栗 ‎) 6817 CJK UNIFIED IDEOGRAPH-6817 +← (‎ 栗 ‎) F9DA CJK COMPATIBILITY IDEOGRAPH-F9DA + +# 栟 栟 + (‎ 栟 ‎) 681F CJK UNIFIED IDEOGRAPH-681F +← (‎ 栟 ‎) 2F8E5 CJK COMPATIBILITY IDEOGRAPH-2F8E5 + +# 桒 桒 + (‎ 桒 ‎) 6852 CJK UNIFIED IDEOGRAPH-6852 +← (‎ 桒 ‎) 2F8E1 CJK COMPATIBILITY IDEOGRAPH-2F8E1 + +# 梁 梁 + (‎ 梁 ‎) 6881 CJK UNIFIED IDEOGRAPH-6881 +← (‎ 梁 ‎) F97A CJK COMPATIBILITY IDEOGRAPH-F97A + +# 梅 梅 梅 + (‎ 梅 ‎) 6885 CJK UNIFIED IDEOGRAPH-6885 +← (‎ 梅 ‎) FA44 CJK COMPATIBILITY IDEOGRAPH-FA44 +← (‎ 梅 ‎) 2F8E2 CJK COMPATIBILITY IDEOGRAPH-2F8E2 + +# 梎 梎 + (‎ 梎 ‎) 688E CJK UNIFIED IDEOGRAPH-688E +← (‎ 梎 ‎) 2F8E4 CJK COMPATIBILITY IDEOGRAPH-2F8E4 + +# 梨 梨 + (‎ 梨 ‎) 68A8 CJK UNIFIED IDEOGRAPH-68A8 +← (‎ 梨 ‎) F9E2 CJK COMPATIBILITY IDEOGRAPH-F9E2 + +# 椔 椔 + (‎ 椔 ‎) 6914 CJK UNIFIED IDEOGRAPH-6914 +← (‎ 椔 ‎) 2F8E6 CJK COMPATIBILITY IDEOGRAPH-2F8E6 + +# 楂 楂 + (‎ 楂 ‎) 6942 CJK UNIFIED IDEOGRAPH-6942 +← (‎ 楂 ‎) 2F8E8 CJK COMPATIBILITY IDEOGRAPH-2F8E8 + +# 榝 樧 + (‎ 榝 ‎) 699D CJK UNIFIED IDEOGRAPH-699D +← (‎ 樧 ‎) 6A27 CJK UNIFIED IDEOGRAPH-6A27 + +# 榣 榣 + (‎ 榣 ‎) 69A3 CJK UNIFIED IDEOGRAPH-69A3 +← (‎ 榣 ‎) 2F8E9 CJK COMPATIBILITY IDEOGRAPH-2F8E9 + +# 槪 槪 + (‎ 槪 ‎) 69EA CJK UNIFIED IDEOGRAPH-69EA +← (‎ 槪 ‎) 2F8EA CJK COMPATIBILITY IDEOGRAPH-2F8EA + +# 樂 樂 樂 樂 + (‎ 樂 ‎) 6A02 CJK UNIFIED IDEOGRAPH-6A02 +← (‎ 樂 ‎) F914 CJK COMPATIBILITY IDEOGRAPH-F914 +← (‎ 樂 ‎) F95C CJK COMPATIBILITY IDEOGRAPH-F95C +← (‎ 樂 ‎) F9BF CJK COMPATIBILITY IDEOGRAPH-F9BF + +# 樓 樓 + (‎ 樓 ‎) 6A13 CJK UNIFIED IDEOGRAPH-6A13 +← (‎ 樓 ‎) F94C CJK COMPATIBILITY IDEOGRAPH-F94C + +# 檨 檨 + (‎ 檨 ‎) 6AA8 CJK UNIFIED IDEOGRAPH-6AA8 +← (‎ 檨 ‎) 2F8EB CJK COMPATIBILITY IDEOGRAPH-2F8EB + +# 櫓 櫓 + (‎ 櫓 ‎) 6AD3 CJK UNIFIED IDEOGRAPH-6AD3 +← (‎ 櫓 ‎) F931 CJK COMPATIBILITY IDEOGRAPH-F931 + +# 櫛 櫛 + (‎ 櫛 ‎) 6ADB CJK UNIFIED IDEOGRAPH-6ADB +← (‎ 櫛 ‎) 2F8ED CJK COMPATIBILITY IDEOGRAPH-2F8ED + +# 欄 欄 + (‎ 欄 ‎) 6B04 CJK UNIFIED IDEOGRAPH-6B04 +← (‎ 欄 ‎) F91D CJK COMPATIBILITY IDEOGRAPH-F91D + +# 次 次 + (‎ 次 ‎) 6B21 CJK UNIFIED IDEOGRAPH-6B21 +← (‎ 次 ‎) 2F8EF CJK COMPATIBILITY IDEOGRAPH-2F8EF + +# 歔 歔 + (‎ 歔 ‎) 6B54 CJK UNIFIED IDEOGRAPH-6B54 +← (‎ 歔 ‎) 2F8F1 CJK COMPATIBILITY IDEOGRAPH-2F8F1 + +# 歲 歲 + (‎ 歲 ‎) 6B72 CJK UNIFIED IDEOGRAPH-6B72 +← (‎ 歲 ‎) 2F8F3 CJK COMPATIBILITY IDEOGRAPH-2F8F3 + +# 歷 歷 + (‎ 歷 ‎) 6B77 CJK UNIFIED IDEOGRAPH-6B77 +← (‎ 歷 ‎) F98C CJK COMPATIBILITY IDEOGRAPH-F98C + +# 殟 殟 + (‎ 殟 ‎) 6B9F CJK UNIFIED IDEOGRAPH-6B9F +← (‎ 殟 ‎) 2F8F4 CJK COMPATIBILITY IDEOGRAPH-2F8F4 + +# 殮 殮 + (‎ 殮 ‎) 6BAE CJK UNIFIED IDEOGRAPH-6BAE +← (‎ 殮 ‎) F9A5 CJK COMPATIBILITY IDEOGRAPH-F9A5 + +# 殺 殺 殺 殺 + (‎ 殺 ‎) 6BBA CJK UNIFIED IDEOGRAPH-6BBA +← (‎ 殺 ‎) F970 CJK COMPATIBILITY IDEOGRAPH-F970 +← (‎ 殺 ‎) FA96 CJK COMPATIBILITY IDEOGRAPH-FA96 +← (‎ 殺 ‎) 2F8F5 CJK COMPATIBILITY IDEOGRAPH-2F8F5 + +# 殻 殻 + (‎ 殻 ‎) 6BBB CJK UNIFIED IDEOGRAPH-6BBB +← (‎ 殻 ‎) 2F8F6 CJK COMPATIBILITY IDEOGRAPH-2F8F6 + +# 汎 汎 + (‎ 汎 ‎) 6C4E CJK UNIFIED IDEOGRAPH-6C4E +← (‎ 汎 ‎) 2F8FA CJK COMPATIBILITY IDEOGRAPH-2F8FA + +# 汧 汧 + (‎ 汧 ‎) 6C67 CJK UNIFIED IDEOGRAPH-6C67 +← (‎ 汧 ‎) 2F8FE CJK COMPATIBILITY IDEOGRAPH-2F8FE + +# 沈 沈 + (‎ 沈 ‎) 6C88 CJK UNIFIED IDEOGRAPH-6C88 +← (‎ 沈 ‎) F972 CJK COMPATIBILITY IDEOGRAPH-F972 + +# 沿 沿 + (‎ 沿 ‎) 6CBF CJK UNIFIED IDEOGRAPH-6CBF +← (‎ 沿 ‎) 2F8FC CJK COMPATIBILITY IDEOGRAPH-2F8FC + +# 泌 泌 + (‎ 泌 ‎) 6CCC CJK UNIFIED IDEOGRAPH-6CCC +← (‎ 泌 ‎) F968 CJK COMPATIBILITY IDEOGRAPH-F968 + +# 泍 泍 + (‎ 泍 ‎) 6CCD CJK UNIFIED IDEOGRAPH-6CCD +← (‎ 泍 ‎) 2F8FD CJK COMPATIBILITY IDEOGRAPH-2F8FD + +# 泥 泥 + (‎ 泥 ‎) 6CE5 CJK UNIFIED IDEOGRAPH-6CE5 +← (‎ 泥 ‎) F9E3 CJK COMPATIBILITY IDEOGRAPH-F9E3 + +# 洖 洖 + (‎ 洖 ‎) 6D16 CJK UNIFIED IDEOGRAPH-6D16 +← (‎ 洖 ‎) 2F8FF CJK COMPATIBILITY IDEOGRAPH-2F8FF + +# 洛 洛 + (‎ 洛 ‎) 6D1B CJK UNIFIED IDEOGRAPH-6D1B +← (‎ 洛 ‎) F915 CJK COMPATIBILITY IDEOGRAPH-F915 + +# 洞 洞 + (‎ 洞 ‎) 6D1E CJK UNIFIED IDEOGRAPH-6D1E +← (‎ 洞 ‎) FA05 CJK COMPATIBILITY IDEOGRAPH-FA05 + +# 洴 洴 + (‎ 洴 ‎) 6D34 CJK UNIFIED IDEOGRAPH-6D34 +← (‎ 洴 ‎) 2F907 CJK COMPATIBILITY IDEOGRAPH-2F907 + +# 派 派 + (‎ 派 ‎) 6D3E CJK UNIFIED IDEOGRAPH-6D3E +← (‎ 派 ‎) 2F900 CJK COMPATIBILITY IDEOGRAPH-2F900 + +# 流 流 流 流 + (‎ 流 ‎) 6D41 CJK UNIFIED IDEOGRAPH-6D41 +← (‎ 流 ‎) F9CA CJK COMPATIBILITY IDEOGRAPH-F9CA +← (‎ 流 ‎) FA97 CJK COMPATIBILITY IDEOGRAPH-FA97 +← (‎ 流 ‎) 2F902 CJK COMPATIBILITY IDEOGRAPH-2F902 + +# 浩 浩 + (‎ 浩 ‎) 6D69 CJK UNIFIED IDEOGRAPH-6D69 +← (‎ 浩 ‎) 2F903 CJK COMPATIBILITY IDEOGRAPH-2F903 + +# 浪 浪 + (‎ 浪 ‎) 6D6A CJK UNIFIED IDEOGRAPH-6D6A +← (‎ 浪 ‎) F92A CJK COMPATIBILITY IDEOGRAPH-F92A + +# 海 海 海 + (‎ 海 ‎) 6D77 CJK UNIFIED IDEOGRAPH-6D77 +← (‎ 海 ‎) FA45 CJK COMPATIBILITY IDEOGRAPH-FA45 +← (‎ 海 ‎) 2F901 CJK COMPATIBILITY IDEOGRAPH-2F901 + +# 浸 浸 + (‎ 浸 ‎) 6D78 CJK UNIFIED IDEOGRAPH-6D78 +← (‎ 浸 ‎) 2F904 CJK COMPATIBILITY IDEOGRAPH-2F904 + +# 涅 涅 + (‎ 涅 ‎) 6D85 CJK UNIFIED IDEOGRAPH-6D85 +← (‎ 涅 ‎) 2F905 CJK COMPATIBILITY IDEOGRAPH-2F905 + +# 淋 淋 + (‎ 淋 ‎) 6DCB CJK UNIFIED IDEOGRAPH-6DCB +← (‎ 淋 ‎) F9F5 CJK COMPATIBILITY IDEOGRAPH-F9F5 + +# 淚 淚 + (‎ 淚 ‎) 6DDA CJK UNIFIED IDEOGRAPH-6DDA +← (‎ 淚 ‎) F94D CJK COMPATIBILITY IDEOGRAPH-F94D + +# 淪 淪 + (‎ 淪 ‎) 6DEA CJK UNIFIED IDEOGRAPH-6DEA +← (‎ 淪 ‎) F9D6 CJK COMPATIBILITY IDEOGRAPH-F9D6 + +# 淹 淹 + (‎ 淹 ‎) 6DF9 CJK UNIFIED IDEOGRAPH-6DF9 +← (‎ 淹 ‎) 2F90E CJK COMPATIBILITY IDEOGRAPH-2F90E + +# 渚 渚 + (‎ 渚 ‎) 6E1A CJK UNIFIED IDEOGRAPH-6E1A +← (‎ 渚 ‎) FA46 CJK COMPATIBILITY IDEOGRAPH-FA46 + +# 港 港 + (‎ 港 ‎) 6E2F CJK UNIFIED IDEOGRAPH-6E2F +← (‎ 港 ‎) 2F908 CJK COMPATIBILITY IDEOGRAPH-2F908 + +# 湮 湮 + (‎ 湮 ‎) 6E6E CJK UNIFIED IDEOGRAPH-6E6E +← (‎ 湮 ‎) 2F909 CJK COMPATIBILITY IDEOGRAPH-2F909 + +# 溈 潙 + (‎ 溈 ‎) 6E88 CJK UNIFIED IDEOGRAPH-6E88 +← (‎ 潙 ‎) 6F59 CJK UNIFIED IDEOGRAPH-6F59 + +# 溜 溜 + (‎ 溜 ‎) 6E9C CJK UNIFIED IDEOGRAPH-6E9C +← (‎ 溜 ‎) F9CB CJK COMPATIBILITY IDEOGRAPH-F9CB + +# 溺 溺 + (‎ 溺 ‎) 6EBA CJK UNIFIED IDEOGRAPH-6EBA +← (‎ 溺 ‎) F9EC CJK COMPATIBILITY IDEOGRAPH-F9EC + +# 滇 滇 + (‎ 滇 ‎) 6EC7 CJK UNIFIED IDEOGRAPH-6EC7 +← (‎ 滇 ‎) 2F90C CJK COMPATIBILITY IDEOGRAPH-2F90C + +# 滋 滋 滋 + (‎ 滋 ‎) 6ECB CJK UNIFIED IDEOGRAPH-6ECB +← (‎ 滋 ‎) FA99 CJK COMPATIBILITY IDEOGRAPH-FA99 +← (‎ 滋 ‎) 2F90B CJK COMPATIBILITY IDEOGRAPH-2F90B + +# 滑 滑 + (‎ 滑 ‎) 6ED1 CJK UNIFIED IDEOGRAPH-6ED1 +← (‎ 滑 ‎) F904 CJK COMPATIBILITY IDEOGRAPH-F904 + +# 滛 滛 + (‎ 滛 ‎) 6EDB CJK UNIFIED IDEOGRAPH-6EDB +← (‎ 滛 ‎) FA98 CJK COMPATIBILITY IDEOGRAPH-FA98 + +# 漏 漏 + (‎ 漏 ‎) 6F0F CJK UNIFIED IDEOGRAPH-6F0F +← (‎ 漏 ‎) F94E CJK COMPATIBILITY IDEOGRAPH-F94E + +# 漢 漢 漢 + (‎ 漢 ‎) 6F22 CJK UNIFIED IDEOGRAPH-6F22 +← (‎ 漢 ‎) FA47 CJK COMPATIBILITY IDEOGRAPH-FA47 +← (‎ 漢 ‎) FA9A CJK COMPATIBILITY IDEOGRAPH-FA9A + +# 漣 漣 + (‎ 漣 ‎) 6F23 CJK UNIFIED IDEOGRAPH-6F23 +← (‎ 漣 ‎) F992 CJK COMPATIBILITY IDEOGRAPH-F992 + +# 潮 潮 + (‎ 潮 ‎) 6F6E CJK UNIFIED IDEOGRAPH-6F6E +← (‎ 潮 ‎) 2F90F CJK COMPATIBILITY IDEOGRAPH-2F90F + +# 濆 濆 + (‎ 濆 ‎) 6FC6 CJK UNIFIED IDEOGRAPH-6FC6 +← (‎ 濆 ‎) 2F912 CJK COMPATIBILITY IDEOGRAPH-2F912 + +# 濫 濫 + (‎ 濫 ‎) 6FEB CJK UNIFIED IDEOGRAPH-6FEB +← (‎ 濫 ‎) F922 CJK COMPATIBILITY IDEOGRAPH-F922 + +# 濾 濾 + (‎ 濾 ‎) 6FFE CJK UNIFIED IDEOGRAPH-6FFE +← (‎ 濾 ‎) F984 CJK COMPATIBILITY IDEOGRAPH-F984 + +# 瀛 瀛 + (‎ 瀛 ‎) 701B CJK UNIFIED IDEOGRAPH-701B +← (‎ 瀛 ‎) 2F915 CJK COMPATIBILITY IDEOGRAPH-2F915 + +# 瀞 瀞 瀞 + (‎ 瀞 ‎) 701E CJK UNIFIED IDEOGRAPH-701E +← (‎ 瀞 ‎) FA9B CJK COMPATIBILITY IDEOGRAPH-FA9B +← (‎ 瀞 ‎) 2F914 CJK COMPATIBILITY IDEOGRAPH-2F914 + +# 瀹 瀹 + (‎ 瀹 ‎) 7039 CJK UNIFIED IDEOGRAPH-7039 +← (‎ 瀹 ‎) 2F913 CJK COMPATIBILITY IDEOGRAPH-2F913 + +# 灊 灊 + (‎ 灊 ‎) 704A CJK UNIFIED IDEOGRAPH-704A +← (‎ 灊 ‎) 2F917 CJK COMPATIBILITY IDEOGRAPH-2F917 + +# 灰 灰 + (‎ 灰 ‎) 7070 CJK UNIFIED IDEOGRAPH-7070 +← (‎ 灰 ‎) 2F835 CJK COMPATIBILITY IDEOGRAPH-2F835 + +# 灷 灷 + (‎ 灷 ‎) 7077 CJK UNIFIED IDEOGRAPH-7077 +← (‎ 灷 ‎) 2F919 CJK COMPATIBILITY IDEOGRAPH-2F919 + +# 災 災 + (‎ 災 ‎) 707D CJK UNIFIED IDEOGRAPH-707D +← (‎ 災 ‎) 2F918 CJK COMPATIBILITY IDEOGRAPH-2F918 + +# 炙 炙 + (‎ 炙 ‎) 7099 CJK UNIFIED IDEOGRAPH-7099 +← (‎ 炙 ‎) F9FB CJK COMPATIBILITY IDEOGRAPH-F9FB + +# 炭 炭 + (‎ 炭 ‎) 70AD CJK UNIFIED IDEOGRAPH-70AD +← (‎ 炭 ‎) 2F91A CJK COMPATIBILITY IDEOGRAPH-2F91A + +# 烈 烈 + (‎ 烈 ‎) 70C8 CJK UNIFIED IDEOGRAPH-70C8 +← (‎ 烈 ‎) F99F CJK COMPATIBILITY IDEOGRAPH-F99F + +# 烙 烙 + (‎ 烙 ‎) 70D9 CJK UNIFIED IDEOGRAPH-70D9 +← (‎ 烙 ‎) F916 CJK COMPATIBILITY IDEOGRAPH-F916 + +# 煅 煅 + (‎ 煅 ‎) 7145 CJK UNIFIED IDEOGRAPH-7145 +← (‎ 煅 ‎) 2F91C CJK COMPATIBILITY IDEOGRAPH-2F91C + +# 煉 煉 + (‎ 煉 ‎) 7149 CJK UNIFIED IDEOGRAPH-7149 +← (‎ 煉 ‎) F993 CJK COMPATIBILITY IDEOGRAPH-F993 + +# 煮 煮 煮 + (‎ 煮 ‎) 716E CJK UNIFIED IDEOGRAPH-716E +← (‎ 煮 ‎) FA48 CJK COMPATIBILITY IDEOGRAPH-FA48 +← (‎ 煮 ‎) FA9C CJK COMPATIBILITY IDEOGRAPH-FA9C + +# 熜 熜 + (‎ 熜 ‎) 719C CJK UNIFIED IDEOGRAPH-719C +← (‎ 熜 ‎) 2F91E CJK COMPATIBILITY IDEOGRAPH-2F91E + +# 燎 燎 + (‎ 燎 ‎) 71CE CJK UNIFIED IDEOGRAPH-71CE +← (‎ 燎 ‎) F9C0 CJK COMPATIBILITY IDEOGRAPH-F9C0 + +# 燐 燐 + (‎ 燐 ‎) 71D0 CJK UNIFIED IDEOGRAPH-71D0 +← (‎ 燐 ‎) F9EE CJK COMPATIBILITY IDEOGRAPH-F9EE + +# 爐 爐 + (‎ 爐 ‎) 7210 CJK UNIFIED IDEOGRAPH-7210 +← (‎ 爐 ‎) F932 CJK COMPATIBILITY IDEOGRAPH-F932 + +# 爛 爛 + (‎ 爛 ‎) 721B CJK UNIFIED IDEOGRAPH-721B +← (‎ 爛 ‎) F91E CJK COMPATIBILITY IDEOGRAPH-F91E + +# 爨 爨 + (‎ 爨 ‎) 7228 CJK UNIFIED IDEOGRAPH-7228 +← (‎ 爨 ‎) 2F920 CJK COMPATIBILITY IDEOGRAPH-2F920 + +# 爵 爵 爵 + (‎ 爵 ‎) 7235 CJK UNIFIED IDEOGRAPH-7235 +← (‎ 爵 ‎) FA9E CJK COMPATIBILITY IDEOGRAPH-FA9E +← (‎ 爵 ‎) 2F921 CJK COMPATIBILITY IDEOGRAPH-2F921 + +# 牐 牐 + (‎ 牐 ‎) 7250 CJK UNIFIED IDEOGRAPH-7250 +← (‎ 牐 ‎) 2F922 CJK COMPATIBILITY IDEOGRAPH-2F922 + +# 牢 牢 + (‎ 牢 ‎) 7262 CJK UNIFIED IDEOGRAPH-7262 +← (‎ 牢 ‎) F946 CJK COMPATIBILITY IDEOGRAPH-F946 + +# 犀 犀 + (‎ 犀 ‎) 7280 CJK UNIFIED IDEOGRAPH-7280 +← (‎ 犀 ‎) 2F924 CJK COMPATIBILITY IDEOGRAPH-2F924 + +# 犕 犕 + (‎ 犕 ‎) 7295 CJK UNIFIED IDEOGRAPH-7295 +← (‎ 犕 ‎) 2F925 CJK COMPATIBILITY IDEOGRAPH-2F925 + +# 犯 犯 + (‎ 犯 ‎) 72AF CJK UNIFIED IDEOGRAPH-72AF +← (‎ 犯 ‎) FA9F CJK COMPATIBILITY IDEOGRAPH-FA9F + +# 狀 狀 + (‎ 狀 ‎) 72C0 CJK UNIFIED IDEOGRAPH-72C0 +← (‎ 狀 ‎) F9FA CJK COMPATIBILITY IDEOGRAPH-F9FA + +# 狼 狼 + (‎ 狼 ‎) 72FC CJK UNIFIED IDEOGRAPH-72FC +← (‎ 狼 ‎) F92B CJK COMPATIBILITY IDEOGRAPH-F92B + +# 猪 猪 猪 + (‎ 猪 ‎) 732A CJK UNIFIED IDEOGRAPH-732A +← (‎ 猪 ‎) FA16 CJK COMPATIBILITY IDEOGRAPH-FA16 +← (‎ 猪 ‎) FAA0 CJK COMPATIBILITY IDEOGRAPH-FAA0 + +# 獵 獵 + (‎ 獵 ‎) 7375 CJK UNIFIED IDEOGRAPH-7375 +← (‎ 獵 ‎) F9A7 CJK COMPATIBILITY IDEOGRAPH-F9A7 + +# 獺 獺 + (‎ 獺 ‎) 737A CJK UNIFIED IDEOGRAPH-737A +← (‎ 獺 ‎) 2F928 CJK COMPATIBILITY IDEOGRAPH-2F928 + +# 率 率 率 + (‎ 率 ‎) 7387 CJK UNIFIED IDEOGRAPH-7387 +← (‎ 率 ‎) F961 CJK COMPATIBILITY IDEOGRAPH-F961 +← (‎ 率 ‎) F9DB CJK COMPATIBILITY IDEOGRAPH-F9DB + +# 王 王 + (‎ 王 ‎) 738B CJK UNIFIED IDEOGRAPH-738B +← (‎ 王 ‎) 2F929 CJK COMPATIBILITY IDEOGRAPH-2F929 + +# 玥 玥 + (‎ 玥 ‎) 73A5 CJK UNIFIED IDEOGRAPH-73A5 +← (‎ 玥 ‎) 2F92B CJK COMPATIBILITY IDEOGRAPH-2F92B + +# 玲 玲 + (‎ 玲 ‎) 73B2 CJK UNIFIED IDEOGRAPH-73B2 +← (‎ 玲 ‎) F9AD CJK COMPATIBILITY IDEOGRAPH-F9AD + +# 珞 珞 + (‎ 珞 ‎) 73DE CJK UNIFIED IDEOGRAPH-73DE +← (‎ 珞 ‎) F917 CJK COMPATIBILITY IDEOGRAPH-F917 + +# 理 理 + (‎ 理 ‎) 7406 CJK UNIFIED IDEOGRAPH-7406 +← (‎ 理 ‎) F9E4 CJK COMPATIBILITY IDEOGRAPH-F9E4 + +# 琉 琉 + (‎ 琉 ‎) 7409 CJK UNIFIED IDEOGRAPH-7409 +← (‎ 琉 ‎) F9CC CJK COMPATIBILITY IDEOGRAPH-F9CC + +# 琢 琢 + (‎ 琢 ‎) 7422 CJK UNIFIED IDEOGRAPH-7422 +← (‎ 琢 ‎) FA4A CJK COMPATIBILITY IDEOGRAPH-FA4A + +# 瑇 瑇 + (‎ 瑇 ‎) 7447 CJK UNIFIED IDEOGRAPH-7447 +← (‎ 瑇 ‎) 2F92E CJK COMPATIBILITY IDEOGRAPH-2F92E + +# 瑜 瑜 + (‎ 瑜 ‎) 745C CJK UNIFIED IDEOGRAPH-745C +← (‎ 瑜 ‎) 2F92F CJK COMPATIBILITY IDEOGRAPH-2F92F + +# 瑩 瑩 + (‎ 瑩 ‎) 7469 CJK UNIFIED IDEOGRAPH-7469 +← (‎ 瑩 ‎) F9AE CJK COMPATIBILITY IDEOGRAPH-F9AE + +# 瑱 瑱 瑱 + (‎ 瑱 ‎) 7471 CJK UNIFIED IDEOGRAPH-7471 +← (‎ 瑱 ‎) FAA1 CJK COMPATIBILITY IDEOGRAPH-FAA1 +← (‎ 瑱 ‎) 2F930 CJK COMPATIBILITY IDEOGRAPH-2F930 + +# 璅 璅 + (‎ 璅 ‎) 7485 CJK UNIFIED IDEOGRAPH-7485 +← (‎ 璅 ‎) 2F931 CJK COMPATIBILITY IDEOGRAPH-2F931 + +# 璉 璉 + (‎ 璉 ‎) 7489 CJK UNIFIED IDEOGRAPH-7489 +← (‎ 璉 ‎) F994 CJK COMPATIBILITY IDEOGRAPH-F994 + +# 璘 璘 + (‎ 璘 ‎) 7498 CJK UNIFIED IDEOGRAPH-7498 +← (‎ 璘 ‎) F9EF CJK COMPATIBILITY IDEOGRAPH-F9EF + +# 瓊 瓊 + (‎ 瓊 ‎) 74CA CJK UNIFIED IDEOGRAPH-74CA +← (‎ 瓊 ‎) 2F932 CJK COMPATIBILITY IDEOGRAPH-2F932 + +# 甆 甆 + (‎ 甆 ‎) 7506 CJK UNIFIED IDEOGRAPH-7506 +← (‎ 甆 ‎) FAA2 CJK COMPATIBILITY IDEOGRAPH-FAA2 + +# 甤 甤 + (‎ 甤 ‎) 7524 CJK UNIFIED IDEOGRAPH-7524 +← (‎ 甤 ‎) 2F934 CJK COMPATIBILITY IDEOGRAPH-2F934 + +# 画 画 + (‎ 画 ‎) 753B CJK UNIFIED IDEOGRAPH-753B +← (‎ 画 ‎) FAA3 CJK COMPATIBILITY IDEOGRAPH-FAA3 + +# 甾 甾 + (‎ 甾 ‎) 753E CJK UNIFIED IDEOGRAPH-753E +← (‎ 甾 ‎) 2F936 CJK COMPATIBILITY IDEOGRAPH-2F936 + +# 留 留 + (‎ 留 ‎) 7559 CJK UNIFIED IDEOGRAPH-7559 +← (‎ 留 ‎) F9CD CJK COMPATIBILITY IDEOGRAPH-F9CD + +# 略 略 + (‎ 略 ‎) 7565 CJK UNIFIED IDEOGRAPH-7565 +← (‎ 略 ‎) F976 CJK COMPATIBILITY IDEOGRAPH-F976 + +# 異 異 異 + (‎ 異 ‎) 7570 CJK UNIFIED IDEOGRAPH-7570 +← (‎ 異 ‎) F962 CJK COMPATIBILITY IDEOGRAPH-F962 +← (‎ 異 ‎) 2F938 CJK COMPATIBILITY IDEOGRAPH-2F938 + +# 痢 痢 + (‎ 痢 ‎) 75E2 CJK UNIFIED IDEOGRAPH-75E2 +← (‎ 痢 ‎) F9E5 CJK COMPATIBILITY IDEOGRAPH-F9E5 + +# 瘐 瘐 + (‎ 瘐 ‎) 7610 CJK UNIFIED IDEOGRAPH-7610 +← (‎ 瘐 ‎) 2F93A CJK COMPATIBILITY IDEOGRAPH-2F93A + +# 瘝 瘝 + (‎ 瘝 ‎) 761D CJK UNIFIED IDEOGRAPH-761D +← (‎ 瘝 ‎) FAA4 CJK COMPATIBILITY IDEOGRAPH-FAA4 + +# 瘟 瘟 + (‎ 瘟 ‎) 761F CJK UNIFIED IDEOGRAPH-761F +← (‎ 瘟 ‎) FAA5 CJK COMPATIBILITY IDEOGRAPH-FAA5 + +# 療 療 + (‎ 療 ‎) 7642 CJK UNIFIED IDEOGRAPH-7642 +← (‎ 療 ‎) F9C1 CJK COMPATIBILITY IDEOGRAPH-F9C1 + +# 癩 癩 + (‎ 癩 ‎) 7669 CJK UNIFIED IDEOGRAPH-7669 +← (‎ 癩 ‎) F90E CJK COMPATIBILITY IDEOGRAPH-F90E + +# 益 益 益 + (‎ 益 ‎) 76CA CJK UNIFIED IDEOGRAPH-76CA +← (‎ 益 ‎) FA17 CJK COMPATIBILITY IDEOGRAPH-FA17 +← (‎ 益 ‎) FAA6 CJK COMPATIBILITY IDEOGRAPH-FAA6 + +# 盛 盛 + (‎ 盛 ‎) 76DB CJK UNIFIED IDEOGRAPH-76DB +← (‎ 盛 ‎) FAA7 CJK COMPATIBILITY IDEOGRAPH-FAA7 + +# 盧 盧 + (‎ 盧 ‎) 76E7 CJK UNIFIED IDEOGRAPH-76E7 +← (‎ 盧 ‎) F933 CJK COMPATIBILITY IDEOGRAPH-F933 + +# 直 直 直 + (‎ 直 ‎) 76F4 CJK UNIFIED IDEOGRAPH-76F4 +← (‎ 直 ‎) FAA8 CJK COMPATIBILITY IDEOGRAPH-FAA8 +← (‎ 直 ‎) 2F940 CJK COMPATIBILITY IDEOGRAPH-2F940 + +# 省 省 + (‎ 省 ‎) 7701 CJK UNIFIED IDEOGRAPH-7701 +← (‎ 省 ‎) F96D CJK COMPATIBILITY IDEOGRAPH-F96D + +# 眞 眞 + (‎ 眞 ‎) 771E CJK UNIFIED IDEOGRAPH-771E +← (‎ 眞 ‎) 2F945 CJK COMPATIBILITY IDEOGRAPH-2F945 + +# 真 真 真 + (‎ 真 ‎) 771F CJK UNIFIED IDEOGRAPH-771F +← (‎ 真 ‎) 2F946 CJK COMPATIBILITY IDEOGRAPH-2F946 +← (‎ 真 ‎) 2F947 CJK COMPATIBILITY IDEOGRAPH-2F947 + +# 着 着 + (‎ 着 ‎) 7740 CJK UNIFIED IDEOGRAPH-7740 +← (‎ 着 ‎) FAAA CJK COMPATIBILITY IDEOGRAPH-FAAA + +# 睊 睊 睊 + (‎ 睊 ‎) 774A CJK UNIFIED IDEOGRAPH-774A +← (‎ 睊 ‎) FAA9 CJK COMPATIBILITY IDEOGRAPH-FAA9 +← (‎ 睊 ‎) 2F948 CJK COMPATIBILITY IDEOGRAPH-2F948 + +# 瞋 瞋 + (‎ 瞋 ‎) 778B CJK UNIFIED IDEOGRAPH-778B +← (‎ 瞋 ‎) 2F94A CJK COMPATIBILITY IDEOGRAPH-2F94A + +# 瞧 瞧 + (‎ 瞧 ‎) 77A7 CJK UNIFIED IDEOGRAPH-77A7 +← (‎ 瞧 ‎) FA9D CJK COMPATIBILITY IDEOGRAPH-FA9D + +# 研 硏 + (‎ 研 ‎) 7814 CJK UNIFIED IDEOGRAPH-7814 +← (‎ 硏 ‎) 784F CJK UNIFIED IDEOGRAPH-784F + +# 硎 硎 + (‎ 硎 ‎) 784E CJK UNIFIED IDEOGRAPH-784E +← (‎ 硎 ‎) 2F94E CJK COMPATIBILITY IDEOGRAPH-2F94E + +# 硫 硫 + (‎ 硫 ‎) 786B CJK UNIFIED IDEOGRAPH-786B +← (‎ 硫 ‎) F9CE CJK COMPATIBILITY IDEOGRAPH-F9CE + +# 碌 碌 碌 + (‎ 碌 ‎) 788C CJK UNIFIED IDEOGRAPH-788C +← (‎ 碌 ‎) F93B CJK COMPATIBILITY IDEOGRAPH-F93B +← (‎ 碌 ‎) 2F94F CJK COMPATIBILITY IDEOGRAPH-2F94F + +# 碑 碑 + (‎ 碑 ‎) 7891 CJK UNIFIED IDEOGRAPH-7891 +← (‎ 碑 ‎) FA4B CJK COMPATIBILITY IDEOGRAPH-FA4B + +# 磊 磊 + (‎ 磊 ‎) 78CA CJK UNIFIED IDEOGRAPH-78CA +← (‎ 磊 ‎) F947 CJK COMPATIBILITY IDEOGRAPH-F947 + +# 磌 磌 磌 + (‎ 磌 ‎) 78CC CJK UNIFIED IDEOGRAPH-78CC +← (‎ 磌 ‎) FAAB CJK COMPATIBILITY IDEOGRAPH-FAAB +← (‎ 磌 ‎) 2F950 CJK COMPATIBILITY IDEOGRAPH-2F950 + +# 磻 磻 + (‎ 磻 ‎) 78FB CJK UNIFIED IDEOGRAPH-78FB +← (‎ 磻 ‎) F964 CJK COMPATIBILITY IDEOGRAPH-F964 + +# 礪 礪 + (‎ 礪 ‎) 792A CJK UNIFIED IDEOGRAPH-792A +← (‎ 礪 ‎) F985 CJK COMPATIBILITY IDEOGRAPH-F985 + +# 礼 礼 + (‎ 礼 ‎) 793C CJK UNIFIED IDEOGRAPH-793C +← (‎ 礼 ‎) FA18 CJK COMPATIBILITY IDEOGRAPH-FA18 + +# 社 社 + (‎ 社 ‎) 793E CJK UNIFIED IDEOGRAPH-793E +← (‎ 社 ‎) FA4C CJK COMPATIBILITY IDEOGRAPH-FA4C + +# 祈 祈 + (‎ 祈 ‎) 7948 CJK UNIFIED IDEOGRAPH-7948 +← (‎ 祈 ‎) FA4E CJK COMPATIBILITY IDEOGRAPH-FA4E + +# 祉 祉 + (‎ 祉 ‎) 7949 CJK UNIFIED IDEOGRAPH-7949 +← (‎ 祉 ‎) FA4D CJK COMPATIBILITY IDEOGRAPH-FA4D + +# 祐 祐 + (‎ 祐 ‎) 7950 CJK UNIFIED IDEOGRAPH-7950 +← (‎ 祐 ‎) FA4F CJK COMPATIBILITY IDEOGRAPH-FA4F + +# 祖 祖 祖 + (‎ 祖 ‎) 7956 CJK UNIFIED IDEOGRAPH-7956 +← (‎ 祖 ‎) FA50 CJK COMPATIBILITY IDEOGRAPH-FA50 +← (‎ 祖 ‎) 2F953 CJK COMPATIBILITY IDEOGRAPH-2F953 + +# 祝 祝 + (‎ 祝 ‎) 795D CJK UNIFIED IDEOGRAPH-795D +← (‎ 祝 ‎) FA51 CJK COMPATIBILITY IDEOGRAPH-FA51 + +# 神 神 + (‎ 神 ‎) 795E CJK UNIFIED IDEOGRAPH-795E +← (‎ 神 ‎) FA19 CJK COMPATIBILITY IDEOGRAPH-FA19 + +# 祥 祥 + (‎ 祥 ‎) 7965 CJK UNIFIED IDEOGRAPH-7965 +← (‎ 祥 ‎) FA1A CJK COMPATIBILITY IDEOGRAPH-FA1A + +# 祿 祿 + (‎ 祿 ‎) 797F CJK UNIFIED IDEOGRAPH-797F +← (‎ 祿 ‎) F93C CJK COMPATIBILITY IDEOGRAPH-F93C + +# 禍 禍 + (‎ 禍 ‎) 798D CJK UNIFIED IDEOGRAPH-798D +← (‎ 禍 ‎) FA52 CJK COMPATIBILITY IDEOGRAPH-FA52 + +# 禎 禎 + (‎ 禎 ‎) 798E CJK UNIFIED IDEOGRAPH-798E +← (‎ 禎 ‎) FA53 CJK COMPATIBILITY IDEOGRAPH-FA53 + +# 福 福 福 + (‎ 福 ‎) 798F CJK UNIFIED IDEOGRAPH-798F +← (‎ 福 ‎) FA1B CJK COMPATIBILITY IDEOGRAPH-FA1B +← (‎ 福 ‎) 2F956 CJK COMPATIBILITY IDEOGRAPH-2F956 + +# 禮 禮 + (‎ 禮 ‎) 79AE CJK UNIFIED IDEOGRAPH-79AE +← (‎ 禮 ‎) F9B6 CJK COMPATIBILITY IDEOGRAPH-F9B6 + +# 秊 秊 + (‎ 秊 ‎) 79CA CJK UNIFIED IDEOGRAPH-79CA +← (‎ 秊 ‎) F995 CJK COMPATIBILITY IDEOGRAPH-F995 + +# 秫 秫 + (‎ 秫 ‎) 79EB CJK UNIFIED IDEOGRAPH-79EB +← (‎ 秫 ‎) 2F957 CJK COMPATIBILITY IDEOGRAPH-2F957 + +# 稜 稜 + (‎ 稜 ‎) 7A1C CJK UNIFIED IDEOGRAPH-7A1C +← (‎ 稜 ‎) F956 CJK COMPATIBILITY IDEOGRAPH-F956 + +# 穀 穀 穀 + (‎ 穀 ‎) 7A40 CJK UNIFIED IDEOGRAPH-7A40 +← (‎ 穀 ‎) FA54 CJK COMPATIBILITY IDEOGRAPH-FA54 +← (‎ 穀 ‎) 2F959 CJK COMPATIBILITY IDEOGRAPH-2F959 + +# 穊 穊 + (‎ 穊 ‎) 7A4A CJK UNIFIED IDEOGRAPH-7A4A +← (‎ 穊 ‎) 2F95A CJK COMPATIBILITY IDEOGRAPH-2F95A + +# 穏 穏 + (‎ 穏 ‎) 7A4F CJK UNIFIED IDEOGRAPH-7A4F +← (‎ 穏 ‎) 2F95B CJK COMPATIBILITY IDEOGRAPH-2F95B + +# 突 突 + (‎ 突 ‎) 7A81 CJK UNIFIED IDEOGRAPH-7A81 +← (‎ 突 ‎) FA55 CJK COMPATIBILITY IDEOGRAPH-FA55 + +# 窱 窱 + (‎ 窱 ‎) 7AB1 CJK UNIFIED IDEOGRAPH-7AB1 +← (‎ 窱 ‎) FAAC CJK COMPATIBILITY IDEOGRAPH-FAAC + +# 竮 竮 + (‎ 竮 ‎) 7AEE CJK UNIFIED IDEOGRAPH-7AEE +← (‎ 竮 ‎) 2F95F CJK COMPATIBILITY IDEOGRAPH-2F95F + +# 笠 笠 + (‎ 笠 ‎) 7B20 CJK UNIFIED IDEOGRAPH-7B20 +← (‎ 笠 ‎) F9F8 CJK COMPATIBILITY IDEOGRAPH-F9F8 + +# 節 節 節 + (‎ 節 ‎) 7BC0 CJK UNIFIED IDEOGRAPH-7BC0 +← (‎ 節 ‎) FA56 CJK COMPATIBILITY IDEOGRAPH-FA56 +← (‎ 節 ‎) FAAD CJK COMPATIBILITY IDEOGRAPH-FAAD + +# 篆 篆 + (‎ 篆 ‎) 7BC6 CJK UNIFIED IDEOGRAPH-7BC6 +← (‎ 篆 ‎) 2F962 CJK COMPATIBILITY IDEOGRAPH-2F962 + +# 築 築 + (‎ 築 ‎) 7BC9 CJK UNIFIED IDEOGRAPH-7BC9 +← (‎ 築 ‎) 2F963 CJK COMPATIBILITY IDEOGRAPH-2F963 + +# 簾 簾 + (‎ 簾 ‎) 7C3E CJK UNIFIED IDEOGRAPH-7C3E +← (‎ 簾 ‎) F9A6 CJK COMPATIBILITY IDEOGRAPH-F9A6 + +# 籠 籠 + (‎ 籠 ‎) 7C60 CJK UNIFIED IDEOGRAPH-7C60 +← (‎ 籠 ‎) F944 CJK COMPATIBILITY IDEOGRAPH-F944 + +# 类 类 + (‎ 类 ‎) 7C7B CJK UNIFIED IDEOGRAPH-7C7B +← (‎ 类 ‎) FAAE CJK COMPATIBILITY IDEOGRAPH-FAAE + +# 粒 粒 + (‎ 粒 ‎) 7C92 CJK UNIFIED IDEOGRAPH-7C92 +← (‎ 粒 ‎) F9F9 CJK COMPATIBILITY IDEOGRAPH-F9F9 + +# 精 精 + (‎ 精 ‎) 7CBE CJK UNIFIED IDEOGRAPH-7CBE +← (‎ 精 ‎) FA1D CJK COMPATIBILITY IDEOGRAPH-FA1D + +# 糒 糒 + (‎ 糒 ‎) 7CD2 CJK UNIFIED IDEOGRAPH-7CD2 +← (‎ 糒 ‎) 2F966 CJK COMPATIBILITY IDEOGRAPH-2F966 + +# 糖 糖 + (‎ 糖 ‎) 7CD6 CJK UNIFIED IDEOGRAPH-7CD6 +← (‎ 糖 ‎) FA03 CJK COMPATIBILITY IDEOGRAPH-FA03 + +# 糣 糣 + (‎ 糣 ‎) 7CE3 CJK UNIFIED IDEOGRAPH-7CE3 +← (‎ 糣 ‎) 2F969 CJK COMPATIBILITY IDEOGRAPH-2F969 + +# 糧 糧 + (‎ 糧 ‎) 7CE7 CJK UNIFIED IDEOGRAPH-7CE7 +← (‎ 糧 ‎) F97B CJK COMPATIBILITY IDEOGRAPH-F97B + +# 糨 糨 + (‎ 糨 ‎) 7CE8 CJK UNIFIED IDEOGRAPH-7CE8 +← (‎ 糨 ‎) 2F968 CJK COMPATIBILITY IDEOGRAPH-2F968 + +# 紀 紀 + (‎ 紀 ‎) 7D00 CJK UNIFIED IDEOGRAPH-7D00 +← (‎ 紀 ‎) 2F96A CJK COMPATIBILITY IDEOGRAPH-2F96A + +# 紐 紐 + (‎ 紐 ‎) 7D10 CJK UNIFIED IDEOGRAPH-7D10 +← (‎ 紐 ‎) F9CF CJK COMPATIBILITY IDEOGRAPH-F9CF + +# 索 索 + (‎ 索 ‎) 7D22 CJK UNIFIED IDEOGRAPH-7D22 +← (‎ 索 ‎) F96A CJK COMPATIBILITY IDEOGRAPH-F96A + +# 累 累 + (‎ 累 ‎) 7D2F CJK UNIFIED IDEOGRAPH-7D2F +← (‎ 累 ‎) F94F CJK COMPATIBILITY IDEOGRAPH-F94F + +# 絕 絶 + (‎ 絕 ‎) 7D55 CJK UNIFIED IDEOGRAPH-7D55 +← (‎ 絶 ‎) 7D76 CJK UNIFIED IDEOGRAPH-7D76 + +# 絛 絛 + (‎ 絛 ‎) 7D5B CJK UNIFIED IDEOGRAPH-7D5B +← (‎ 絛 ‎) FAAF CJK COMPATIBILITY IDEOGRAPH-FAAF + +# 絣 絣 + (‎ 絣 ‎) 7D63 CJK UNIFIED IDEOGRAPH-7D63 +← (‎ 絣 ‎) 2F96C CJK COMPATIBILITY IDEOGRAPH-2F96C + +# 綠 綠 + (‎ 綠 ‎) 7DA0 CJK UNIFIED IDEOGRAPH-7DA0 +← (‎ 綠 ‎) F93D CJK COMPATIBILITY IDEOGRAPH-F93D + +# 綾 綾 + (‎ 綾 ‎) 7DBE CJK UNIFIED IDEOGRAPH-7DBE +← (‎ 綾 ‎) F957 CJK COMPATIBILITY IDEOGRAPH-F957 + +# 緇 緇 + (‎ 緇 ‎) 7DC7 CJK UNIFIED IDEOGRAPH-7DC7 +← (‎ 緇 ‎) 2F96E CJK COMPATIBILITY IDEOGRAPH-2F96E + +# 練 練 練 練 + (‎ 練 ‎) 7DF4 CJK UNIFIED IDEOGRAPH-7DF4 +← (‎ 練 ‎) F996 CJK COMPATIBILITY IDEOGRAPH-F996 +← (‎ 練 ‎) FA57 CJK COMPATIBILITY IDEOGRAPH-FA57 +← (‎ 練 ‎) FAB0 CJK COMPATIBILITY IDEOGRAPH-FAB0 + +# 縂 縂 + (‎ 縂 ‎) 7E02 CJK UNIFIED IDEOGRAPH-7E02 +← (‎ 縂 ‎) 2F96F CJK COMPATIBILITY IDEOGRAPH-2F96F + +# 縉 縉 + (‎ 縉 ‎) 7E09 CJK UNIFIED IDEOGRAPH-7E09 +← (‎ 縉 ‎) FA58 CJK COMPATIBILITY IDEOGRAPH-FA58 + +# 縷 縷 + (‎ 縷 ‎) 7E37 CJK UNIFIED IDEOGRAPH-7E37 +← (‎ 縷 ‎) F950 CJK COMPATIBILITY IDEOGRAPH-F950 + +# 繁 繁 + (‎ 繁 ‎) 7E41 CJK UNIFIED IDEOGRAPH-7E41 +← (‎ 繁 ‎) FA59 CJK COMPATIBILITY IDEOGRAPH-FA59 + +# 繅 繅 + (‎ 繅 ‎) 7E45 CJK UNIFIED IDEOGRAPH-7E45 +← (‎ 繅 ‎) 2F970 CJK COMPATIBILITY IDEOGRAPH-2F970 + +# 缾 缾 + (‎ 缾 ‎) 7F3E CJK UNIFIED IDEOGRAPH-7F3E +← (‎ 缾 ‎) FAB1 CJK COMPATIBILITY IDEOGRAPH-FAB1 + +# 署 署 + (‎ 署 ‎) 7F72 CJK UNIFIED IDEOGRAPH-7F72 +← (‎ 署 ‎) FA5A CJK COMPATIBILITY IDEOGRAPH-FA5A + +# 罹 罹 + (‎ 罹 ‎) 7F79 CJK UNIFIED IDEOGRAPH-7F79 +← (‎ 罹 ‎) F9E6 CJK COMPATIBILITY IDEOGRAPH-F9E6 + +# 罺 罺 + (‎ 罺 ‎) 7F7A CJK UNIFIED IDEOGRAPH-7F7A +← (‎ 罺 ‎) 2F976 CJK COMPATIBILITY IDEOGRAPH-2F976 + +# 羅 羅 + (‎ 羅 ‎) 7F85 CJK UNIFIED IDEOGRAPH-7F85 +← (‎ 羅 ‎) F90F CJK COMPATIBILITY IDEOGRAPH-F90F + +# 羕 羕 + (‎ 羕 ‎) 7F95 CJK UNIFIED IDEOGRAPH-7F95 +← (‎ 羕 ‎) 2F978 CJK COMPATIBILITY IDEOGRAPH-2F978 + +# 羚 羚 + (‎ 羚 ‎) 7F9A CJK UNIFIED IDEOGRAPH-7F9A +← (‎ 羚 ‎) F9AF CJK COMPATIBILITY IDEOGRAPH-F9AF + +# 翺 翺 + (‎ 翺 ‎) 7FFA CJK UNIFIED IDEOGRAPH-7FFA +← (‎ 翺 ‎) 2F979 CJK COMPATIBILITY IDEOGRAPH-2F979 + +# 者 者 者 者 + (‎ 者 ‎) 8005 CJK UNIFIED IDEOGRAPH-8005 +← (‎ 者 ‎) FA5B CJK COMPATIBILITY IDEOGRAPH-FA5B +← (‎ 者 ‎) FAB2 CJK COMPATIBILITY IDEOGRAPH-FAB2 +← (‎ 者 ‎) 2F97A CJK COMPATIBILITY IDEOGRAPH-2F97A + +# 聆 聆 + (‎ 聆 ‎) 8046 CJK UNIFIED IDEOGRAPH-8046 +← (‎ 聆 ‎) F9B0 CJK COMPATIBILITY IDEOGRAPH-F9B0 + +# 聠 聠 + (‎ 聠 ‎) 8060 CJK UNIFIED IDEOGRAPH-8060 +← (‎ 聠 ‎) 2F97D CJK COMPATIBILITY IDEOGRAPH-2F97D + +# 聯 聯 + (‎ 聯 ‎) 806F CJK UNIFIED IDEOGRAPH-806F +← (‎ 聯 ‎) F997 CJK COMPATIBILITY IDEOGRAPH-F997 + +# 聰 聰 + (‎ 聰 ‎) 8070 CJK UNIFIED IDEOGRAPH-8070 +← (‎ 聰 ‎) 2F97F CJK COMPATIBILITY IDEOGRAPH-2F97F + +# 聾 聾 + (‎ 聾 ‎) 807E CJK UNIFIED IDEOGRAPH-807E +← (‎ 聾 ‎) F945 CJK COMPATIBILITY IDEOGRAPH-F945 + +# 肋 肋 + (‎ 肋 ‎) 808B CJK UNIFIED IDEOGRAPH-808B +← (‎ 肋 ‎) F953 CJK COMPATIBILITY IDEOGRAPH-F953 + +# 肭 肭 + (‎ 肭 ‎) 80AD CJK UNIFIED IDEOGRAPH-80AD +← (‎ 肭 ‎) 2F8D6 CJK COMPATIBILITY IDEOGRAPH-2F8D6 + +# 育 育 + (‎ 育 ‎) 80B2 CJK UNIFIED IDEOGRAPH-80B2 +← (‎ 育 ‎) 2F982 CJK COMPATIBILITY IDEOGRAPH-2F982 + +# 胼 腁 + (‎ 胼 ‎) 80FC CJK UNIFIED IDEOGRAPH-80FC +← (‎ 腁 ‎) 8141 CJK UNIFIED IDEOGRAPH-8141 + +# 脃 脃 + (‎ 脃 ‎) 8103 CJK UNIFIED IDEOGRAPH-8103 +← (‎ 脃 ‎) 2F983 CJK COMPATIBILITY IDEOGRAPH-2F983 + +# 脾 脾 + (‎ 脾 ‎) 813E CJK UNIFIED IDEOGRAPH-813E +← (‎ 脾 ‎) 2F985 CJK COMPATIBILITY IDEOGRAPH-2F985 + +# 臘 臘 + (‎ 臘 ‎) 81D8 CJK UNIFIED IDEOGRAPH-81D8 +← (‎ 臘 ‎) F926 CJK COMPATIBILITY IDEOGRAPH-F926 + +# 臨 臨 + (‎ 臨 ‎) 81E8 CJK UNIFIED IDEOGRAPH-81E8 +← (‎ 臨 ‎) F9F6 CJK COMPATIBILITY IDEOGRAPH-F9F6 + +# 臭 臭 + (‎ 臭 ‎) 81ED CJK UNIFIED IDEOGRAPH-81ED +← (‎ 臭 ‎) FA5C CJK COMPATIBILITY IDEOGRAPH-FA5C + +# 舁 舁 舁 + (‎ 舁 ‎) 8201 CJK UNIFIED IDEOGRAPH-8201 +← (‎ 舁 ‎) 2F893 CJK COMPATIBILITY IDEOGRAPH-2F893 +← (‎ 舁 ‎) 2F98B CJK COMPATIBILITY IDEOGRAPH-2F98B + +# 舄 舄 + (‎ 舄 ‎) 8204 CJK UNIFIED IDEOGRAPH-8204 +← (‎ 舄 ‎) 2F98C CJK COMPATIBILITY IDEOGRAPH-2F98C + +# 舘 舘 + (‎ 舘 ‎) 8218 CJK UNIFIED IDEOGRAPH-8218 +← (‎ 舘 ‎) FA6D CJK COMPATIBILITY IDEOGRAPH-FA6D + +# 良 良 + (‎ 良 ‎) 826F CJK UNIFIED IDEOGRAPH-826F +← (‎ 良 ‎) F97C CJK COMPATIBILITY IDEOGRAPH-F97C + +# 芋 芋 + (‎ 芋 ‎) 828B CJK UNIFIED IDEOGRAPH-828B +← (‎ 芋 ‎) 2F990 CJK COMPATIBILITY IDEOGRAPH-2F990 + +# 芑 芑 + (‎ 芑 ‎) 8291 CJK UNIFIED IDEOGRAPH-8291 +← (‎ 芑 ‎) 2F98F CJK COMPATIBILITY IDEOGRAPH-2F98F + +# 芝 芝 + (‎ 芝 ‎) 829D CJK UNIFIED IDEOGRAPH-829D +← (‎ 芝 ‎) 2F991 CJK COMPATIBILITY IDEOGRAPH-2F991 + +# 花 花 + (‎ 花 ‎) 82B1 CJK UNIFIED IDEOGRAPH-82B1 +← (‎ 花 ‎) 2F993 CJK COMPATIBILITY IDEOGRAPH-2F993 + +# 芳 芳 + (‎ 芳 ‎) 82B3 CJK UNIFIED IDEOGRAPH-82B3 +← (‎ 芳 ‎) 2F994 CJK COMPATIBILITY IDEOGRAPH-2F994 + +# 芽 芽 + (‎ 芽 ‎) 82BD CJK UNIFIED IDEOGRAPH-82BD +← (‎ 芽 ‎) 2F995 CJK COMPATIBILITY IDEOGRAPH-2F995 + +# 若 若 若 + (‎ 若 ‎) 82E5 CJK UNIFIED IDEOGRAPH-82E5 +← (‎ 若 ‎) F974 CJK COMPATIBILITY IDEOGRAPH-F974 +← (‎ 若 ‎) 2F998 CJK COMPATIBILITY IDEOGRAPH-2F998 + +# 苦 苦 + (‎ 苦 ‎) 82E6 CJK UNIFIED IDEOGRAPH-82E6 +← (‎ 苦 ‎) 2F996 CJK COMPATIBILITY IDEOGRAPH-2F996 + +# 茝 茝 + (‎ 茝 ‎) 831D CJK UNIFIED IDEOGRAPH-831D +← (‎ 茝 ‎) 2F999 CJK COMPATIBILITY IDEOGRAPH-2F999 + +# 茣 茣 + (‎ 茣 ‎) 8323 CJK UNIFIED IDEOGRAPH-8323 +← (‎ 茣 ‎) 2F99C CJK COMPATIBILITY IDEOGRAPH-2F99C + +# 茶 茶 + (‎ 茶 ‎) 8336 CJK UNIFIED IDEOGRAPH-8336 +← (‎ 茶 ‎) F9FE CJK COMPATIBILITY IDEOGRAPH-F9FE + +# 荒 荒 + (‎ 荒 ‎) 8352 CJK UNIFIED IDEOGRAPH-8352 +← (‎ 荒 ‎) FAB3 CJK COMPATIBILITY IDEOGRAPH-FAB3 + +# 荓 荓 + (‎ 荓 ‎) 8353 CJK UNIFIED IDEOGRAPH-8353 +← (‎ 荓 ‎) 2F9A0 CJK COMPATIBILITY IDEOGRAPH-2F9A0 + +# 荣 荣 + (‎ 荣 ‎) 8363 CJK UNIFIED IDEOGRAPH-8363 +← (‎ 荣 ‎) 2F99A CJK COMPATIBILITY IDEOGRAPH-2F99A + +# 莭 莭 + (‎ 莭 ‎) 83AD CJK UNIFIED IDEOGRAPH-83AD +← (‎ 莭 ‎) 2F99B CJK COMPATIBILITY IDEOGRAPH-2F99B + +# 莽 莽 + (‎ 莽 ‎) 83BD CJK UNIFIED IDEOGRAPH-83BD +← (‎ 莽 ‎) 2F99D CJK COMPATIBILITY IDEOGRAPH-2F99D + +# 菉 菉 + (‎ 菉 ‎) 83C9 CJK UNIFIED IDEOGRAPH-83C9 +← (‎ 菉 ‎) F93E CJK COMPATIBILITY IDEOGRAPH-F93E + +# 菊 菊 + (‎ 菊 ‎) 83CA CJK UNIFIED IDEOGRAPH-83CA +← (‎ 菊 ‎) 2F9A1 CJK COMPATIBILITY IDEOGRAPH-2F9A1 + +# 菌 菌 + (‎ 菌 ‎) 83CC CJK UNIFIED IDEOGRAPH-83CC +← (‎ 菌 ‎) 2F9A2 CJK COMPATIBILITY IDEOGRAPH-2F9A2 + +# 菜 菜 + (‎ 菜 ‎) 83DC CJK UNIFIED IDEOGRAPH-83DC +← (‎ 菜 ‎) 2F9A3 CJK COMPATIBILITY IDEOGRAPH-2F9A3 + +# 菧 菧 + (‎ 菧 ‎) 83E7 CJK UNIFIED IDEOGRAPH-83E7 +← (‎ 菧 ‎) 2F99E CJK COMPATIBILITY IDEOGRAPH-2F99E + +# 華 華 + (‎ 華 ‎) 83EF CJK UNIFIED IDEOGRAPH-83EF +← (‎ 華 ‎) FAB4 CJK COMPATIBILITY IDEOGRAPH-FAB4 + +# 菱 菱 + (‎ 菱 ‎) 83F1 CJK UNIFIED IDEOGRAPH-83F1 +← (‎ 菱 ‎) F958 CJK COMPATIBILITY IDEOGRAPH-F958 + +# 落 落 + (‎ 落 ‎) 843D CJK UNIFIED IDEOGRAPH-843D +← (‎ 落 ‎) F918 CJK COMPATIBILITY IDEOGRAPH-F918 + +# 葉 葉 + (‎ 葉 ‎) 8449 CJK UNIFIED IDEOGRAPH-8449 +← (‎ 葉 ‎) F96E CJK COMPATIBILITY IDEOGRAPH-F96E + +# 著 著 著 + (‎ 著 ‎) 8457 CJK UNIFIED IDEOGRAPH-8457 +← (‎ 著 ‎) FA5F CJK COMPATIBILITY IDEOGRAPH-FA5F +← (‎ 著 ‎) 2F99F CJK COMPATIBILITY IDEOGRAPH-2F99F + +# 蒍 蔿 + (‎ 蒍 ‎) 848D CJK UNIFIED IDEOGRAPH-848D +← (‎ 蔿 ‎) 853F CJK UNIFIED IDEOGRAPH-853F + +# 蓮 蓮 + (‎ 蓮 ‎) 84EE CJK UNIFIED IDEOGRAPH-84EE +← (‎ 蓮 ‎) F999 CJK COMPATIBILITY IDEOGRAPH-F999 + +# 蓱 蓱 + (‎ 蓱 ‎) 84F1 CJK UNIFIED IDEOGRAPH-84F1 +← (‎ 蓱 ‎) 2F9A8 CJK COMPATIBILITY IDEOGRAPH-2F9A8 + +# 蓳 蓳 + (‎ 蓳 ‎) 84F3 CJK UNIFIED IDEOGRAPH-84F3 +← (‎ 蓳 ‎) 2F9A9 CJK COMPATIBILITY IDEOGRAPH-2F9A9 + +# 蓼 蓼 + (‎ 蓼 ‎) 84FC CJK UNIFIED IDEOGRAPH-84FC +← (‎ 蓼 ‎) F9C2 CJK COMPATIBILITY IDEOGRAPH-F9C2 + +# 蔖 蔖 + (‎ 蔖 ‎) 8516 CJK UNIFIED IDEOGRAPH-8516 +← (‎ 蔖 ‎) 2F9AA CJK COMPATIBILITY IDEOGRAPH-2F9AA + +# 蕤 蕤 + (‎ 蕤 ‎) 8564 CJK UNIFIED IDEOGRAPH-8564 +← (‎ 蕤 ‎) 2F9AC CJK COMPATIBILITY IDEOGRAPH-2F9AC + +# 藍 藍 + (‎ 藍 ‎) 85CD CJK UNIFIED IDEOGRAPH-85CD +← (‎ 藍 ‎) F923 CJK COMPATIBILITY IDEOGRAPH-F923 + +# 藺 藺 + (‎ 藺 ‎) 85FA CJK UNIFIED IDEOGRAPH-85FA +← (‎ 藺 ‎) F9F0 CJK COMPATIBILITY IDEOGRAPH-F9F0 + +# 蘆 蘆 + (‎ 蘆 ‎) 8606 CJK UNIFIED IDEOGRAPH-8606 +← (‎ 蘆 ‎) F935 CJK COMPATIBILITY IDEOGRAPH-F935 + +# 蘒 蘒 + (‎ 蘒 ‎) 8612 CJK UNIFIED IDEOGRAPH-8612 +← (‎ 蘒 ‎) FA20 CJK COMPATIBILITY IDEOGRAPH-FA20 + +# 蘭 蘭 + (‎ 蘭 ‎) 862D CJK UNIFIED IDEOGRAPH-862D +← (‎ 蘭 ‎) F91F CJK COMPATIBILITY IDEOGRAPH-F91F + +# 蘷 虁 + (‎ 蘷 ‎) 8637 CJK UNIFIED IDEOGRAPH-8637 +← (‎ 虁 ‎) 8641 CJK UNIFIED IDEOGRAPH-8641 + +# 蘿 蘿 + (‎ 蘿 ‎) 863F CJK UNIFIED IDEOGRAPH-863F +← (‎ 蘿 ‎) F910 CJK COMPATIBILITY IDEOGRAPH-F910 + +# 虐 虐 + (‎ 虐 ‎) 8650 CJK UNIFIED IDEOGRAPH-8650 +← (‎ 虐 ‎) 2F9B3 CJK COMPATIBILITY IDEOGRAPH-2F9B3 + +# 虜 虜 虜 + (‎ 虜 ‎) 865C CJK UNIFIED IDEOGRAPH-865C +← (‎ 虜 ‎) F936 CJK COMPATIBILITY IDEOGRAPH-F936 +← (‎ 虜 ‎) 2F9B4 CJK COMPATIBILITY IDEOGRAPH-2F9B4 + +# 虧 虧 + (‎ 虧 ‎) 8667 CJK UNIFIED IDEOGRAPH-8667 +← (‎ 虧 ‎) 2F9B5 CJK COMPATIBILITY IDEOGRAPH-2F9B5 + +# 虩 虩 + (‎ 虩 ‎) 8669 CJK UNIFIED IDEOGRAPH-8669 +← (‎ 虩 ‎) 2F9B6 CJK COMPATIBILITY IDEOGRAPH-2F9B6 + +# 蚈 蚈 + (‎ 蚈 ‎) 8688 CJK UNIFIED IDEOGRAPH-8688 +← (‎ 蚈 ‎) 2F9B8 CJK COMPATIBILITY IDEOGRAPH-2F9B8 + +# 蚩 蚩 + (‎ 蚩 ‎) 86A9 CJK UNIFIED IDEOGRAPH-86A9 +← (‎ 蚩 ‎) 2F9B7 CJK COMPATIBILITY IDEOGRAPH-2F9B7 + +# 蛢 蛢 + (‎ 蛢 ‎) 86E2 CJK UNIFIED IDEOGRAPH-86E2 +← (‎ 蛢 ‎) 2F9BA CJK COMPATIBILITY IDEOGRAPH-2F9BA + +# 蜎 蜎 + (‎ 蜎 ‎) 870E CJK UNIFIED IDEOGRAPH-870E +← (‎ 蜎 ‎) 2F9B9 CJK COMPATIBILITY IDEOGRAPH-2F9B9 + +# 蜨 蜨 + (‎ 蜨 ‎) 8728 CJK UNIFIED IDEOGRAPH-8728 +← (‎ 蜨 ‎) 2F9BC CJK COMPATIBILITY IDEOGRAPH-2F9BC + +# 蝫 蝫 + (‎ 蝫 ‎) 876B CJK UNIFIED IDEOGRAPH-876B +← (‎ 蝫 ‎) 2F9BD CJK COMPATIBILITY IDEOGRAPH-2F9BD + +# 蝹 蝹 蝹 + (‎ 蝹 ‎) 8779 CJK UNIFIED IDEOGRAPH-8779 +← (‎ 蝹 ‎) FAB5 CJK COMPATIBILITY IDEOGRAPH-FAB5 +← (‎ 蝹 ‎) 2F9BB CJK COMPATIBILITY IDEOGRAPH-2F9BB + +# 螆 螆 + (‎ 螆 ‎) 8786 CJK UNIFIED IDEOGRAPH-8786 +← (‎ 螆 ‎) 2F9BE CJK COMPATIBILITY IDEOGRAPH-2F9BE + +# 螺 螺 + (‎ 螺 ‎) 87BA CJK UNIFIED IDEOGRAPH-87BA +← (‎ 螺 ‎) F911 CJK COMPATIBILITY IDEOGRAPH-F911 + +# 蟡 蟡 + (‎ 蟡 ‎) 87E1 CJK UNIFIED IDEOGRAPH-87E1 +← (‎ 蟡 ‎) 2F9C0 CJK COMPATIBILITY IDEOGRAPH-2F9C0 + +# 蠁 蠁 + (‎ 蠁 ‎) 8801 CJK UNIFIED IDEOGRAPH-8801 +← (‎ 蠁 ‎) 2F9C1 CJK COMPATIBILITY IDEOGRAPH-2F9C1 + +# 蠟 蠟 + (‎ 蠟 ‎) 881F CJK UNIFIED IDEOGRAPH-881F +← (‎ 蠟 ‎) F927 CJK COMPATIBILITY IDEOGRAPH-F927 + +# 衠 衠 + (‎ 衠 ‎) 8860 CJK UNIFIED IDEOGRAPH-8860 +← (‎ 衠 ‎) 2F9C3 CJK COMPATIBILITY IDEOGRAPH-2F9C3 + +# 裂 裂 + (‎ 裂 ‎) 88C2 CJK UNIFIED IDEOGRAPH-88C2 +← (‎ 裂 ‎) F9A0 CJK COMPATIBILITY IDEOGRAPH-F9A0 + +# 裏 裏 + (‎ 裏 ‎) 88CF CJK UNIFIED IDEOGRAPH-88CF +← (‎ 裏 ‎) F9E7 CJK COMPATIBILITY IDEOGRAPH-F9E7 + +# 裗 裗 + (‎ 裗 ‎) 88D7 CJK UNIFIED IDEOGRAPH-88D7 +← (‎ 裗 ‎) 2F9C6 CJK COMPATIBILITY IDEOGRAPH-2F9C6 + +# 裞 裞 + (‎ 裞 ‎) 88DE CJK UNIFIED IDEOGRAPH-88DE +← (‎ 裞 ‎) 2F9C7 CJK COMPATIBILITY IDEOGRAPH-2F9C7 + +# 裡 裡 + (‎ 裡 ‎) 88E1 CJK UNIFIED IDEOGRAPH-88E1 +← (‎ 裡 ‎) F9E8 CJK COMPATIBILITY IDEOGRAPH-F9E8 + +# 裸 裸 + (‎ 裸 ‎) 88F8 CJK UNIFIED IDEOGRAPH-88F8 +← (‎ 裸 ‎) F912 CJK COMPATIBILITY IDEOGRAPH-F912 + +# 裺 裺 + (‎ 裺 ‎) 88FA CJK UNIFIED IDEOGRAPH-88FA +← (‎ 裺 ‎) 2F9C9 CJK COMPATIBILITY IDEOGRAPH-2F9C9 + +# 褐 褐 + (‎ 褐 ‎) 8910 CJK UNIFIED IDEOGRAPH-8910 +← (‎ 褐 ‎) FA60 CJK COMPATIBILITY IDEOGRAPH-FA60 + +# 襁 襁 + (‎ 襁 ‎) 8941 CJK UNIFIED IDEOGRAPH-8941 +← (‎ 襁 ‎) FAB6 CJK COMPATIBILITY IDEOGRAPH-FAB6 + +# 襤 襤 + (‎ 襤 ‎) 8964 CJK UNIFIED IDEOGRAPH-8964 +← (‎ 襤 ‎) F924 CJK COMPATIBILITY IDEOGRAPH-F924 + +# 覆 覆 + (‎ 覆 ‎) 8986 CJK UNIFIED IDEOGRAPH-8986 +← (‎ 覆 ‎) FAB7 CJK COMPATIBILITY IDEOGRAPH-FAB7 + +# 視 視 視 + (‎ 視 ‎) 8996 CJK UNIFIED IDEOGRAPH-8996 +← (‎ 視 ‎) FA61 CJK COMPATIBILITY IDEOGRAPH-FA61 +← (‎ 視 ‎) FAB8 CJK COMPATIBILITY IDEOGRAPH-FAB8 + +# 訮 詽 + (‎ 訮 ‎) 8A2E CJK UNIFIED IDEOGRAPH-8A2E +← (‎ 詽 ‎) 8A7D CJK UNIFIED IDEOGRAPH-8A7D + +# 誠 誠 + (‎ 誠 ‎) 8AA0 CJK UNIFIED IDEOGRAPH-8AA0 +← (‎ 誠 ‎) 2F9CF CJK COMPATIBILITY IDEOGRAPH-2F9CF + +# 說 說 說 + (‎ 說 ‎) 8AAA CJK UNIFIED IDEOGRAPH-8AAA +← (‎ 說 ‎) F96F CJK COMPATIBILITY IDEOGRAPH-F96F +← (‎ 說 ‎) F9A1 CJK COMPATIBILITY IDEOGRAPH-F9A1 + +# 調 調 + (‎ 調 ‎) 8ABF CJK UNIFIED IDEOGRAPH-8ABF +← (‎ 調 ‎) FAB9 CJK COMPATIBILITY IDEOGRAPH-FAB9 + +# 請 請 + (‎ 請 ‎) 8ACB CJK UNIFIED IDEOGRAPH-8ACB +← (‎ 請 ‎) FABB CJK COMPATIBILITY IDEOGRAPH-FABB + +# 諒 諒 + (‎ 諒 ‎) 8AD2 CJK UNIFIED IDEOGRAPH-8AD2 +← (‎ 諒 ‎) F97D CJK COMPATIBILITY IDEOGRAPH-F97D + +# 論 論 + (‎ 論 ‎) 8AD6 CJK UNIFIED IDEOGRAPH-8AD6 +← (‎ 論 ‎) F941 CJK COMPATIBILITY IDEOGRAPH-F941 + +# 諭 諭 諭 + (‎ 諭 ‎) 8AED CJK UNIFIED IDEOGRAPH-8AED +← (‎ 諭 ‎) FABE CJK COMPATIBILITY IDEOGRAPH-FABE +← (‎ 諭 ‎) 2F9D0 CJK COMPATIBILITY IDEOGRAPH-2F9D0 + +# 諸 諸 諸 + (‎ 諸 ‎) 8AF8 CJK UNIFIED IDEOGRAPH-8AF8 +← (‎ 諸 ‎) FA22 CJK COMPATIBILITY IDEOGRAPH-FA22 +← (‎ 諸 ‎) FABA CJK COMPATIBILITY IDEOGRAPH-FABA + +# 諾 諾 諾 + (‎ 諾 ‎) 8AFE CJK UNIFIED IDEOGRAPH-8AFE +← (‎ 諾 ‎) F95D CJK COMPATIBILITY IDEOGRAPH-F95D +← (‎ 諾 ‎) FABD CJK COMPATIBILITY IDEOGRAPH-FABD + +# 謁 謁 謁 + (‎ 謁 ‎) 8B01 CJK UNIFIED IDEOGRAPH-8B01 +← (‎ 謁 ‎) FA62 CJK COMPATIBILITY IDEOGRAPH-FA62 +← (‎ 謁 ‎) FABC CJK COMPATIBILITY IDEOGRAPH-FABC + +# 謹 謹 謹 + (‎ 謹 ‎) 8B39 CJK UNIFIED IDEOGRAPH-8B39 +← (‎ 謹 ‎) FA63 CJK COMPATIBILITY IDEOGRAPH-FA63 +← (‎ 謹 ‎) FABF CJK COMPATIBILITY IDEOGRAPH-FABF + +# 識 識 + (‎ 識 ‎) 8B58 CJK UNIFIED IDEOGRAPH-8B58 +← (‎ 識 ‎) F9FC CJK COMPATIBILITY IDEOGRAPH-F9FC + +# 讀 讀 + (‎ 讀 ‎) 8B80 CJK UNIFIED IDEOGRAPH-8B80 +← (‎ 讀 ‎) F95A CJK COMPATIBILITY IDEOGRAPH-F95A + +# 讆 讏 + (‎ 讆 ‎) 8B86 CJK UNIFIED IDEOGRAPH-8B86 +← (‎ 讏 ‎) 8B8F CJK UNIFIED IDEOGRAPH-8B8F + +# 變 變 變 + (‎ 變 ‎) 8B8A CJK UNIFIED IDEOGRAPH-8B8A +← (‎ 變 ‎) FAC0 CJK COMPATIBILITY IDEOGRAPH-FAC0 +← (‎ 變 ‎) 2F9D1 CJK COMPATIBILITY IDEOGRAPH-2F9D1 + +# 豈 豈 + (‎ 豈 ‎) 8C48 CJK UNIFIED IDEOGRAPH-8C48 +← (‎ 豈 ‎) F900 CJK COMPATIBILITY IDEOGRAPH-F900 + +# 豜 豣 + (‎ 豜 ‎) 8C5C CJK UNIFIED IDEOGRAPH-8C5C +← (‎ 豣 ‎) 8C63 CJK UNIFIED IDEOGRAPH-8C63 + +# 貫 貫 + (‎ 貫 ‎) 8CAB CJK UNIFIED IDEOGRAPH-8CAB +← (‎ 貫 ‎) 2F9D4 CJK COMPATIBILITY IDEOGRAPH-2F9D4 + +# 賁 賁 + (‎ 賁 ‎) 8CC1 CJK UNIFIED IDEOGRAPH-8CC1 +← (‎ 賁 ‎) 2F9D5 CJK COMPATIBILITY IDEOGRAPH-2F9D5 + +# 賂 賂 + (‎ 賂 ‎) 8CC2 CJK UNIFIED IDEOGRAPH-8CC2 +← (‎ 賂 ‎) F948 CJK COMPATIBILITY IDEOGRAPH-F948 + +# 賈 賈 + (‎ 賈 ‎) 8CC8 CJK UNIFIED IDEOGRAPH-8CC8 +← (‎ 賈 ‎) F903 CJK COMPATIBILITY IDEOGRAPH-F903 + +# 賓 賓 + (‎ 賓 ‎) 8CD3 CJK UNIFIED IDEOGRAPH-8CD3 +← (‎ 賓 ‎) FA64 CJK COMPATIBILITY IDEOGRAPH-FA64 + +# 贈 贈 贈 + (‎ 贈 ‎) 8D08 CJK UNIFIED IDEOGRAPH-8D08 +← (‎ 贈 ‎) FA65 CJK COMPATIBILITY IDEOGRAPH-FA65 +← (‎ 贈 ‎) FAC1 CJK COMPATIBILITY IDEOGRAPH-FAC1 + +# 贛 贛 + (‎ 贛 ‎) 8D1B CJK UNIFIED IDEOGRAPH-8D1B +← (‎ 贛 ‎) 2F9D6 CJK COMPATIBILITY IDEOGRAPH-2F9D6 + +# 起 起 + (‎ 起 ‎) 8D77 CJK UNIFIED IDEOGRAPH-8D77 +← (‎ 起 ‎) 2F9D7 CJK COMPATIBILITY IDEOGRAPH-2F9D7 + +# 赿 趆 + (‎ 赿 ‎) 8D7F CJK UNIFIED IDEOGRAPH-8D7F +← (‎ 趆 ‎) 8D86 CJK UNIFIED IDEOGRAPH-8D86 + +# 趼 趼 + (‎ 趼 ‎) 8DBC CJK UNIFIED IDEOGRAPH-8DBC +← (‎ 趼 ‎) 2F9DB CJK COMPATIBILITY IDEOGRAPH-2F9DB + +# 跋 跋 + (‎ 跋 ‎) 8DCB CJK UNIFIED IDEOGRAPH-8DCB +← (‎ 跋 ‎) 2F9DA CJK COMPATIBILITY IDEOGRAPH-2F9DA + +# 跥 跺 + (‎ 跥 ‎) 8DE5 CJK UNIFIED IDEOGRAPH-8DE5 +← (‎ 跺 ‎) 8DFA CJK UNIFIED IDEOGRAPH-8DFA + +# 路 路 + (‎ 路 ‎) 8DEF CJK UNIFIED IDEOGRAPH-8DEF +← (‎ 路 ‎) F937 CJK COMPATIBILITY IDEOGRAPH-F937 + +# 跰 跰 + (‎ 跰 ‎) 8DF0 CJK UNIFIED IDEOGRAPH-8DF0 +← (‎ 跰 ‎) 2F9DC CJK COMPATIBILITY IDEOGRAPH-2F9DC + +# 躗 躛 + (‎ 躗 ‎) 8E97 CJK UNIFIED IDEOGRAPH-8E97 +← (‎ 躛 ‎) 8E9B CJK UNIFIED IDEOGRAPH-8E9B + +# 軔 軔 + (‎ 軔 ‎) 8ED4 CJK UNIFIED IDEOGRAPH-8ED4 +← (‎ 軔 ‎) 2F9DE CJK COMPATIBILITY IDEOGRAPH-2F9DE + +# 軿 輧 + (‎ 軿 ‎) 8EFF CJK UNIFIED IDEOGRAPH-8EFF +← (‎ 輧 ‎) 8F27 CJK UNIFIED IDEOGRAPH-8F27 + +# 輦 輦 + (‎ 輦 ‎) 8F26 CJK UNIFIED IDEOGRAPH-8F26 +← (‎ 輦 ‎) F998 CJK COMPATIBILITY IDEOGRAPH-F998 + +# 輪 輪 + (‎ 輪 ‎) 8F2A CJK UNIFIED IDEOGRAPH-8F2A +← (‎ 輪 ‎) F9D7 CJK COMPATIBILITY IDEOGRAPH-F9D7 + +# 輸 輸 輸 + (‎ 輸 ‎) 8F38 CJK UNIFIED IDEOGRAPH-8F38 +← (‎ 輸 ‎) FAC2 CJK COMPATIBILITY IDEOGRAPH-FAC2 +← (‎ 輸 ‎) 2F9DF CJK COMPATIBILITY IDEOGRAPH-2F9DF + +# 輻 輻 + (‎ 輻 ‎) 8F3B CJK UNIFIED IDEOGRAPH-8F3B +← (‎ 輻 ‎) FA07 CJK COMPATIBILITY IDEOGRAPH-FA07 + +# 轢 轢 + (‎ 轢 ‎) 8F62 CJK UNIFIED IDEOGRAPH-8F62 +← (‎ 轢 ‎) F98D CJK COMPATIBILITY IDEOGRAPH-F98D + +# 辞 辞 + (‎ 辞 ‎) 8F9E CJK UNIFIED IDEOGRAPH-8F9E +← (‎ 辞 ‎) 2F98D CJK COMPATIBILITY IDEOGRAPH-2F98D + +# 連 連 + (‎ 連 ‎) 9023 CJK UNIFIED IDEOGRAPH-9023 +← (‎ 連 ‎) F99A CJK COMPATIBILITY IDEOGRAPH-F99A + +# 逸 逸 逸 + (‎ 逸 ‎) 9038 CJK UNIFIED IDEOGRAPH-9038 +← (‎ 逸 ‎) FA25 CJK COMPATIBILITY IDEOGRAPH-FA25 +← (‎ 逸 ‎) FA67 CJK COMPATIBILITY IDEOGRAPH-FA67 + +# 遲 遲 + (‎ 遲 ‎) 9072 CJK UNIFIED IDEOGRAPH-9072 +← (‎ 遲 ‎) FAC3 CJK COMPATIBILITY IDEOGRAPH-FAC3 + +# 遼 遼 + (‎ 遼 ‎) 907C CJK UNIFIED IDEOGRAPH-907C +← (‎ 遼 ‎) F9C3 CJK COMPATIBILITY IDEOGRAPH-F9C3 + +# 邏 邏 + (‎ 邏 ‎) 908F CJK UNIFIED IDEOGRAPH-908F +← (‎ 邏 ‎) F913 CJK COMPATIBILITY IDEOGRAPH-F913 + +# 邔 邔 + (‎ 邔 ‎) 9094 CJK UNIFIED IDEOGRAPH-9094 +← (‎ 邔 ‎) 2F9E2 CJK COMPATIBILITY IDEOGRAPH-2F9E2 + +# 郎 郞 郎 郞 + (‎ 郎 ‎) 90CE CJK UNIFIED IDEOGRAPH-90CE +← (‎ 郞 ‎) 90DE CJK UNIFIED IDEOGRAPH-90DE # →郎→ +← (‎ 郎 ‎) F92C CJK COMPATIBILITY IDEOGRAPH-F92C +← (‎ 郞 ‎) FA2E CJK COMPATIBILITY IDEOGRAPH-FA2E # →郞→→郎→ + +# 郱 郱 + (‎ 郱 ‎) 90F1 CJK UNIFIED IDEOGRAPH-90F1 +← (‎ 郱 ‎) 2F9E3 CJK COMPATIBILITY IDEOGRAPH-2F9E3 + +# 都 都 + (‎ 都 ‎) 90FD CJK UNIFIED IDEOGRAPH-90FD +← (‎ 都 ‎) FA26 CJK COMPATIBILITY IDEOGRAPH-FA26 + +# 鄑 鄑 + (‎ 鄑 ‎) 9111 CJK UNIFIED IDEOGRAPH-9111 +← (‎ 鄑 ‎) 2F9E4 CJK COMPATIBILITY IDEOGRAPH-2F9E4 + +# 鄛 鄛 + (‎ 鄛 ‎) 911B CJK UNIFIED IDEOGRAPH-911B +← (‎ 鄛 ‎) 2F9E6 CJK COMPATIBILITY IDEOGRAPH-2F9E6 + +# 酪 酪 + (‎ 酪 ‎) 916A CJK UNIFIED IDEOGRAPH-916A +← (‎ 酪 ‎) F919 CJK COMPATIBILITY IDEOGRAPH-F919 + +# 醙 醙 + (‎ 醙 ‎) 9199 CJK UNIFIED IDEOGRAPH-9199 +← (‎ 醙 ‎) FAC4 CJK COMPATIBILITY IDEOGRAPH-FAC4 + +# 醴 醴 + (‎ 醴 ‎) 91B4 CJK UNIFIED IDEOGRAPH-91B4 +← (‎ 醴 ‎) F9B7 CJK COMPATIBILITY IDEOGRAPH-F9B7 + +# 量 量 + (‎ 量 ‎) 91CF CJK UNIFIED IDEOGRAPH-91CF +← (‎ 量 ‎) F97E CJK COMPATIBILITY IDEOGRAPH-F97E + +# 鈴 鈴 + (‎ 鈴 ‎) 9234 CJK UNIFIED IDEOGRAPH-9234 +← (‎ 鈴 ‎) F9B1 CJK COMPATIBILITY IDEOGRAPH-F9B1 + +# 鈸 鈸 + (‎ 鈸 ‎) 9238 CJK UNIFIED IDEOGRAPH-9238 +← (‎ 鈸 ‎) 2F9E7 CJK COMPATIBILITY IDEOGRAPH-2F9E7 + +# 鉶 鉶 + (‎ 鉶 ‎) 9276 CJK UNIFIED IDEOGRAPH-9276 +← (‎ 鉶 ‎) FAC5 CJK COMPATIBILITY IDEOGRAPH-FAC5 + +# 鉼 鉼 + (‎ 鉼 ‎) 927C CJK UNIFIED IDEOGRAPH-927C +← (‎ 鉼 ‎) 2F9EA CJK COMPATIBILITY IDEOGRAPH-2F9EA + +# 鋗 鋗 + (‎ 鋗 ‎) 92D7 CJK UNIFIED IDEOGRAPH-92D7 +← (‎ 鋗 ‎) 2F9E8 CJK COMPATIBILITY IDEOGRAPH-2F9E8 + +# 鋘 鋘 + (‎ 鋘 ‎) 92D8 CJK UNIFIED IDEOGRAPH-92D8 +← (‎ 鋘 ‎) 2F9E9 CJK COMPATIBILITY IDEOGRAPH-2F9E9 + +# 錄 錄 + (‎ 錄 ‎) 9304 CJK UNIFIED IDEOGRAPH-9304 +← (‎ 錄 ‎) F93F CJK COMPATIBILITY IDEOGRAPH-F93F + +# 鍊 鍊 + (‎ 鍊 ‎) 934A CJK UNIFIED IDEOGRAPH-934A +← (‎ 鍊 ‎) F99B CJK COMPATIBILITY IDEOGRAPH-F99B + +# 鎭 鎮 + (‎ 鎭 ‎) 93AD CJK UNIFIED IDEOGRAPH-93AD +← (‎ 鎮 ‎) 93AE CJK UNIFIED IDEOGRAPH-93AE + +# 鏹 鏹 + (‎ 鏹 ‎) 93F9 CJK UNIFIED IDEOGRAPH-93F9 +← (‎ 鏹 ‎) 2F9EB CJK COMPATIBILITY IDEOGRAPH-2F9EB + +# 鐕 鐕 + (‎ 鐕 ‎) 9415 CJK UNIFIED IDEOGRAPH-9415 +← (‎ 鐕 ‎) 2F9EC CJK COMPATIBILITY IDEOGRAPH-2F9EC + +# 開 開 + (‎ 開 ‎) 958B CJK UNIFIED IDEOGRAPH-958B +← (‎ 開 ‎) 2F9EE CJK COMPATIBILITY IDEOGRAPH-2F9EE + +# 閭 閭 + (‎ 閭 ‎) 95AD CJK UNIFIED IDEOGRAPH-95AD +← (‎ 閭 ‎) F986 CJK COMPATIBILITY IDEOGRAPH-F986 + +# 閷 閷 + (‎ 閷 ‎) 95B7 CJK UNIFIED IDEOGRAPH-95B7 +← (‎ 閷 ‎) 2F9F0 CJK COMPATIBILITY IDEOGRAPH-2F9F0 + +# 阮 阮 + (‎ 阮 ‎) 962E CJK UNIFIED IDEOGRAPH-962E +← (‎ 阮 ‎) F9C6 CJK COMPATIBILITY IDEOGRAPH-F9C6 + +# 陋 陋 + (‎ 陋 ‎) 964B CJK UNIFIED IDEOGRAPH-964B +← (‎ 陋 ‎) F951 CJK COMPATIBILITY IDEOGRAPH-F951 + +# 降 降 + (‎ 降 ‎) 964D CJK UNIFIED IDEOGRAPH-964D +← (‎ 降 ‎) FA09 CJK COMPATIBILITY IDEOGRAPH-FA09 + +# 陵 陵 + (‎ 陵 ‎) 9675 CJK UNIFIED IDEOGRAPH-9675 +← (‎ 陵 ‎) F959 CJK COMPATIBILITY IDEOGRAPH-F959 + +# 陸 陸 + (‎ 陸 ‎) 9678 CJK UNIFIED IDEOGRAPH-9678 +← (‎ 陸 ‎) F9D3 CJK COMPATIBILITY IDEOGRAPH-F9D3 + +# 陼 陼 + (‎ 陼 ‎) 967C CJK UNIFIED IDEOGRAPH-967C +← (‎ 陼 ‎) FAC6 CJK COMPATIBILITY IDEOGRAPH-FAC6 + +# 隆 隆 + (‎ 隆 ‎) 9686 CJK UNIFIED IDEOGRAPH-9686 +← (‎ 隆 ‎) F9DC CJK COMPATIBILITY IDEOGRAPH-F9DC + +# 隣 隣 + (‎ 隣 ‎) 96A3 CJK UNIFIED IDEOGRAPH-96A3 +← (‎ 隣 ‎) F9F1 CJK COMPATIBILITY IDEOGRAPH-F9F1 + +# 隷 隸 隸 隷 + (‎ 隷 ‎) 96B7 CJK UNIFIED IDEOGRAPH-96B7 +← (‎ 隸 ‎) 96B8 CJK UNIFIED IDEOGRAPH-96B8 # →隸→ +← (‎ 隸 ‎) F9B8 CJK COMPATIBILITY IDEOGRAPH-F9B8 +← (‎ 隷 ‎) FA2F CJK COMPATIBILITY IDEOGRAPH-FA2F + +# 雃 雃 + (‎ 雃 ‎) 96C3 CJK UNIFIED IDEOGRAPH-96C3 +← (‎ 雃 ‎) 2F9F3 CJK COMPATIBILITY IDEOGRAPH-2F9F3 + +# 離 離 + (‎ 離 ‎) 96E2 CJK UNIFIED IDEOGRAPH-96E2 +← (‎ 離 ‎) F9EA CJK COMPATIBILITY IDEOGRAPH-F9EA + +# 難 難 難 + (‎ 難 ‎) 96E3 CJK UNIFIED IDEOGRAPH-96E3 +← (‎ 難 ‎) FA68 CJK COMPATIBILITY IDEOGRAPH-FA68 +← (‎ 難 ‎) FAC7 CJK COMPATIBILITY IDEOGRAPH-FAC7 + +# 零 零 + (‎ 零 ‎) 96F6 CJK UNIFIED IDEOGRAPH-96F6 +← (‎ 零 ‎) F9B2 CJK COMPATIBILITY IDEOGRAPH-F9B2 + +# 雷 雷 + (‎ 雷 ‎) 96F7 CJK UNIFIED IDEOGRAPH-96F7 +← (‎ 雷 ‎) F949 CJK COMPATIBILITY IDEOGRAPH-F949 + +# 霣 霣 + (‎ 霣 ‎) 9723 CJK UNIFIED IDEOGRAPH-9723 +← (‎ 霣 ‎) 2F9F5 CJK COMPATIBILITY IDEOGRAPH-2F9F5 + +# 露 露 + (‎ 露 ‎) 9732 CJK UNIFIED IDEOGRAPH-9732 +← (‎ 露 ‎) F938 CJK COMPATIBILITY IDEOGRAPH-F938 + +# 靈 靈 + (‎ 靈 ‎) 9748 CJK UNIFIED IDEOGRAPH-9748 +← (‎ 靈 ‎) F9B3 CJK COMPATIBILITY IDEOGRAPH-F9B3 + +# 靖 靖 靖 + (‎ 靖 ‎) 9756 CJK UNIFIED IDEOGRAPH-9756 +← (‎ 靖 ‎) FA1C CJK COMPATIBILITY IDEOGRAPH-FA1C +← (‎ 靖 ‎) FAC8 CJK COMPATIBILITY IDEOGRAPH-FAC8 + +# 韛 韛 + (‎ 韛 ‎) 97DB CJK UNIFIED IDEOGRAPH-97DB +← (‎ 韛 ‎) FAC9 CJK COMPATIBILITY IDEOGRAPH-FAC9 + +# 韠 韠 + (‎ 韠 ‎) 97E0 CJK UNIFIED IDEOGRAPH-97E0 +← (‎ 韠 ‎) 2F9FA CJK COMPATIBILITY IDEOGRAPH-2F9FA + +# 響 響 響 + (‎ 響 ‎) 97FF CJK UNIFIED IDEOGRAPH-97FF +← (‎ 響 ‎) FA69 CJK COMPATIBILITY IDEOGRAPH-FA69 +← (‎ 響 ‎) FACA CJK COMPATIBILITY IDEOGRAPH-FACA + +# 頋 頋 頋 頋 + (‎ 頋 ‎) 980B CJK UNIFIED IDEOGRAPH-980B +← (‎ 頋 ‎) FACB CJK COMPATIBILITY IDEOGRAPH-FACB +← (‎ 頋 ‎) 2F9FE CJK COMPATIBILITY IDEOGRAPH-2F9FE +← (‎ 頋 ‎) 2F9FF CJK COMPATIBILITY IDEOGRAPH-2F9FF + +# 領 領 + (‎ 領 ‎) 9818 CJK UNIFIED IDEOGRAPH-9818 +← (‎ 領 ‎) F9B4 CJK COMPATIBILITY IDEOGRAPH-F9B4 + +# 頩 頩 + (‎ 頩 ‎) 9829 CJK UNIFIED IDEOGRAPH-9829 +← (‎ 頩 ‎) 2FA00 CJK COMPATIBILITY IDEOGRAPH-2FA00 + +# 頻 頻 頻 + (‎ 頻 ‎) 983B CJK UNIFIED IDEOGRAPH-983B +← (‎ 頻 ‎) FA6A CJK COMPATIBILITY IDEOGRAPH-FA6A +← (‎ 頻 ‎) FACC CJK COMPATIBILITY IDEOGRAPH-FACC + +# 類 類 + (‎ 類 ‎) 985E CJK UNIFIED IDEOGRAPH-985E +← (‎ 類 ‎) F9D0 CJK COMPATIBILITY IDEOGRAPH-F9D0 + +# 飢 飢 + (‎ 飢 ‎) 98E2 CJK UNIFIED IDEOGRAPH-98E2 +← (‎ 飢 ‎) 2FA02 CJK COMPATIBILITY IDEOGRAPH-2FA02 + +# 飯 飯 + (‎ 飯 ‎) 98EF CJK UNIFIED IDEOGRAPH-98EF +← (‎ 飯 ‎) FA2A CJK COMPATIBILITY IDEOGRAPH-FA2A + +# 飼 飼 + (‎ 飼 ‎) 98FC CJK UNIFIED IDEOGRAPH-98FC +← (‎ 飼 ‎) FA2B CJK COMPATIBILITY IDEOGRAPH-FA2B + +# 館 館 + (‎ 館 ‎) 9928 CJK UNIFIED IDEOGRAPH-9928 +← (‎ 館 ‎) FA2C CJK COMPATIBILITY IDEOGRAPH-FA2C + +# 餩 餩 + (‎ 餩 ‎) 9929 CJK UNIFIED IDEOGRAPH-9929 +← (‎ 餩 ‎) 2FA04 CJK COMPATIBILITY IDEOGRAPH-2FA04 + +# 馧 馧 + (‎ 馧 ‎) 99A7 CJK UNIFIED IDEOGRAPH-99A7 +← (‎ 馧 ‎) 2FA05 CJK COMPATIBILITY IDEOGRAPH-2FA05 + +# 駂 駂 + (‎ 駂 ‎) 99C2 CJK UNIFIED IDEOGRAPH-99C2 +← (‎ 駂 ‎) 2FA06 CJK COMPATIBILITY IDEOGRAPH-2FA06 + +# 駱 駱 + (‎ 駱 ‎) 99F1 CJK UNIFIED IDEOGRAPH-99F1 +← (‎ 駱 ‎) F91A CJK COMPATIBILITY IDEOGRAPH-F91A + +# 駾 駾 + (‎ 駾 ‎) 99FE CJK UNIFIED IDEOGRAPH-99FE +← (‎ 駾 ‎) 2FA07 CJK COMPATIBILITY IDEOGRAPH-2FA07 + +# 驪 驪 + (‎ 驪 ‎) 9A6A CJK UNIFIED IDEOGRAPH-9A6A +← (‎ 驪 ‎) F987 CJK COMPATIBILITY IDEOGRAPH-F987 + +# 鬒 鬒 鬒 + (‎ 鬒 ‎) 9B12 CJK UNIFIED IDEOGRAPH-9B12 +← (‎ 鬒 ‎) FACD CJK COMPATIBILITY IDEOGRAPH-FACD +← (‎ 鬒 ‎) 2FA0A CJK COMPATIBILITY IDEOGRAPH-2FA0A + +# 魯 魯 + (‎ 魯 ‎) 9B6F CJK UNIFIED IDEOGRAPH-9B6F +← (‎ 魯 ‎) F939 CJK COMPATIBILITY IDEOGRAPH-F939 + +# 鱀 鱀 + (‎ 鱀 ‎) 9C40 CJK UNIFIED IDEOGRAPH-9C40 +← (‎ 鱀 ‎) 2FA0B CJK COMPATIBILITY IDEOGRAPH-2FA0B + +# 鱗 鱗 + (‎ 鱗 ‎) 9C57 CJK UNIFIED IDEOGRAPH-9C57 +← (‎ 鱗 ‎) F9F2 CJK COMPATIBILITY IDEOGRAPH-F9F2 + +# 鳽 鳽 + (‎ 鳽 ‎) 9CFD CJK UNIFIED IDEOGRAPH-9CFD +← (‎ 鳽 ‎) 2FA0C CJK COMPATIBILITY IDEOGRAPH-2FA0C + +# 鵧 鵧 + (‎ 鵧 ‎) 9D67 CJK UNIFIED IDEOGRAPH-9D67 +← (‎ 鵧 ‎) 2FA0F CJK COMPATIBILITY IDEOGRAPH-2FA0F + +# 鶴 鶴 + (‎ 鶴 ‎) 9DB4 CJK UNIFIED IDEOGRAPH-9DB4 +← (‎ 鶴 ‎) FA2D CJK COMPATIBILITY IDEOGRAPH-FA2D + +# 鷺 鷺 + (‎ 鷺 ‎) 9DFA CJK UNIFIED IDEOGRAPH-9DFA +← (‎ 鷺 ‎) F93A CJK COMPATIBILITY IDEOGRAPH-F93A + +# 鸞 鸞 + (‎ 鸞 ‎) 9E1E CJK UNIFIED IDEOGRAPH-9E1E +← (‎ 鸞 ‎) F920 CJK COMPATIBILITY IDEOGRAPH-F920 + +# 鹂 鹃 + (‎ 鹂 ‎) 9E42 CJK UNIFIED IDEOGRAPH-9E42 +← (‎ 鹃 ‎) 9E43 CJK UNIFIED IDEOGRAPH-9E43 + +# 麗 麗 + (‎ 麗 ‎) 9E97 CJK UNIFIED IDEOGRAPH-9E97 +← (‎ 麗 ‎) F988 CJK COMPATIBILITY IDEOGRAPH-F988 + +# 麟 麟 + (‎ 麟 ‎) 9E9F CJK UNIFIED IDEOGRAPH-9E9F +← (‎ 麟 ‎) F9F3 CJK COMPATIBILITY IDEOGRAPH-F9F3 + +# 黎 黎 + (‎ 黎 ‎) 9ECE CJK UNIFIED IDEOGRAPH-9ECE +← (‎ 黎 ‎) F989 CJK COMPATIBILITY IDEOGRAPH-F989 + +# 黾 黾 + (‎ 黾 ‎) 9EFE CJK UNIFIED IDEOGRAPH-9EFE +← (‎ 黾 ‎) 2FA18 CJK COMPATIBILITY IDEOGRAPH-2FA18 + +# 鼅 鼅 + (‎ 鼅 ‎) 9F05 CJK UNIFIED IDEOGRAPH-9F05 +← (‎ 鼅 ‎) 2FA19 CJK COMPATIBILITY IDEOGRAPH-2FA19 + +# 鼏 鼏 + (‎ 鼏 ‎) 9F0F CJK UNIFIED IDEOGRAPH-9F0F +← (‎ 鼏 ‎) 2FA1A CJK COMPATIBILITY IDEOGRAPH-2FA1A + +# 鼖 鼖 + (‎ 鼖 ‎) 9F16 CJK UNIFIED IDEOGRAPH-9F16 +← (‎ 鼖 ‎) 2FA1B CJK COMPATIBILITY IDEOGRAPH-2FA1B + +# 齃 齃 + (‎ 齃 ‎) 9F43 CJK UNIFIED IDEOGRAPH-9F43 +← (‎ 齃 ‎) FAD8 CJK COMPATIBILITY IDEOGRAPH-FAD8 + +# 龎 龎 + (‎ 龎 ‎) 9F8E CJK UNIFIED IDEOGRAPH-9F8E +← (‎ 龎 ‎) FAD9 CJK COMPATIBILITY IDEOGRAPH-FAD9 + +# ꁊ ꒞ + (‎ ꁊ ‎) A04A YI SYLLABLE PUT +← (‎ ꒞ ‎) A49E YI RADICAL PUT + +# ꁐ ꒬ + (‎ ꁐ ‎) A050 YI SYLLABLE PYT +← (‎ ꒬ ‎) A4AC YI RADICAL PYT + +# ꃀ ꒜ + (‎ ꃀ ‎) A0C0 YI SYLLABLE MOP +← (‎ ꒜ ‎) A49C YI RADICAL MOP + +# ꄲ ꒨ + (‎ ꄲ ‎) A132 YI SYLLABLE TU +← (‎ ꒨ ‎) A4A8 YI RADICAL TU + +# ꉙ ꒿ + (‎ ꉙ ‎) A259 YI SYLLABLE HXOP +← (‎ ꒿ ‎) A4BF YI RADICAL HXOP + +# ꊱ ꒾ + (‎ ꊱ ‎) A2B1 YI SYLLABLE CIP +← (‎ ꒾ ‎) A4BE YI RADICAL CIP + +# ꋍ ꒔ + (‎ ꋍ ‎) A2CD YI SYLLABLE CYP +← (‎ ꒔ ‎) A494 YI RADICAL CYP + +# ꎫ ꓀ + (‎ ꎫ ‎) A3AB YI SYLLABLE SHAT +← (‎ ꓀ ‎) A4C0 YI RADICAL SHAT + +# ꎵ ꓂ + (‎ ꎵ ‎) A3B5 YI SYLLABLE SHOP +← (‎ ꓂ ‎) A4C2 YI RADICAL SHOP + +# ꎿ ꒺ + (‎ ꎿ ‎) A3BF YI SYLLABLE SHUR +← (‎ ꒺ ‎) A4BA YI RADICAL SHUR + +# ꏂ ꒰ + (‎ ꏂ ‎) A3C2 YI SYLLABLE SHY +← (‎ ꒰ ‎) A4B0 YI RADICAL SHY + +# ꑘ ꒧ + (‎ ꑘ ‎) A458 YI SYLLABLE NYOP +← (‎ ꒧ ‎) A4A7 YI RADICAL NYOP + +# ꓤ Ꞟ + (‎ ꓤ ‎) A4E4 LISU LETTER ZA +← (‎ Ꞟ ‎) A79E LATIN CAPITAL LETTER VOLAPUK UE + +# Ꙍ Ꞷ + (‎ Ꙍ ‎) A64C CYRILLIC CAPITAL LETTER BROAD OMEGA +← (‎ Ꞷ ‎) A7B6 LATIN CAPITAL LETTER OMEGA + +# Ꙙ 𖼜 🜁 + (‎ Ꙙ ‎) A658 CYRILLIC CAPITAL LETTER CLOSED LITTLE YUS +← (‎ 𖼜 ‎) 16F1C MIAO LETTER TLHYA +← (‎ 🜁 ‎) 1F701 ALCHEMICAL SYMBOL FOR AIR + +# ꙩ 𐓫 + (‎ ꙩ ‎) A669 CYRILLIC SMALL LETTER MONOCULAR O +← (‎ 𐓫 ‎) 104EB OSAGE SMALL LETTER OIN + +# ꛳꛳ ꛴ + (‎ ꛳꛳ ‎) A6F3 A6F3 BAMUM FULL STOP, BAMUM FULL STOP +← (‎ ꛴ ‎) A6F4 BAMUM COLON + +# ꜧ ꞕ + (‎ ꜧ ‎) A727 LATIN SMALL LETTER HENG +← (‎ ꞕ ‎) A795 LATIN SMALL LETTER H WITH PALATAL HOOK + +# Ꝺ ꝺ + (‎ Ꝺ ‎) A779 LATIN CAPITAL LETTER INSULAR D +← (‎ ꝺ ‎) A77A LATIN SMALL LETTER INSULAR D + +# 𐐒 Ꞛ + (‎ Ꞛ ‎) A79A LATIN CAPITAL LETTER VOLAPUK AE +← (‎ 𐐒 ‎) 10412 DESERET CAPITAL LETTER BEE + +# 𐐺 ꞛ + (‎ ꞛ ‎) A79B LATIN SMALL LETTER VOLAPUK AE +← (‎ 𐐺 ‎) 1043A DESERET SMALL LETTER BEE + +# ꣻ 𑇜 + (‎ ꣻ ‎) A8FB DEVANAGARI HEADSTROKE +← (‎ 𑇜 ‎) 111DC SHARADA HEADSTROKE + +# ꣼ 𑇛 + (‎ ꣼ ‎) A8FC DEVANAGARI SIGN SIDDHAM +← (‎ 𑇛 ‎) 111DB SHARADA SIGN SIDDHAM + +# ꦝ ꦣ + (‎ ꦝ ‎) A99D JAVANESE LETTER DDA +← (‎ ꦣ ‎) A9A3 JAVANESE LETTER DA MAHAPRANA + +# ꧐ ꧆ + (‎ ꧆ ‎) A9C6 JAVANESE PADA WINDU +← (‎ ꧐ ‎) A9D0 JAVANESE DIGIT ZERO + +# ꨁ ꩓ + (‎ ꨁ ‎) AA01 CHAM LETTER I +← (‎ ꩓ ‎) AA53 CHAM DIGIT THREE + +# ꨣ ꩖ + (‎ ꨣ ‎) AA23 CHAM LETTER RA +← (‎ ꩖ ‎) AA56 CHAM DIGIT SIX + +# 𤋮 𤋮 + (‎ 𤋮 ‎) FA6C CJK COMPATIBILITY IDEOGRAPH-FA6C +← (‎ 𤋮 ‎) 242EE CJK UNIFIED IDEOGRAPH-242EE + +# 𢡊 𢡊 + (‎ 𢡊 ‎) FACF CJK COMPATIBILITY IDEOGRAPH-FACF +← (‎ 𢡊 ‎) 2284A CJK UNIFIED IDEOGRAPH-2284A + +# 𢡄 𢡄 + (‎ 𢡄 ‎) FAD0 CJK COMPATIBILITY IDEOGRAPH-FAD0 +← (‎ 𢡄 ‎) 22844 CJK UNIFIED IDEOGRAPH-22844 + +# 𣏕 𣏕 + (‎ 𣏕 ‎) FAD1 CJK COMPATIBILITY IDEOGRAPH-FAD1 +← (‎ 𣏕 ‎) 233D5 CJK UNIFIED IDEOGRAPH-233D5 + +# 𥉉 𥉉 + (‎ 𥉉 ‎) FAD5 CJK COMPATIBILITY IDEOGRAPH-FAD5 +← (‎ 𥉉 ‎) 25249 CJK UNIFIED IDEOGRAPH-25249 + +# 𥳐 𥳐 + (‎ 𥳐 ‎) FAD6 CJK COMPATIBILITY IDEOGRAPH-FAD6 +← (‎ 𥳐 ‎) 25CD0 CJK UNIFIED IDEOGRAPH-25CD0 + +# 𧻓 𧻓 + (‎ 𧻓 ‎) FAD7 CJK COMPATIBILITY IDEOGRAPH-FAD7 +← (‎ 𧻓 ‎) 27ED3 CJK UNIFIED IDEOGRAPH-27ED3 + +# יִ יּ + (‎ יִ ‎) FB1D HEBREW LETTER YOD WITH HIRIQ +← (‎ יּ ‎) FB39 HEBREW LETTER YOD WITH DAGESH + +# שׁ שׂ שּ + (‎ שׁ ‎) FB2A HEBREW LETTER SHIN WITH SHIN DOT +← (‎ שׂ ‎) FB2B HEBREW LETTER SHIN WITH SIN DOT +← (‎ שּ ‎) FB49 HEBREW LETTER SHIN WITH DAGESH + +# שּׁ שּׂ + (‎ שּׁ ‎) FB2C HEBREW LETTER SHIN WITH DAGESH AND SHIN DOT +← (‎ שּׂ ‎) FB2D HEBREW LETTER SHIN WITH DAGESH AND SIN DOT + +# אַ אָ אּ + (‎ אַ ‎) FB2E HEBREW LETTER ALEF WITH PATAH +← (‎ אָ ‎) FB2F HEBREW LETTER ALEF WITH QAMATS +← (‎ אּ ‎) FB30 HEBREW LETTER ALEF WITH MAPIQ + +# ﹲّ ﱞ + (‎ ﱞ ‎) FC5E ARABIC LIGATURE SHADDA WITH DAMMATAN ISOLATED FORM +← (‎ ﹲّ ‎) FE72 0651 ARABIC DAMMATAN ISOLATED FORM, ARABIC SHADDA + +# ﹴّ ﱟ + (‎ ﱟ ‎) FC5F ARABIC LIGATURE SHADDA WITH KASRATAN ISOLATED FORM +← (‎ ﹴّ ‎) FE74 0651 ARABIC KASRATAN ISOLATED FORM, ARABIC SHADDA + +# ﹶّ ﱠ + (‎ ﱠ ‎) FC60 ARABIC LIGATURE SHADDA WITH FATHA ISOLATED FORM +← (‎ ﹶّ ‎) FE76 0651 ARABIC FATHA ISOLATED FORM, ARABIC SHADDA + +# ﹸّ ﱡ + (‎ ﱡ ‎) FC61 ARABIC LIGATURE SHADDA WITH DAMMA ISOLATED FORM +← (‎ ﹸّ ‎) FE78 0651 ARABIC DAMMA ISOLATED FORM, ARABIC SHADDA + +# ﹺّ ﱢ + (‎ ﱢ ‎) FC62 ARABIC LIGATURE SHADDA WITH KASRA ISOLATED FORM +← (‎ ﹺّ ‎) FE7A 0651 ARABIC KASRA ISOLATED FORM, ARABIC SHADDA + +# ﹼٰ ﱣ + (‎ ﱣ ‎) FC63 ARABIC LIGATURE SHADDA WITH SUPERSCRIPT ALEF ISOLATED FORM +← (‎ ﹼٰ ‎) FE7C 0670 ARABIC SHADDA ISOLATED FORM, ARABIC LETTER SUPERSCRIPT ALEF + +# ﹷّ ﳲ + (‎ ﳲ ‎) FCF2 ARABIC LIGATURE SHADDA WITH FATHA MEDIAL FORM +← (‎ ﹷّ ‎) FE77 0651 ARABIC FATHA MEDIAL FORM, ARABIC SHADDA + +# ﹹّ ﳳ + (‎ ﳳ ‎) FCF3 ARABIC LIGATURE SHADDA WITH DAMMA MEDIAL FORM +← (‎ ﹹّ ‎) FE79 0651 ARABIC DAMMA MEDIAL FORM, ARABIC SHADDA + +# ﹻّ ﳴ + (‎ ﳴ ‎) FCF4 ARABIC LIGATURE SHADDA WITH KASRA MEDIAL FORM +← (‎ ﹻّ ‎) FE7B 0651 ARABIC KASRA MEDIAL FORM, ARABIC SHADDA + +# ︿ ^ + (‎ ︿ ‎) FE3F PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET +← (‎ ^ ‎) FF3E FULLWIDTH CIRCUMFLEX ACCENT + +# 𐎂 𐏑 + (‎ 𐎂 ‎) 10382 UGARITIC LETTER GAMLA +← (‎ 𐏑 ‎) 103D1 OLD PERSIAN NUMBER ONE + +# 𐎓 𐏓 + (‎ 𐎓 ‎) 10393 UGARITIC LETTER AIN +← (‎ 𐏓 ‎) 103D3 OLD PERSIAN NUMBER TEN + +# 𐎚 𒀸 + (‎ 𐎚 ‎) 1039A UGARITIC LETTER TO +← (‎ 𒀸 ‎) 12038 CUNEIFORM SIGN ASH + +# 𐒆 𐒠 + (‎ 𐒆 ‎) 10486 OSMANYA LETTER DEEL +← (‎ 𐒠 ‎) 104A0 OSMANYA DIGIT ZERO + +# 𐩖𐩖 𐩗 + (‎ 𐩖𐩖 ‎) 10A56 10A56 KHAROSHTHI PUNCTUATION DANDA, KHAROSHTHI PUNCTUATION DANDA +← (‎ 𐩗 ‎) 10A57 KHAROSHTHI PUNCTUATION DOUBLE DANDA + +# 𐲂 𐳼 + (‎ 𐲂 ‎) 10C82 OLD HUNGARIAN CAPITAL LETTER EB +← (‎ 𐳼 ‎) 10CFC OLD HUNGARIAN NUMBER TEN + +# 𐲥 𐳺 + (‎ 𐲥 ‎) 10CA5 OLD HUNGARIAN CAPITAL LETTER ESZ +← (‎ 𐳺 ‎) 10CFA OLD HUNGARIAN NUMBER ONE + +# 𑐴𑑂𑐒 𑐓 + (‎ 𑐓 ‎) 11413 NEWA LETTER NGHA +← (‎ 𑐴𑑂𑐒 ‎) 11434 11442 11412 NEWA LETTER HA, NEWA SIGN VIRAMA, NEWA LETTER NGA + +# 𑐴𑑂𑐘 𑐙 + (‎ 𑐙 ‎) 11419 NEWA LETTER NYHA +← (‎ 𑐴𑑂𑐘 ‎) 11434 11442 11418 NEWA LETTER HA, NEWA SIGN VIRAMA, NEWA LETTER NYA + +# 𑐴𑑂𑐣 𑐤 + (‎ 𑐤 ‎) 11424 NEWA LETTER NHA +← (‎ 𑐴𑑂𑐣 ‎) 11434 11442 11423 NEWA LETTER HA, NEWA SIGN VIRAMA, NEWA LETTER NA + +# 𑐴𑑂𑐩 𑐪 + (‎ 𑐪 ‎) 1142A NEWA LETTER MHA +← (‎ 𑐴𑑂𑐩 ‎) 11434 11442 11429 NEWA LETTER HA, NEWA SIGN VIRAMA, NEWA LETTER MA + +# 𑐴𑑂𑐬 𑐭 + (‎ 𑐭 ‎) 1142D NEWA LETTER RHA +← (‎ 𑐴𑑂𑐬 ‎) 11434 11442 1142C NEWA LETTER HA, NEWA SIGN VIRAMA, NEWA LETTER RA + +# 𑐴𑑂𑐮 𑐯 + (‎ 𑐯 ‎) 1142F NEWA LETTER LHA +← (‎ 𑐴𑑂𑐮 ‎) 11434 11442 1142E NEWA LETTER HA, NEWA SIGN VIRAMA, NEWA LETTER LA + +# 𑑋𑑋 𑑌 + (‎ 𑑋𑑋 ‎) 1144B 1144B NEWA DANDA, NEWA DANDA +← (‎ 𑑌 ‎) 1144C NEWA DOUBLE DANDA + +# 𑖂 𑗘 𑗙 + (‎ 𑖂 ‎) 11582 SIDDHAM LETTER I +← (‎ 𑗘 ‎) 115D8 SIDDHAM LETTER THREE-CIRCLE ALTERNATE I +← (‎ 𑗙 ‎) 115D9 SIDDHAM LETTER TWO-CIRCLE ALTERNATE I + +# 𑖃 𑗚 + (‎ 𑖃 ‎) 11583 SIDDHAM LETTER II +← (‎ 𑗚 ‎) 115DA SIDDHAM LETTER TWO-CIRCLE ALTERNATE II + +# 𑖄 𑗛 + (‎ 𑖄 ‎) 11584 SIDDHAM LETTER U +← (‎ 𑗛 ‎) 115DB SIDDHAM LETTER ALTERNATE U + +# 𑖲 𑗜 + (‎ 𑖲 ‎) 115B2 SIDDHAM VOWEL SIGN U +← (‎ 𑗜 ‎) 115DC SIDDHAM VOWEL SIGN ALTERNATE U + +# 𑖳 𑗝 + (‎ 𑖳 ‎) 115B3 SIDDHAM VOWEL SIGN UU +← (‎ 𑗝 ‎) 115DD SIDDHAM VOWEL SIGN ALTERNATE UU + +# 𑙁𑙁 𑙂 + (‎ 𑙁𑙁 ‎) 11641 11641 MODI DANDA, MODI DANDA +← (‎ 𑙂 ‎) 11642 MODI DOUBLE DANDA + +# 𑫥𑫥 𑫨 + (‎ 𑫥𑫥 ‎) 11AE5 11AE5 PAU CIN HAU RISING TONE LONG, PAU CIN HAU RISING TONE LONG +← (‎ 𑫨 ‎) 11AE8 PAU CIN HAU RISING TONE LONG FINAL + +# 𑫥𑫥𑫯 𑫥𑫦 𑫩 + (‎ 𑫥𑫥𑫯 ‎) 11AE5 11AE5 11AEF PAU CIN HAU RISING TONE LONG, PAU CIN HAU RISING TONE LONG, PAU CIN HAU MID-LEVEL TONE +← (‎ 𑫥𑫦 ‎) 11AE5 11AE6 PAU CIN HAU RISING TONE LONG, PAU CIN HAU RISING TONE +← (‎ 𑫩 ‎) 11AE9 PAU CIN HAU RISING TONE FINAL # →𑫥𑫦→ + +# 𑫥𑫥𑫰 𑫥𑫧 𑫪 + (‎ 𑫥𑫥𑫰 ‎) 11AE5 11AE5 11AF0 PAU CIN HAU RISING TONE LONG, PAU CIN HAU RISING TONE LONG, PAU CIN HAU GLOTTAL STOP VARIANT +← (‎ 𑫥𑫧 ‎) 11AE5 11AE7 PAU CIN HAU RISING TONE LONG, PAU CIN HAU SANDHI GLOTTAL STOP +← (‎ 𑫪 ‎) 11AEA PAU CIN HAU SANDHI GLOTTAL STOP FINAL # →𑫥𑫧→ + +# 𑫥𑫯 𑫦 + (‎ 𑫥𑫯 ‎) 11AE5 11AEF PAU CIN HAU RISING TONE LONG, PAU CIN HAU MID-LEVEL TONE +← (‎ 𑫦 ‎) 11AE6 PAU CIN HAU RISING TONE + +# 𑫥𑫰 𑫧 + (‎ 𑫥𑫰 ‎) 11AE5 11AF0 PAU CIN HAU RISING TONE LONG, PAU CIN HAU GLOTTAL STOP VARIANT +← (‎ 𑫧 ‎) 11AE7 PAU CIN HAU SANDHI GLOTTAL STOP + +# 𑫫𑫫 𑫭 + (‎ 𑫫𑫫 ‎) 11AEB 11AEB PAU CIN HAU SANDHI TONE LONG, PAU CIN HAU SANDHI TONE LONG +← (‎ 𑫭 ‎) 11AED PAU CIN HAU SANDHI TONE LONG FINAL + +# 𑫫𑫫𑫯 𑫫𑫬 𑫮 + (‎ 𑫫𑫫𑫯 ‎) 11AEB 11AEB 11AEF PAU CIN HAU SANDHI TONE LONG, PAU CIN HAU SANDHI TONE LONG, PAU CIN HAU MID-LEVEL TONE +← (‎ 𑫫𑫬 ‎) 11AEB 11AEC PAU CIN HAU SANDHI TONE LONG, PAU CIN HAU SANDHI TONE +← (‎ 𑫮 ‎) 11AEE PAU CIN HAU SANDHI TONE FINAL # →𑫫𑫬→ + +# 𑫫𑫯 𑫬 + (‎ 𑫫𑫯 ‎) 11AEB 11AEF PAU CIN HAU SANDHI TONE LONG, PAU CIN HAU MID-LEVEL TONE +← (‎ 𑫬 ‎) 11AEC PAU CIN HAU SANDHI TONE + +# 𑫳𑫯 𑫴 + (‎ 𑫳𑫯 ‎) 11AF3 11AEF PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU MID-LEVEL TONE +← (‎ 𑫴 ‎) 11AF4 PAU CIN HAU LOW-FALLING TONE + +# 𑫳𑫰 𑫵 + (‎ 𑫳𑫰 ‎) 11AF3 11AF0 PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU GLOTTAL STOP VARIANT +← (‎ 𑫵 ‎) 11AF5 PAU CIN HAU GLOTTAL STOP + +# 𑫳𑫳 𑫶 + (‎ 𑫳𑫳 ‎) 11AF3 11AF3 PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU LOW-FALLING TONE LONG +← (‎ 𑫶 ‎) 11AF6 PAU CIN HAU LOW-FALLING TONE LONG FINAL + +# 𑫳𑫳𑫯 𑫳𑫴 𑫷 + (‎ 𑫳𑫳𑫯 ‎) 11AF3 11AF3 11AEF PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU MID-LEVEL TONE +← (‎ 𑫳𑫴 ‎) 11AF3 11AF4 PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU LOW-FALLING TONE +← (‎ 𑫷 ‎) 11AF7 PAU CIN HAU LOW-FALLING TONE FINAL # →𑫳𑫴→ + +# 𑫳𑫳𑫰 𑫳𑫵 𑫸 + (‎ 𑫳𑫳𑫰 ‎) 11AF3 11AF3 11AF0 PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU GLOTTAL STOP VARIANT +← (‎ 𑫳𑫵 ‎) 11AF3 11AF5 PAU CIN HAU LOW-FALLING TONE LONG, PAU CIN HAU GLOTTAL STOP +← (‎ 𑫸 ‎) 11AF8 PAU CIN HAU GLOTTAL STOP FINAL # →𑫳𑫵→ + +# 𑱁𑱁 𑱂 + (‎ 𑱁𑱁 ‎) 11C41 11C41 BHAIKSUKI DANDA, BHAIKSUKI DANDA +← (‎ 𑱂 ‎) 11C42 BHAIKSUKI DOUBLE DANDA + +# 𑲪 𑲲 + (‎ 𑲪 ‎) 11CAA MARCHEN SUBJOINED LETTER RA +← (‎ 𑲲 ‎) 11CB2 MARCHEN VOWEL SIGN U + +# 𠄢 𠄢 + (‎ 𠄢 ‎) 20122 CJK UNIFIED IDEOGRAPH-20122 +← (‎ 𠄢 ‎) 2F803 CJK COMPATIBILITY IDEOGRAPH-2F803 + +# 𠔜 𠔜 + (‎ 𠔜 ‎) 2051C CJK UNIFIED IDEOGRAPH-2051C +← (‎ 𠔜 ‎) 2F812 CJK COMPATIBILITY IDEOGRAPH-2F812 + +# 𠔥 𠔥 + (‎ 𠔥 ‎) 20525 CJK UNIFIED IDEOGRAPH-20525 +← (‎ 𠔥 ‎) 2F91B CJK COMPATIBILITY IDEOGRAPH-2F91B + +# 𠕋 𠕋 + (‎ 𠕋 ‎) 2054B CJK UNIFIED IDEOGRAPH-2054B +← (‎ 𠕋 ‎) 2F816 CJK COMPATIBILITY IDEOGRAPH-2F816 + +# 𠘺 𠘺 + (‎ 𠘺 ‎) 2063A CJK UNIFIED IDEOGRAPH-2063A +← (‎ 𠘺 ‎) 2F80D CJK COMPATIBILITY IDEOGRAPH-2F80D + +# 𠠄 𠠄 + (‎ 𠠄 ‎) 20804 CJK UNIFIED IDEOGRAPH-20804 +← (‎ 𠠄 ‎) 2F9D9 CJK COMPATIBILITY IDEOGRAPH-2F9D9 + +# 𠣞 𠣞 + (‎ 𠣞 ‎) 208DE CJK UNIFIED IDEOGRAPH-208DE +← (‎ 𠣞 ‎) 2F9DD CJK COMPATIBILITY IDEOGRAPH-2F9DD + +# 𠨬 𠨬 + (‎ 𠨬 ‎) 20A2C CJK UNIFIED IDEOGRAPH-20A2C +← (‎ 𠨬 ‎) 2F834 CJK COMPATIBILITY IDEOGRAPH-2F834 + +# 𠭣 𠭣 + (‎ 𠭣 ‎) 20B63 CJK UNIFIED IDEOGRAPH-20B63 +← (‎ 𠭣 ‎) 2F838 CJK COMPATIBILITY IDEOGRAPH-2F838 + +# 𡓤 𡓤 + (‎ 𡓤 ‎) 214E4 CJK UNIFIED IDEOGRAPH-214E4 +← (‎ 𡓤 ‎) 2F859 CJK COMPATIBILITY IDEOGRAPH-2F859 + +# 𡚨 𡚨 + (‎ 𡚨 ‎) 216A8 CJK UNIFIED IDEOGRAPH-216A8 +← (‎ 𡚨 ‎) 2F860 CJK COMPATIBILITY IDEOGRAPH-2F860 + +# 𡛪 𡛪 + (‎ 𡛪 ‎) 216EA CJK UNIFIED IDEOGRAPH-216EA +← (‎ 𡛪 ‎) 2F861 CJK COMPATIBILITY IDEOGRAPH-2F861 + +# 𡧈 𡧈 + (‎ 𡧈 ‎) 219C8 CJK UNIFIED IDEOGRAPH-219C8 +← (‎ 𡧈 ‎) 2F86C CJK COMPATIBILITY IDEOGRAPH-2F86C + +# 𡬘 𡬘 + (‎ 𡬘 ‎) 21B18 CJK UNIFIED IDEOGRAPH-21B18 +← (‎ 𡬘 ‎) 2F871 CJK COMPATIBILITY IDEOGRAPH-2F871 + +# 𡴋 𡴋 + (‎ 𡴋 ‎) 21D0B CJK UNIFIED IDEOGRAPH-21D0B +← (‎ 𡴋 ‎) 2F8F8 CJK COMPATIBILITY IDEOGRAPH-2F8F8 + +# 𡷤 𡷤 + (‎ 𡷤 ‎) 21DE4 CJK UNIFIED IDEOGRAPH-21DE4 +← (‎ 𡷤 ‎) 2F87B CJK COMPATIBILITY IDEOGRAPH-2F87B + +# 𡷦 𡷦 + (‎ 𡷦 ‎) 21DE6 CJK UNIFIED IDEOGRAPH-21DE6 +← (‎ 𡷦 ‎) 2F87D CJK COMPATIBILITY IDEOGRAPH-2F87D + +# 𢆃 𢆃 + (‎ 𢆃 ‎) 22183 CJK UNIFIED IDEOGRAPH-22183 +← (‎ 𢆃 ‎) 2F889 CJK COMPATIBILITY IDEOGRAPH-2F889 + +# 𢆟 𢆟 + (‎ 𢆟 ‎) 2219F CJK UNIFIED IDEOGRAPH-2219F +← (‎ 𢆟 ‎) 2F939 CJK COMPATIBILITY IDEOGRAPH-2F939 + +# 𢌱 𢌱 𢌱 + (‎ 𢌱 ‎) 22331 CJK UNIFIED IDEOGRAPH-22331 +← (‎ 𢌱 ‎) 2F891 CJK COMPATIBILITY IDEOGRAPH-2F891 +← (‎ 𢌱 ‎) 2F892 CJK COMPATIBILITY IDEOGRAPH-2F892 + +# 𢛔 𢛔 + (‎ 𢛔 ‎) 226D4 CJK UNIFIED IDEOGRAPH-226D4 +← (‎ 𢛔 ‎) 2F8A4 CJK COMPATIBILITY IDEOGRAPH-2F8A4 + +# 𢬌 𢬌 + (‎ 𢬌 ‎) 22B0C CJK UNIFIED IDEOGRAPH-22B0C +← (‎ 𢬌 ‎) 2F8B8 CJK COMPATIBILITY IDEOGRAPH-2F8B8 + +# 𢯱 𢯱 + (‎ 𢯱 ‎) 22BF1 CJK UNIFIED IDEOGRAPH-22BF1 +← (‎ 𢯱 ‎) 2F8BE CJK COMPATIBILITY IDEOGRAPH-2F8BE + +# 𣀊 𣀊 + (‎ 𣀊 ‎) 2300A CJK UNIFIED IDEOGRAPH-2300A +← (‎ 𣀊 ‎) 2F8CA CJK COMPATIBILITY IDEOGRAPH-2F8CA + +# 𣊸 𣊸 + (‎ 𣊸 ‎) 232B8 CJK UNIFIED IDEOGRAPH-232B8 +← (‎ 𣊸 ‎) 2F897 CJK COMPATIBILITY IDEOGRAPH-2F897 + +# 𣍟 𣍟 + (‎ 𣍟 ‎) 2335F CJK UNIFIED IDEOGRAPH-2335F +← (‎ 𣍟 ‎) 2F980 CJK COMPATIBILITY IDEOGRAPH-2F980 + +# 𣎓 𣎓 + (‎ 𣎓 ‎) 23393 CJK UNIFIED IDEOGRAPH-23393 +← (‎ 𣎓 ‎) 2F989 CJK COMPATIBILITY IDEOGRAPH-2F989 + +# 𣎜 𣎜 + (‎ 𣎜 ‎) 2339C CJK UNIFIED IDEOGRAPH-2339C +← (‎ 𣎜 ‎) 2F98A CJK COMPATIBILITY IDEOGRAPH-2F98A + +# 𣏃 𣏃 + (‎ 𣏃 ‎) 233C3 CJK UNIFIED IDEOGRAPH-233C3 +← (‎ 𣏃 ‎) 2F8DD CJK COMPATIBILITY IDEOGRAPH-2F8DD + +# 𣑭 𣑭 + (‎ 𣑭 ‎) 2346D CJK UNIFIED IDEOGRAPH-2346D +← (‎ 𣑭 ‎) 2F8E3 CJK COMPATIBILITY IDEOGRAPH-2F8E3 + +# 𣚣 𣚣 + (‎ 𣚣 ‎) 236A3 CJK UNIFIED IDEOGRAPH-236A3 +← (‎ 𣚣 ‎) 2F8EC CJK COMPATIBILITY IDEOGRAPH-2F8EC + +# 𣢧 𣢧 + (‎ 𣢧 ‎) 238A7 CJK UNIFIED IDEOGRAPH-238A7 +← (‎ 𣢧 ‎) 2F8F0 CJK COMPATIBILITY IDEOGRAPH-2F8F0 + +# 𣪍 𣪍 + (‎ 𣪍 ‎) 23A8D CJK UNIFIED IDEOGRAPH-23A8D +← (‎ 𣪍 ‎) 2F8F7 CJK COMPATIBILITY IDEOGRAPH-2F8F7 + +# 𣫺 𣫺 + (‎ 𣫺 ‎) 23AFA CJK UNIFIED IDEOGRAPH-23AFA +← (‎ 𣫺 ‎) 2F8F9 CJK COMPATIBILITY IDEOGRAPH-2F8F9 + +# 𣲼 𣲼 + (‎ 𣲼 ‎) 23CBC CJK UNIFIED IDEOGRAPH-23CBC +← (‎ 𣲼 ‎) 2F8FB CJK COMPATIBILITY IDEOGRAPH-2F8FB + +# 𣴞 𣴞 + (‎ 𣴞 ‎) 23D1E CJK UNIFIED IDEOGRAPH-23D1E +← (‎ 𣴞 ‎) 2F906 CJK COMPATIBILITY IDEOGRAPH-2F906 + +# 𣻑 𣻑 + (‎ 𣻑 ‎) 23ED1 CJK UNIFIED IDEOGRAPH-23ED1 +← (‎ 𣻑 ‎) 2F90D CJK COMPATIBILITY IDEOGRAPH-2F90D + +# 𣽞 𣽞 + (‎ 𣽞 ‎) 23F5E CJK UNIFIED IDEOGRAPH-23F5E +← (‎ 𣽞 ‎) 2F910 CJK COMPATIBILITY IDEOGRAPH-2F910 + +# 𣾎 𣾎 + (‎ 𣾎 ‎) 23F8E CJK UNIFIED IDEOGRAPH-23F8E +← (‎ 𣾎 ‎) 2F911 CJK COMPATIBILITY IDEOGRAPH-2F911 + +# 𤉣 𤉣 + (‎ 𤉣 ‎) 24263 CJK UNIFIED IDEOGRAPH-24263 +← (‎ 𤉣 ‎) 2F91D CJK COMPATIBILITY IDEOGRAPH-2F91D + +# 𤎫 𤎫 + (‎ 𤎫 ‎) 243AB CJK UNIFIED IDEOGRAPH-243AB +← (‎ 𤎫 ‎) 2F91F CJK COMPATIBILITY IDEOGRAPH-2F91F + +# 𤘈 𤘈 + (‎ 𤘈 ‎) 24608 CJK UNIFIED IDEOGRAPH-24608 +← (‎ 𤘈 ‎) 2F923 CJK COMPATIBILITY IDEOGRAPH-2F923 + +# 𤜵 𤜵 + (‎ 𤜵 ‎) 24735 CJK UNIFIED IDEOGRAPH-24735 +← (‎ 𤜵 ‎) 2F926 CJK COMPATIBILITY IDEOGRAPH-2F926 + +# 𤠔 𤠔 + (‎ 𤠔 ‎) 24814 CJK UNIFIED IDEOGRAPH-24814 +← (‎ 𤠔 ‎) 2F927 CJK COMPATIBILITY IDEOGRAPH-2F927 + +# 𤰶 𤰶 + (‎ 𤰶 ‎) 24C36 CJK UNIFIED IDEOGRAPH-24C36 +← (‎ 𤰶 ‎) 2F935 CJK COMPATIBILITY IDEOGRAPH-2F935 + +# 𤲒 𤲒 + (‎ 𤲒 ‎) 24C92 CJK UNIFIED IDEOGRAPH-24C92 +← (‎ 𤲒 ‎) 2F937 CJK COMPATIBILITY IDEOGRAPH-2F937 + +# 𤾡 𤾡 + (‎ 𤾡 ‎) 24FA1 CJK UNIFIED IDEOGRAPH-24FA1 +← (‎ 𤾡 ‎) 2F93B CJK COMPATIBILITY IDEOGRAPH-2F93B + +# 𤾸 𤾸 + (‎ 𤾸 ‎) 24FB8 CJK UNIFIED IDEOGRAPH-24FB8 +← (‎ 𤾸 ‎) 2F93C CJK COMPATIBILITY IDEOGRAPH-2F93C + +# 𥁄 𥁄 + (‎ 𥁄 ‎) 25044 CJK UNIFIED IDEOGRAPH-25044 +← (‎ 𥁄 ‎) 2F93D CJK COMPATIBILITY IDEOGRAPH-2F93D + +# 𥃲 𥃲 + (‎ 𥃲 ‎) 250F2 CJK UNIFIED IDEOGRAPH-250F2 +← (‎ 𥃲 ‎) 2F942 CJK COMPATIBILITY IDEOGRAPH-2F942 + +# 𥃳 𥃳 + (‎ 𥃳 ‎) 250F3 CJK UNIFIED IDEOGRAPH-250F3 +← (‎ 𥃳 ‎) 2F941 CJK COMPATIBILITY IDEOGRAPH-2F941 + +# 𥄙 𥄙 + (‎ 𥄙 ‎) 25119 CJK UNIFIED IDEOGRAPH-25119 +← (‎ 𥄙 ‎) 2F943 CJK COMPATIBILITY IDEOGRAPH-2F943 + +# 𥄳 𥄳 + (‎ 𥄳 ‎) 25133 CJK UNIFIED IDEOGRAPH-25133 +← (‎ 𥄳 ‎) 2F944 CJK COMPATIBILITY IDEOGRAPH-2F944 + +# 𥐝 𥐝 + (‎ 𥐝 ‎) 2541D CJK UNIFIED IDEOGRAPH-2541D +← (‎ 𥐝 ‎) 2F94D CJK COMPATIBILITY IDEOGRAPH-2F94D + +# 𥘦 𥘦 + (‎ 𥘦 ‎) 25626 CJK UNIFIED IDEOGRAPH-25626 +← (‎ 𥘦 ‎) 2F952 CJK COMPATIBILITY IDEOGRAPH-2F952 + +# 𥚚 𥚚 + (‎ 𥚚 ‎) 2569A CJK UNIFIED IDEOGRAPH-2569A +← (‎ 𥚚 ‎) 2F954 CJK COMPATIBILITY IDEOGRAPH-2F954 + +# 𥛅 𥛅 + (‎ 𥛅 ‎) 256C5 CJK UNIFIED IDEOGRAPH-256C5 +← (‎ 𥛅 ‎) 2F955 CJK COMPATIBILITY IDEOGRAPH-2F955 + +# 𥥼 𥥼 + (‎ 𥥼 ‎) 2597C CJK UNIFIED IDEOGRAPH-2597C +← (‎ 𥥼 ‎) 2F95C CJK COMPATIBILITY IDEOGRAPH-2F95C + +# 𥪧 𥪧 𥪧 + (‎ 𥪧 ‎) 25AA7 CJK UNIFIED IDEOGRAPH-25AA7 +← (‎ 𥪧 ‎) 2F95D CJK COMPATIBILITY IDEOGRAPH-2F95D +← (‎ 𥪧 ‎) 2F95E CJK COMPATIBILITY IDEOGRAPH-2F95E + +# 𥮫 𥮫 + (‎ 𥮫 ‎) 25BAB CJK UNIFIED IDEOGRAPH-25BAB +← (‎ 𥮫 ‎) 2F961 CJK COMPATIBILITY IDEOGRAPH-2F961 + +# 𥲀 𥲀 + (‎ 𥲀 ‎) 25C80 CJK UNIFIED IDEOGRAPH-25C80 +← (‎ 𥲀 ‎) 2F965 CJK COMPATIBILITY IDEOGRAPH-2F965 + +# 𥾆 𥾆 + (‎ 𥾆 ‎) 25F86 CJK UNIFIED IDEOGRAPH-25F86 +← (‎ 𥾆 ‎) 2F96B CJK COMPATIBILITY IDEOGRAPH-2F96B + +# 𦇚 𦇚 + (‎ 𦇚 ‎) 261DA CJK UNIFIED IDEOGRAPH-261DA +← (‎ 𦇚 ‎) 2F898 CJK COMPATIBILITY IDEOGRAPH-2F898 + +# 𦈨 𦈨 + (‎ 𦈨 ‎) 26228 CJK UNIFIED IDEOGRAPH-26228 +← (‎ 𦈨 ‎) 2F972 CJK COMPATIBILITY IDEOGRAPH-2F972 + +# 𦉇 𦉇 + (‎ 𦉇 ‎) 26247 CJK UNIFIED IDEOGRAPH-26247 +← (‎ 𦉇 ‎) 2F973 CJK COMPATIBILITY IDEOGRAPH-2F973 + +# 𦋙 𦋙 + (‎ 𦋙 ‎) 262D9 CJK UNIFIED IDEOGRAPH-262D9 +← (‎ 𦋙 ‎) 2F975 CJK COMPATIBILITY IDEOGRAPH-2F975 + +# 𦌾 𦌾 + (‎ 𦌾 ‎) 2633E CJK UNIFIED IDEOGRAPH-2633E +← (‎ 𦌾 ‎) 2F977 CJK COMPATIBILITY IDEOGRAPH-2F977 + +# 𦓚 𦓚 + (‎ 𦓚 ‎) 264DA CJK UNIFIED IDEOGRAPH-264DA +← (‎ 𦓚 ‎) 2F97B CJK COMPATIBILITY IDEOGRAPH-2F97B + +# 𦔣 𦔣 + (‎ 𦔣 ‎) 26523 CJK UNIFIED IDEOGRAPH-26523 +← (‎ 𦔣 ‎) 2F97C CJK COMPATIBILITY IDEOGRAPH-2F97C + +# 𦖨 𦖨 + (‎ 𦖨 ‎) 265A8 CJK UNIFIED IDEOGRAPH-265A8 +← (‎ 𦖨 ‎) 2F97E CJK COMPATIBILITY IDEOGRAPH-2F97E + +# 𦞧 𦞧 + (‎ 𦞧 ‎) 267A7 CJK UNIFIED IDEOGRAPH-267A7 +← (‎ 𦞧 ‎) 2F987 CJK COMPATIBILITY IDEOGRAPH-2F987 + +# 𦞵 𦞵 + (‎ 𦞵 ‎) 267B5 CJK UNIFIED IDEOGRAPH-267B5 +← (‎ 𦞵 ‎) 2F988 CJK COMPATIBILITY IDEOGRAPH-2F988 + +# 𦬼 𦬼 + (‎ 𦬼 ‎) 26B3C CJK UNIFIED IDEOGRAPH-26B3C +← (‎ 𦬼 ‎) 2F997 CJK COMPATIBILITY IDEOGRAPH-2F997 + +# 𦰶 𦰶 + (‎ 𦰶 ‎) 26C36 CJK UNIFIED IDEOGRAPH-26C36 +← (‎ 𦰶 ‎) 2F9A4 CJK COMPATIBILITY IDEOGRAPH-2F9A4 + +# 𦳕 𦳕 + (‎ 𦳕 ‎) 26CD5 CJK UNIFIED IDEOGRAPH-26CD5 +← (‎ 𦳕 ‎) 2F9A6 CJK COMPATIBILITY IDEOGRAPH-2F9A6 + +# 𦵫 𦵫 + (‎ 𦵫 ‎) 26D6B CJK UNIFIED IDEOGRAPH-26D6B +← (‎ 𦵫 ‎) 2F9A5 CJK COMPATIBILITY IDEOGRAPH-2F9A5 + +# 𦼬 𦼬 + (‎ 𦼬 ‎) 26F2C CJK UNIFIED IDEOGRAPH-26F2C +← (‎ 𦼬 ‎) 2F9AD CJK COMPATIBILITY IDEOGRAPH-2F9AD + +# 𦾱 𦾱 + (‎ 𦾱 ‎) 26FB1 CJK UNIFIED IDEOGRAPH-26FB1 +← (‎ 𦾱 ‎) 2F9B0 CJK COMPATIBILITY IDEOGRAPH-2F9B0 + +# 𧃒 𧃒 + (‎ 𧃒 ‎) 270D2 CJK UNIFIED IDEOGRAPH-270D2 +← (‎ 𧃒 ‎) 2F9B1 CJK COMPATIBILITY IDEOGRAPH-2F9B1 + +# 𧏊 𧏊 + (‎ 𧏊 ‎) 273CA CJK UNIFIED IDEOGRAPH-273CA +← (‎ 𧏊 ‎) 2F9AB CJK COMPATIBILITY IDEOGRAPH-2F9AB + +# 𧙧 𧙧 + (‎ 𧙧 ‎) 27667 CJK UNIFIED IDEOGRAPH-27667 +← (‎ 𧙧 ‎) 2F9C5 CJK COMPATIBILITY IDEOGRAPH-2F9C5 + +# 𧢮 𧢮 + (‎ 𧢮 ‎) 278AE CJK UNIFIED IDEOGRAPH-278AE +← (‎ 𧢮 ‎) 2F9CB CJK COMPATIBILITY IDEOGRAPH-2F9CB + +# 𧥦 𧥦 + (‎ 𧥦 ‎) 27966 CJK UNIFIED IDEOGRAPH-27966 +← (‎ 𧥦 ‎) 2F9CC CJK COMPATIBILITY IDEOGRAPH-2F9CC + +# 𧲨 𧲨 + (‎ 𧲨 ‎) 27CA8 CJK UNIFIED IDEOGRAPH-27CA8 +← (‎ 𧲨 ‎) 2F9D3 CJK COMPATIBILITY IDEOGRAPH-2F9D3 + +# 𧼯 𧼯 + (‎ 𧼯 ‎) 27F2F CJK UNIFIED IDEOGRAPH-27F2F +← (‎ 𧼯 ‎) 2F9D8 CJK COMPATIBILITY IDEOGRAPH-2F9D8 + +# 𨗒 𨗒 + (‎ 𨗒 ‎) 285D2 CJK UNIFIED IDEOGRAPH-285D2 +← (‎ 𨗒 ‎) 2F9E0 CJK COMPATIBILITY IDEOGRAPH-2F9E0 + +# 𨗭 𨗭 + (‎ 𨗭 ‎) 285ED CJK UNIFIED IDEOGRAPH-285ED +← (‎ 𨗭 ‎) 2F9E1 CJK COMPATIBILITY IDEOGRAPH-2F9E1 + +# 𨜮 𨜮 + (‎ 𨜮 ‎) 2872E CJK UNIFIED IDEOGRAPH-2872E +← (‎ 𨜮 ‎) 2F9E5 CJK COMPATIBILITY IDEOGRAPH-2F9E5 + +# 𨯺 𨯺 + (‎ 𨯺 ‎) 28BFA CJK UNIFIED IDEOGRAPH-28BFA +← (‎ 𨯺 ‎) 2F9ED CJK COMPATIBILITY IDEOGRAPH-2F9ED + +# 𨵷 𨵷 + (‎ 𨵷 ‎) 28D77 CJK UNIFIED IDEOGRAPH-28D77 +← (‎ 𨵷 ‎) 2F9F1 CJK COMPATIBILITY IDEOGRAPH-2F9F1 + +# 𩅅 𩅅 + (‎ 𩅅 ‎) 29145 CJK UNIFIED IDEOGRAPH-29145 +← (‎ 𩅅 ‎) 2F9F6 CJK COMPATIBILITY IDEOGRAPH-2F9F6 + +# 𩇟 𩇟 + (‎ 𩇟 ‎) 291DF CJK UNIFIED IDEOGRAPH-291DF +← (‎ 𩇟 ‎) 2F81C CJK COMPATIBILITY IDEOGRAPH-2F81C + +# 𩈚 𩈚 + (‎ 𩈚 ‎) 2921A CJK UNIFIED IDEOGRAPH-2921A +← (‎ 𩈚 ‎) 2F9F7 CJK COMPATIBILITY IDEOGRAPH-2F9F7 + +# 𩐊 𩐊 + (‎ 𩐊 ‎) 2940A CJK UNIFIED IDEOGRAPH-2940A +← (‎ 𩐊 ‎) 2F9FB CJK COMPATIBILITY IDEOGRAPH-2F9FB + +# 𩒖 𩒖 + (‎ 𩒖 ‎) 29496 CJK UNIFIED IDEOGRAPH-29496 +← (‎ 𩒖 ‎) 2F9FD CJK COMPATIBILITY IDEOGRAPH-2F9FD + +# 𩖶 𩖶 + (‎ 𩖶 ‎) 295B6 CJK UNIFIED IDEOGRAPH-295B6 +← (‎ 𩖶 ‎) 2FA01 CJK COMPATIBILITY IDEOGRAPH-2FA01 + +# 𩬰 𩬰 + (‎ 𩬰 ‎) 29B30 CJK UNIFIED IDEOGRAPH-29B30 +← (‎ 𩬰 ‎) 2FA09 CJK COMPATIBILITY IDEOGRAPH-2FA09 + +# 𪃎 𪃎 + (‎ 𪃎 ‎) 2A0CE CJK UNIFIED IDEOGRAPH-2A0CE +← (‎ 𪃎 ‎) 2FA10 CJK COMPATIBILITY IDEOGRAPH-2FA10 + +# 𪄅 𪄅 + (‎ 𪄅 ‎) 2A105 CJK UNIFIED IDEOGRAPH-2A105 +← (‎ 𪄅 ‎) 2FA12 CJK COMPATIBILITY IDEOGRAPH-2FA12 + +# 𪈎 𪈎 + (‎ 𪈎 ‎) 2A20E CJK UNIFIED IDEOGRAPH-2A20E +← (‎ 𪈎 ‎) 2FA13 CJK COMPATIBILITY IDEOGRAPH-2FA13 + +# 𪊑 𪊑 + (‎ 𪊑 ‎) 2A291 CJK UNIFIED IDEOGRAPH-2A291 +← (‎ 𪊑 ‎) 2FA14 CJK COMPATIBILITY IDEOGRAPH-2FA14 + +# 𪎒 𪎒 + (‎ 𪎒 ‎) 2A392 CJK UNIFIED IDEOGRAPH-2A392 +← (‎ 𪎒 ‎) 2F88F CJK COMPATIBILITY IDEOGRAPH-2F88F + +# 𪘀 𪘀 + (‎ 𪘀 ‎) 2A600 CJK UNIFIED IDEOGRAPH-2A600 +← (‎ 𪘀 ‎) 2FA1D CJK COMPATIBILITY IDEOGRAPH-2FA1D + +# total : 7248 + diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/AbbreviationDisambiguationHelperTest.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/AbbreviationDisambiguationHelperTest.java new file mode 100644 index 000000000..cc54f05d5 --- /dev/null +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/AbbreviationDisambiguationHelperTest.java @@ -0,0 +1,95 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; + +class AbbreviationDisambiguationHelperTest { + private final String dbJSON = """ + { + "abbreviation": "DB", + "meanings": [ + "Database", + "Decibel" + ] + }"""; + + @Test + void get() { + var abbreviation = AbbreviationDisambiguationHelper.disambiguate("DB"); + assertNotNull(abbreviation); + assertEquals(abbreviation.size(), Math.min(AbbreviationDisambiguationHelper.LIMIT, 3)); + } + + @Test + void deserialize() throws JsonProcessingException { + var abbr = createObjectMapper().readValue(dbJSON, Disambiguation.class); + assertEquals("DB", abbr.getAbbreviation()); + assertTrue(abbr.getMeanings().containsAll(List.of("Database", "Decibel"))); + assertEquals(2, abbr.getMeanings().size()); + } + + @Test + void load() { + var abbreviations = AbbreviationDisambiguationHelper.getInstance().getOrRead(); + assertNotNull(abbreviations); + } + + @Test + void crawlAbbreviationsCom() { + var meanings = AbbreviationDisambiguationHelper.crawlAbbreviationsCom("DB"); + assertNotNull(meanings); + assertEquals(AbbreviationDisambiguationHelper.LIMIT, meanings.size()); + } + + @Test + void crawlAcronymFinderCom() { + var meanings = AbbreviationDisambiguationHelper.crawlAcronymFinderCom("GAE"); + assertNotNull(meanings); + assertEquals(AbbreviationDisambiguationHelper.LIMIT, meanings.size()); + } + + @Test + void crawl() { + //Let's hope no one ever comes up with a sensible meaning for this non-sense. Until then, it + // will remain as our way to test how we handle no search results. + var nonSens = AbbreviationDisambiguationHelper.crawl("8DAS8UDZGU23HG1U"); + assertNotNull(nonSens); + assertTrue(nonSens.getMeanings().isEmpty()); + } + + @Test + void inOrderTest() { + assertEquals(3, AbbreviationDisambiguationHelper.containsInOrder("some text", "stt")); + assertTrue(AbbreviationDisambiguationHelper.containsAllInOrder("some text", "stt")); + assertEquals(4, AbbreviationDisambiguationHelper.containsInOrder("some text", "smex")); + assertTrue(AbbreviationDisambiguationHelper.containsAllInOrder("some text", "smex")); + assertEquals(2, AbbreviationDisambiguationHelper.containsInOrder("some text", "semx")); + assertFalse(AbbreviationDisambiguationHelper.containsAllInOrder("some text", "semx")); + } + + @Test + void shareInitialTest() { + assertTrue(AbbreviationDisambiguationHelper.shareInitial("ahjkdsshds ousidh bndwb", "adfgbodiowdiowhi sdhsdshg")); + assertFalse(AbbreviationDisambiguationHelper.shareInitial(null, "sth")); + assertFalse(AbbreviationDisambiguationHelper.shareInitial("sth", null)); + assertFalse(AbbreviationDisambiguationHelper.shareInitial(null, null)); + assertFalse(AbbreviationDisambiguationHelper.shareInitial("abcde", "bcdea")); + } + + @Test + void maximumAbbreviationScoreTest() { + assertEquals(1.5, AbbreviationDisambiguationHelper.maximumAbbreviationScore("abcd", "a", 1, 0.5, 0, 0)); + assertEquals(1.5, AbbreviationDisambiguationHelper.maximumAbbreviationScore("abad", "a", 1, 0.5, 0, 0)); + assertEquals(2, AbbreviationDisambiguationHelper.maximumAbbreviationScore("abcd", "ac", 1, 0.5, 0, 0)); + assertEquals(3.5, AbbreviationDisambiguationHelper.maximumAbbreviationScore("abcd cd", "acd", 1, 0.5, 0, 0)); + } +} diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/DbPediaHelperTest.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/DbPediaHelperTest.java new file mode 100644 index 000000000..71bc586e0 --- /dev/null +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/DbPediaHelperTest.java @@ -0,0 +1,35 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class DbPediaHelperTest { + @Test + void load() { + var record = DbPediaHelper.getInstance().getOrRead(); + assertFalse(record.programmingLanguages().isEmpty()); + assertFalse(record.markupLanguages().isEmpty()); + assertFalse(record.software().isEmpty()); + } + + @Test + void containsAtLeastSomePopularLanguages() { + var record = DbPediaHelper.getInstance().getOrRead(); + List all = record.programmingLanguages(); + all.addAll(record.markupLanguages()); + all.addAll(record.software()); + all = all.stream().map(String::toLowerCase).toList(); + assertTrue(all.contains("python")); + assertTrue(all.contains("javascript")); + assertTrue(all.contains("java")); + assertTrue(all.contains("c")); + assertTrue(all.contains("c++")); + assertTrue(all.contains("html")); + assertTrue(all.contains("css")); + } +} diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ConfusablesHelperTest.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ConfusablesHelperTest.java new file mode 100644 index 000000000..3f303553f --- /dev/null +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/ConfusablesHelperTest.java @@ -0,0 +1,36 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +public class ConfusablesHelperTest { + public static final String example = """ + # ! ǃ ⵑ ! + """; + public static final List homoglyphsExample = Stream.of("!", "ǃ", "ⵑ", "!").map(UnicodeCharacter::valueOf).toList(); + + @Test + void extractHomoglyphsFromLine() { + var homoglyphs = ConfusablesHelper.extractHomoglyphsFromLine(example); + assertEquals(homoglyphsExample.size(), homoglyphs.size()); + assertTrue(homoglyphs.containsAll(homoglyphsExample)); + } + + @Test + void getHomoglyphs() { + var homoglyphs = ConfusablesHelper.getHomoglyphs(homoglyphsExample.get(0)); + assertTrue(homoglyphs.size() >= homoglyphsExample.size()); + assertTrue(homoglyphs.containsAll(homoglyphsExample)); + } + + @Test + void areHomoglyphs() { + assertTrue(ConfusablesHelper.areHomoglyphs(UnicodeCharacter.valueOf("!"), UnicodeCharacter.valueOf("!"))); + } +} diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtilsTest.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtilsTest.java new file mode 100644 index 000000000..09394192c --- /dev/null +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/WordSimUtilsTest.java @@ -0,0 +1,18 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class WordSimUtilsTest { + + @Test + void getSimilarity() { + var wordSimUtils = new WordSimUtils(); + assertEquals(1, wordSimUtils.getSimilarity("", "")); + assertEquals(1, wordSimUtils.getSimilarity("lorem", "lorem")); + assertEquals(1, wordSimUtils.getSimilarity("lorem ipsum", "lorem ipsum")); + assertEquals(1, wordSimUtils.getSimilarity("lOrEm IpSuM", "lorem ipsum", true)); + } +} diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasureTest.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasureTest.java new file mode 100644 index 000000000..7b7c4483e --- /dev/null +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/jarowinkler/JaroWinklerMeasureTest.java @@ -0,0 +1,51 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.jarowinkler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions; + +class JaroWinklerMeasureTest { + private static final double delta = 0.01; + + /** + * These tests were extracted from the original {@link org.apache.commons.text.similarity.JaroWinklerSimilarity} implementation and should + * still hold true. + */ + @Test + void testSimilarityDirectly() { + String s = null; + assertThrows(IllegalArgumentException.class, () -> UnicodeJaroWinklerSimilarity.apply(s, s, UnicodeCharacterMatchFunctions.EQUAL)); + assertThrows(IllegalArgumentException.class, () -> UnicodeJaroWinklerSimilarity.apply("foo", null, UnicodeCharacterMatchFunctions.EQUAL)); + assertThrows(IllegalArgumentException.class, () -> UnicodeJaroWinklerSimilarity.apply(null, "foo", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(1.0, UnicodeJaroWinklerSimilarity.apply("", "", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(1.0, UnicodeJaroWinklerSimilarity.apply("foo", "foo", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.94, UnicodeJaroWinklerSimilarity.apply("foo", "foo ", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.91, UnicodeJaroWinklerSimilarity.apply("foo", "foo ", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.87, UnicodeJaroWinklerSimilarity.apply("foo", " foo ", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.51, UnicodeJaroWinklerSimilarity.apply("foo", " foo", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.0, UnicodeJaroWinklerSimilarity.apply("", "a", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.0, UnicodeJaroWinklerSimilarity.apply("aaapppp", "", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.93, UnicodeJaroWinklerSimilarity.apply("frog", "fog", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.0, UnicodeJaroWinklerSimilarity.apply("fly", "ant", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(0.44, UnicodeJaroWinklerSimilarity.apply("elephant", "hippo", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.44, UnicodeJaroWinklerSimilarity.apply("hippo", "elephant", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.0, UnicodeJaroWinklerSimilarity.apply("hippo", "zzzzzzzz", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.88, UnicodeJaroWinklerSimilarity.apply("hello", "hallo", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.91, UnicodeJaroWinklerSimilarity.apply("ABC Corporation", "ABC Corp", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.95, UnicodeJaroWinklerSimilarity.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.", UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.94, UnicodeJaroWinklerSimilarity.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness", + UnicodeCharacterMatchFunctions.EQUAL), delta); + assertEquals(0.89, UnicodeJaroWinklerSimilarity.apply("PENNSYLVANIA", "PENNCISYLVNIA", UnicodeCharacterMatchFunctions.EQUAL), delta); + } + + @Test + void testHomoglyphSimilarity() { + var measure = new JaroWinklerMeasure(); + assertEquals(1d, measure.getSimilarity(new ComparisonContext("ℜ𝘂ᖯʏ", "Ruby", UnicodeCharacterMatchFunctions.EQUAL_OR_HOMOGLYPH)), delta); + } +} diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasureTest.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasureTest.java new file mode 100644 index 000000000..3e5fac546 --- /dev/null +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/common/util/wordsim/measures/levenshtein/LevenshteinMeasureTest.java @@ -0,0 +1,39 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.measures.levenshtein; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.ComparisonContext; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions; + +class LevenshteinMeasureTest { + private static final double delta = 0.01; + + /** + * These tests were extracted from the original {@link org.apache.commons.text.similarity.LevenshteinDistance} implementation and should still hold true. + */ + @Test + void testSimilarityDirectly() { + var distance = new UnicodeLevenshteinDistance(); + assertThrows(IllegalArgumentException.class, () -> distance.apply(null, "foo", UnicodeCharacterMatchFunctions.EQUAL)); + assertThrows(IllegalArgumentException.class, () -> distance.apply("foo", null, UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(0, distance.apply("", "", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(1, distance.apply("", "a", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(7, distance.apply("aaapppp", "", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(1, distance.apply("frog", "fog", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(3, distance.apply("fly", "ant", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(7, distance.apply("elephant", "hippo", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(7, distance.apply("hippo", "elephant", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(8, distance.apply("hippo", "zzzzzzzz", UnicodeCharacterMatchFunctions.EQUAL)); + assertEquals(1, distance.apply("hello", "hallo", UnicodeCharacterMatchFunctions.EQUAL)); + } + + @Test + void testHomoglyphSimilarity() { + var measure = new LevenshteinMeasure(); + assertEquals(1d, measure.getSimilarity(new ComparisonContext("ℜ𝘂ᖯʏ", "Ruby", UnicodeCharacterMatchFunctions.EQUAL_OR_HOMOGLYPH)), delta); + } +} diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepOne.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepOne.java index b4b86005c..51d87dd22 100644 --- a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepOne.java +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepOne.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline.impl; import java.util.Arrays; @@ -31,7 +31,7 @@ private void fetchData() { } @Override - public void run() { + public void process() { fetchData(); logger.info("Greetings from {} with id {}", this.getClass().getSimpleName(), getId()); var text = textData.getText(); @@ -40,6 +40,16 @@ public void run() { textData.setTokens(tokens); } + @Override + protected void before() { + //Nothing + } + + @Override + protected void after() { + //Nothing + } + @Override protected void delegateApplyConfigurationToInternalObjects(SortedMap additionalConfiguration) { // NOP diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoOne.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoOne.java index fddb318bd..dd2ca1920 100644 --- a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoOne.java +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoOne.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline.impl; import java.util.ArrayList; @@ -39,7 +39,7 @@ private void fetchAndInitializeData() { } @Override - public void run() { + public void process() { fetchAndInitializeData(); logger.info("Greetings from {} with id {}", this.getClass().getSimpleName(), getId()); List tokens = getTokens(); @@ -58,6 +58,16 @@ public void run() { } + @Override + protected void before() { + //Nothing + } + + @Override + protected void after() { + //Nothing + } + private List getTokens() { var tokens = processedTextData.getImportantTokens(); if (tokens == null || tokens.isEmpty()) { diff --git a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoTwo.java b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoTwo.java index be379b002..166ff29d1 100644 --- a/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoTwo.java +++ b/framework/common/src/test/java/edu/kit/kastel/mcse/ardoco/core/pipeline/impl/ConcretePipelineStepTwoTwo.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.pipeline.impl; import java.util.SortedMap; @@ -34,7 +34,7 @@ private void fetchAndInitializeData() { } @Override - public void run() { + public void process() { fetchAndInitializeData(); logger.info("Greetings from {} with id {}", this.getClass().getSimpleName(), getId()); var tokens = processedTextData.getImportantTokens(); @@ -43,6 +43,16 @@ public void run() { resultData.setResult(firstEntry); } + @Override + protected void before() { + //Nothing + } + + @Override + protected void after() { + //Nothing + } + @Override protected void delegateApplyConfigurationToInternalObjects(SortedMap additionalConfiguration) { // NOP diff --git a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/DtoToObjectConverter.java b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/DtoToObjectConverter.java index e031eb212..1f5c0afa3 100644 --- a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/DtoToObjectConverter.java +++ b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/DtoToObjectConverter.java @@ -1,14 +1,20 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.converter; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; -import edu.kit.kastel.mcse.ardoco.core.api.text.*; +import edu.kit.kastel.mcse.ardoco.core.api.text.POSTag; +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.PhraseType; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.api.text.Text; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.dto.IncomingDependencyDto; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.dto.OutgoingDependencyDto; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.dto.SentenceDto; @@ -60,7 +66,7 @@ private Sentence convertToSentence(SentenceDto sentenceDTO, Text parentText) thr String constituencyTree = sentenceDTO.getConstituencyTree(); SentenceImpl sentence = new SentenceImpl((int) sentenceDTO.getSentenceNo() - 1, sentenceDTO.getText(), Lists.immutable.ofAll(words)); Phrase phrases = parseConstituencyTree(constituencyTree, new ArrayList<>(words)); - sentence.setPhrases(Lists.immutable.of(phrases)); + sentence.setPhrases(Lists.mutable.of(phrases)); return sentence; } @@ -92,11 +98,20 @@ public Phrase parseConstituencyTree(String constituencyTree, List wordsOfS } private boolean isValidConstituencyTree(String constituencyTree) { - return constituencyTree.length() >= 2 && constituencyTree.charAt(0) == CONSTITUENCY_TREE_OPEN_BRACKET && constituencyTree.charAt(constituencyTree - .length() - 1) == CONSTITUENCY_TREE_CLOSE_BRACKET && constituencyTree.chars() - .filter(ch -> ch == CONSTITUENCY_TREE_OPEN_BRACKET) - .count() == constituencyTree.chars().filter(ch -> ch == CONSTITUENCY_TREE_CLOSE_BRACKET).count() && constituencyTree.split( - CONSTITUENCY_TREE_SEPARATOR, 2).length > 1; + var condLength = constituencyTree.length() >= 2; + if (!condLength) + return false; + var condStartWithOpenBracket = constituencyTree.charAt(0) == CONSTITUENCY_TREE_OPEN_BRACKET; + if (!condStartWithOpenBracket) + return false; + var condEndWithClosedBracket = constituencyTree.charAt(constituencyTree.length() - 1) == CONSTITUENCY_TREE_CLOSE_BRACKET; + if (!condEndWithClosedBracket) + return false; + + var condBalancedBrackets = getTreeOpenBrackets(constituencyTree) == getTreeCloseBrackets(constituencyTree); + if (!condBalancedBrackets) + return false; + return constituencyTree.split(CONSTITUENCY_TREE_SEPARATOR, 2).length > 1; } private List getSubtrees(String treeWithoutType) { @@ -105,10 +120,8 @@ private List getSubtrees(String treeWithoutType) { while (!treeWithoutType.isEmpty()) { // find next subtree int index = 1; - while (treeWithoutType.substring(0, index).chars().filter(ch -> ch == CONSTITUENCY_TREE_OPEN_BRACKET).count() != treeWithoutType.substring(0, index) - .chars() - .filter(ch -> ch == CONSTITUENCY_TREE_CLOSE_BRACKET) - .count()) { + while (!treeWithoutType.substring(0, index).endsWith(String.valueOf(CONSTITUENCY_TREE_CLOSE_BRACKET)) || getTreeOpenBrackets(treeWithoutType + .substring(0, index)) != getTreeCloseBrackets(treeWithoutType.substring(0, index))) { index++; } // number of '(' and ')' is equal -> new subphrase tree found @@ -122,8 +135,16 @@ private List getSubtrees(String treeWithoutType) { return subTrees; } + private long getTreeOpenBrackets(String tree) { + return tree.chars().filter(ch -> ch == CONSTITUENCY_TREE_OPEN_BRACKET).count() - StringUtils.countMatches(tree, POSTag.LEFT_PAREN.getTag()); + } + + private long getTreeCloseBrackets(String tree) { + return tree.chars().filter(ch -> ch == CONSTITUENCY_TREE_CLOSE_BRACKET).count() - StringUtils.countMatches(tree, POSTag.RIGHT_PAREN.getTag()); + } + private boolean isWord(String tree) { - return tree.chars().filter(character -> character == CONSTITUENCY_TREE_OPEN_BRACKET).count() == 1; + return getTreeOpenBrackets(tree) == 1; } private Word convertToWord(WordDto wordDTO, Text parent) { diff --git a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/JsonConverter.java b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/JsonConverter.java index b35d1ee97..5e5c8cd30 100644 --- a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/JsonConverter.java +++ b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/JsonConverter.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.converter; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -40,7 +42,7 @@ private JsonConverter() { * @return whether the json string matches the text schema */ public static boolean validateJson(String json) throws IOException { - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = createObjectMapper(); JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); InputStream inputSchema = JsonConverter.class.getClassLoader().getResourceAsStream(SCHEMA_PATH); @@ -69,7 +71,7 @@ public static TextDto fromJsonString(String json) throws IOException, InvalidJso if (!validateJson(json)) { throw new InvalidJsonException("The json string is no valid text DTO."); } - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = createObjectMapper(); return objectMapper.readValue(json, TextDto.class); } @@ -80,7 +82,7 @@ public static TextDto fromJsonString(String json) throws IOException, InvalidJso * @return the JSON string or null */ public static String toJsonString(TextDto obj) throws IOException, InvalidJsonException { - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = createObjectMapper(); String jsonString = objectMapper.writeValueAsString(obj); if (!validateJson(jsonString)) { throw new InvalidJsonException("The text DTO could not be converted into a json string. No valid text Dto"); diff --git a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/DependencyImpl.java b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/DependencyImpl.java index f1bcd8a61..7de835a38 100644 --- a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/DependencyImpl.java +++ b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/DependencyImpl.java @@ -1,11 +1,12 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.textobject; +import java.io.Serializable; import java.util.Objects; import edu.kit.kastel.mcse.ardoco.core.api.text.DependencyTag; -public class DependencyImpl { +public class DependencyImpl implements Serializable { private final DependencyTag dependencyType; private final long wordId; diff --git a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/PhraseImpl.java b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/PhraseImpl.java index e6b7f2a12..66709f3d1 100644 --- a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/PhraseImpl.java +++ b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/PhraseImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.textobject; import java.util.ArrayList; @@ -23,19 +23,19 @@ public class PhraseImpl implements Phrase { private static final String PUNCTUATION_WITH_SPACE = "\\s+([.,;:?!])"; private static final String BRACKETS_WITH_SPACE = "\\s+([()\\[\\]{}<>])"; private final PhraseType type; - private final ImmutableList childPhrases; - private final ImmutableList nonPhraseWords; - private ImmutableList phraseWords; - private ImmutableList containedWords; - private ImmutableList subPhrases; - private ImmutableSortedMap phraseVector; + private MutableList childPhrases; + private MutableList nonPhraseWords; + private MutableList phraseWords; + private MutableList containedWords; + private MutableList subPhrases; + private MutableSortedMap phraseVector; private int sentenceNo = -1; private String text; public PhraseImpl(ImmutableList nonPhraseWords, PhraseType type, List childPhrases) { - this.nonPhraseWords = nonPhraseWords == null ? Lists.immutable.empty() : nonPhraseWords; + this.nonPhraseWords = nonPhraseWords == null ? Lists.mutable.empty() : nonPhraseWords.toList(); this.type = type; - this.childPhrases = Lists.immutable.ofAll(childPhrases); + this.childPhrases = Lists.mutable.ofAll(childPhrases); } @Override @@ -71,28 +71,26 @@ public synchronized ImmutableList getContainedWords() { for (Phrase subphrase : childPhrases) { collectedWords.addAll(subphrase.getContainedWords().castToList()); } - this.phraseWords = Lists.immutable.ofAll(collectedWords); + this.phraseWords = Lists.mutable.ofAll(collectedWords); } - MutableList words = Lists.mutable.ofAll(nonPhraseWords); - words.addAllIterable(phraseWords); - words.sortThis(Comparator.comparingInt(Word::getPosition)); - containedWords = words.toImmutable(); + containedWords = Lists.mutable.ofAll(nonPhraseWords); + containedWords.addAllIterable(phraseWords); + containedWords.sortThis(Comparator.comparingInt(Word::getPosition)); } - return containedWords; + return containedWords.toImmutable(); } @Override public synchronized ImmutableList getSubPhrases() { if (subPhrases == null) { - MutableList tempSubPhrases = Lists.mutable.ofAll(childPhrases); + subPhrases = Lists.mutable.ofAll(childPhrases); for (Phrase childPhrase : childPhrases) { - tempSubPhrases.addAll(childPhrase.getSubPhrases().toList()); + subPhrases.addAll(childPhrase.getSubPhrases().toList()); } - subPhrases = tempSubPhrases.toImmutable(); } - return subPhrases; + return subPhrases.toImmutable(); } @Override @@ -129,13 +127,12 @@ public boolean isSubPhraseOf(Phrase other) { @Override public synchronized ImmutableSortedMap getPhraseVector() { - if (this.phraseVector == null) { - MutableSortedMap tempPhraseVector = SortedMaps.mutable.empty(); + if (phraseVector == null) { + phraseVector = SortedMaps.mutable.empty(); var grouped = getContainedWords().groupBy(Word::getText).toMap(); - grouped.forEach((key, value) -> tempPhraseVector.put(value.getAny(), value.size())); - this.phraseVector = tempPhraseVector.toImmutable(); + grouped.forEach((key, value) -> phraseVector.put(value.getAny(), value.size())); } - return this.phraseVector; + return phraseVector.toImmutable(); } @Override @@ -157,4 +154,13 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(getContainedWords(), getText(), type, childPhrases); } + + @Override + public int compareTo(Phrase o) { + return Comparator.comparing(Phrase::getSentenceNo) + .thenComparing(Phrase::getText) + .thenComparing(Phrase::getPhraseType) + .thenComparingInt(p -> p.getContainedWords().get(0).getPosition()) + .compare(this, o); + } } diff --git a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/SentenceImpl.java b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/SentenceImpl.java index 84f93e656..25cdb5ca5 100644 --- a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/SentenceImpl.java +++ b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/SentenceImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.textobject; import java.util.ArrayList; @@ -7,6 +7,7 @@ import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.list.MutableList; import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; @@ -14,8 +15,8 @@ public class SentenceImpl implements Sentence { - private final ImmutableList words; - private ImmutableList phrases = Lists.immutable.empty(); + private final MutableList words; + private MutableList phrases = Lists.mutable.empty(); private final int sentenceNumber; @@ -24,10 +25,10 @@ public class SentenceImpl implements Sentence { public SentenceImpl(int sentenceNumber, String text, ImmutableList words) { this.sentenceNumber = sentenceNumber; this.text = text; - this.words = words; + this.words = words == null ? Lists.mutable.empty() : words.toList(); } - public void setPhrases(ImmutableList phrases) { + public void setPhrases(MutableList phrases) { this.phrases = phrases; } @@ -38,7 +39,7 @@ public int getSentenceNumber() { @Override public ImmutableList getWords() { - return words; + return words.toImmutable(); } @Override @@ -69,4 +70,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(words, phrases, sentenceNumber, text); } + + @Override + public void addPhrase(Phrase phrase) { + phrases.add(phrase); + } } diff --git a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/TextImpl.java b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/TextImpl.java index e849717f3..422edbbad 100644 --- a/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/TextImpl.java +++ b/framework/text-provider-json/src/main/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/textobject/TextImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.textobject; import java.util.Objects; @@ -14,20 +14,20 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Word; public class TextImpl implements Text { - private ImmutableList sentences; + private MutableList sentences; - private ImmutableList words; + private MutableList words; private final SortedMap wordsIndex = new TreeMap<>(); private int length = -1; public TextImpl() { - sentences = Lists.immutable.empty(); - words = Lists.immutable.empty(); + sentences = Lists.mutable.empty(); + words = Lists.mutable.empty(); } public void setSentences(ImmutableList sentences) { - this.sentences = sentences; + this.sentences = sentences.toList(); } @Override @@ -45,14 +45,14 @@ public synchronized int getLength() { @Override public ImmutableList words() { if (words.isEmpty()) { - words = collectWords(); + words = collectWords().toList(); int index = 0; for (Word word : words) { wordsIndex.put(index, word); index++; } } - return words; + return words.toImmutable(); } @Override @@ -65,7 +65,7 @@ public synchronized Word getWord(int index) { @Override public ImmutableList getSentences() { - return sentences; + return sentences.toImmutable(); } private ImmutableList collectWords() { diff --git a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/PhraseImplTest.java b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/PhraseImplTest.java index cb77b6bfc..58ad39101 100644 --- a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/PhraseImplTest.java +++ b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/PhraseImplTest.java @@ -1,8 +1,6 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson; -import java.io.IOException; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -10,8 +8,8 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; import edu.kit.kastel.mcse.ardoco.core.api.text.Text; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.converter.DtoToObjectConverter; -import edu.kit.kastel.mcse.ardoco.core.textproviderjson.error.NotConvertableException; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.textobject.PhraseImpl; class PhraseImplTest { @@ -27,13 +25,9 @@ static void initAll() { } @BeforeEach - void init() { - try { - Text textImplInstance = CONVERTER.convertText(TestUtil.generateDTOWithMultipleSentences()); - phraseImplInstance = (PhraseImpl) textImplInstance.getSentences().get(1).getPhrases().get(0); - } catch (NotConvertableException | IOException e) { - throw new RuntimeException(e); - } + void init() throws Exception { + Text textImplInstance = CONVERTER.convertText(TestUtil.generateDTOWithMultipleSentences()); + phraseImplInstance = (PhraseImpl) textImplInstance.getSentences().get(1).getPhrases().get(0); } @Test @@ -89,4 +83,9 @@ void simpleHashCodeTest() { Assertions.assertEquals(phraseImplInstance.hashCode(), phraseImplInstance.hashCode()); } + @Test + void serializationTest() { + var serializedCopy = DataRepositoryHelper.deepCopy(phraseImplInstance); + Assertions.assertNotNull(serializedCopy); + } } diff --git a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TestUtil.java b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TestUtil.java index 4e2015f0c..d692fe09b 100644 --- a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TestUtil.java +++ b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TestUtil.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson; import java.io.IOException; @@ -105,7 +105,7 @@ public static Text generateDefaultText() { Phrase phrase1 = new PhraseImpl(Lists.immutable.of(words.get(3)), PhraseType.S, subphrases); List phrases = new ArrayList<>(List.of(phrase1)); Phrase rootPhrase = new PhraseImpl(Lists.immutable.empty(), PhraseType.ROOT, phrases); - sentence1.setPhrases(Lists.immutable.of(rootPhrase)); + sentence1.setPhrases(Lists.mutable.of(rootPhrase)); List sentences = new ArrayList<>(); sentences.add(sentence1); @@ -221,7 +221,7 @@ public static Text generateTextWithMultipleSentences() { Phrase phrase1 = new PhraseImpl(Lists.immutable.of(words.get(3)), PhraseType.S, subphrases); List phrases = new ArrayList<>(List.of(phrase1)); Phrase rootPhrase = new PhraseImpl(Lists.immutable.empty(), PhraseType.ROOT, phrases); - sentence1.setPhrases(Lists.immutable.of(rootPhrase)); + sentence1.setPhrases(Lists.mutable.of(rootPhrase)); sentences.add(sentence1); @@ -239,7 +239,7 @@ public static Text generateTextWithMultipleSentences() { Phrase phrase2 = new PhraseImpl(Lists.immutable.of(words2.get(3)), PhraseType.S, subphrases2); List phrases2 = new ArrayList<>(List.of(phrase2)); Phrase rootPhrase2 = new PhraseImpl(Lists.immutable.empty(), PhraseType.ROOT, phrases2); - sentence2.setPhrases(Lists.immutable.of(rootPhrase2)); + sentence2.setPhrases(Lists.mutable.of(rootPhrase2)); sentences.add(sentence2); @@ -307,7 +307,7 @@ public static Text generateTextWithDependencies() { Phrase phrase1 = new PhraseImpl(Lists.immutable.ofAll(words), PhraseType.INTJ, new ArrayList<>()); List phrases = new ArrayList<>(List.of(phrase1)); Phrase rootPhrase = new PhraseImpl(Lists.immutable.empty(), PhraseType.ROOT, phrases); - sentence1.setPhrases(Lists.immutable.of(rootPhrase)); + sentence1.setPhrases(Lists.mutable.of(rootPhrase)); List sentences = new ArrayList<>(); sentences.add(sentence1); diff --git a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TextImplTest.java b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TextImplTest.java index d3d3ee2eb..1bd951bf1 100644 --- a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TextImplTest.java +++ b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/TextImplTest.java @@ -1,8 +1,6 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson; -import java.io.IOException; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -10,7 +8,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Text; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.converter.DtoToObjectConverter; -import edu.kit.kastel.mcse.ardoco.core.textproviderjson.error.NotConvertableException; import edu.kit.kastel.mcse.ardoco.core.textproviderjson.textobject.TextImpl; class TextImplTest { @@ -24,12 +21,8 @@ static void initAll() { } @BeforeEach - void init() { - try { - textImplInstance = (TextImpl) CONVERTER.convertText(TestUtil.generateDTOWithMultipleSentences()); - } catch (NotConvertableException | IOException e) { - throw new RuntimeException(e); - } + void init() throws Exception { + textImplInstance = (TextImpl) CONVERTER.convertText(TestUtil.generateDTOWithMultipleSentences()); } @Test diff --git a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/ConverterUtilTest.java b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/ConverterUtilTest.java index 15a838693..b7d902e65 100644 --- a/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/ConverterUtilTest.java +++ b/framework/text-provider-json/src/test/java/edu/kit/kastel/mcse/ardoco/core/textproviderjson/converter/ConverterUtilTest.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.textproviderjson.converter; import java.util.ArrayList; @@ -44,7 +44,7 @@ void testGetChildPhrases() { void testGetChildPhrasesSentence() { // no child phrases SentenceImpl sentence1 = new SentenceImpl(0, null, null); - sentence1.setPhrases(Lists.immutable.empty()); + sentence1.setPhrases(Lists.mutable.empty()); List expected1 = new ArrayList<>(); List actual1 = ConverterUtil.getChildPhrases(sentence1); Assertions.assertEquals(expected1, actual1); @@ -53,7 +53,7 @@ void testGetChildPhrasesSentence() { Phrase childPhrase1 = new PhraseImpl(null, null, new ArrayList<>()); Phrase childPhrase2 = new PhraseImpl(null, null, new ArrayList<>()); SentenceImpl sentence2 = new SentenceImpl(0, null, null); - sentence2.setPhrases(Lists.immutable.of(childPhrase1, childPhrase2)); + sentence2.setPhrases(Lists.mutable.of(childPhrase1, childPhrase2)); List expected2 = List.of(childPhrase1, childPhrase2); List actual2 = ConverterUtil.getChildPhrases(sentence2); Assertions.assertEquals(expected2, actual2); @@ -63,7 +63,7 @@ void testGetChildPhrasesSentence() { Phrase childPhrase4 = new PhraseImpl(null, null, new ArrayList<>()); Phrase parentPhrase3 = new PhraseImpl(null, null, List.of(childPhrase3, childPhrase4)); SentenceImpl sentence3 = new SentenceImpl(0, null, null); - sentence3.setPhrases(Lists.immutable.of(parentPhrase3)); + sentence3.setPhrases(Lists.mutable.of(parentPhrase3)); List expected3 = List.of(parentPhrase3); List actual3 = ConverterUtil.getChildPhrases(sentence3); Assertions.assertEquals(expected3, actual3); diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/output/ArDoCoResult.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/output/ArDoCoResult.java index bc1f0fab3..f4d75d694 100644 --- a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/output/ArDoCoResult.java +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/output/ArDoCoResult.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.api.output; import java.util.Comparator; @@ -305,7 +305,7 @@ public List getModelIds() { * Returns the internal {@link LegacyModelExtractionState} for the modelId with the given ID. * * @param modelId the ID of the model - * @return the ModelExtractionState + * @return the LegacyModelExtractionState */ public LegacyModelExtractionState getModelState(String modelId) { ModelStates modelStates = getModelStates(); diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FilePrinter.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FilePrinter.java index 5193989a5..8b0f436e3 100644 --- a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FilePrinter.java +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/common/util/FilePrinter.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.common.util; import static edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper.getConnectionStates; @@ -392,6 +392,7 @@ public static void writeToFile(String filename, String text) { * @param text the text to write */ public static void writeToFile(Path file, String text) { + file.getParent().toFile().mkdirs(); try (BufferedWriter writer = Files.newBufferedWriter(file, UTF_8)) { writer.write(text); } catch (IOException e) { diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCo.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCo.java index 41b314b6e..abc1cc754 100644 --- a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCo.java +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCo.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution; import java.io.File; @@ -56,6 +56,7 @@ public static ArDoCo getInstance(String projectName) { private void initDataRepository() { ProjectPipelineData projectPipelineData = new ProjectPipelineDataImpl(projectName); getDataRepository().addData(ProjectPipelineData.ID, projectPipelineData); + dataRepository.getGlobalConfiguration().setPipeline(this); } @Override diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ConfigurationHelper.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ConfigurationHelper.java index c439f925a..156ab579c 100644 --- a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ConfigurationHelper.java +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ConfigurationHelper.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution; import java.io.File; @@ -96,7 +96,6 @@ protected static void processConfigurationOfClass(Map configs, C fillConfigs(object, fields, configs); } - @SuppressWarnings("java:S3011") private static void fillConfigs(AbstractConfigurable object, List fields, Map configs) throws IllegalAccessException { for (Field f : fields) { f.setAccessible(true); diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/AnonymousRunner.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/AnonymousRunner.java new file mode 100644 index 000000000..03a6e56ac --- /dev/null +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/AnonymousRunner.java @@ -0,0 +1,71 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.execution.runner; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; + +/** + * This class can be used to easily define an anonymous {@link ArDoCoRunner} for testing purposes. + */ +public abstract class AnonymousRunner extends ArDoCoRunner { + private static final Logger logger = LoggerFactory.getLogger(AnonymousRunner.class); + + protected AnonymousRunner(String projectName) { + super(projectName); + setUp(null); + } + + protected AnonymousRunner(String projectName, DataRepository preRunDataRepository) { + super(projectName); + setUp(preRunDataRepository); + } + + /** + * Sets up the runner using {@link #initializePipelineSteps}. Initializes the new data repository using the preRunDataRepository, if present. + * {@link #isSetUp} must return true, if successful. + * + * @param preRunDataRepository data repository of a previous run used as a base + * @return List of AbstractPipelineSteps this runner consists of + */ + private List setUp(@Nullable DataRepository preRunDataRepository) { + try { + var arDoCo = getArDoCo(); + var dataRepository = arDoCo.getDataRepository(); + + if (preRunDataRepository != null) { + dataRepository.addAllData(preRunDataRepository); + } + + var pipelineSteps = initializePipelineSteps(dataRepository); + pipelineSteps.forEach(arDoCo::addPipelineStep); + isSetUp = true; + return pipelineSteps; + } catch (IOException e) { + logger.error("Problem in initialising pipeline when loading data (IOException)", e.getCause()); + isSetUp = false; + return List.of(); + } + } + + /** + * Initializes and returns the pipeline steps according to the supplied parameters + * + * @param dataRepository the data repository of this runner + * @throws IOException can occur when loading data + */ + public abstract List initializePipelineSteps(DataRepository dataRepository) throws IOException; + + @Override + public void setOutputDirectory(File outputDirectory) { + super.setOutputDirectory(outputDirectory); + } +} diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoRunner.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoRunner.java index de8fe8a3c..51269f013 100644 --- a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoRunner.java +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoRunner.java @@ -1,18 +1,23 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution.runner; import java.io.File; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serial; +import java.io.Serializable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; -public abstract class ArDoCoRunner { +public abstract class ArDoCoRunner implements Serializable { private static final Logger logger = LoggerFactory.getLogger(ArDoCoRunner.class); - private final ArDoCo arDoCo; + private final transient ArDoCo arDoCo; private File outputDirectory; protected boolean isSetUp = false; @@ -35,11 +40,31 @@ public final ArDoCoResult run() { } } - protected ArDoCo getArDoCo() { + /** + * {@return the {@link DataRepository} produced by the run} The results are not saved to the output directory. + */ + public final DataRepository runWithoutSaving() { + if (this.isSetUp()) { + this.getArDoCo().run(); + return this.getArDoCo().getDataRepository(); + } else { + logger.error("Cannot run ArDoCo because the runner is not properly set up."); + return null; + } + } + + public ArDoCo getArDoCo() { return this.arDoCo; } protected void setOutputDirectory(File outputDirectory) { this.outputDirectory = outputDirectory; } + + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + if (!getArDoCo().wasExecuted()) + runWithoutSaving(); + out.defaultWriteObject(); + } } diff --git a/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ParameterizedRunner.java b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ParameterizedRunner.java new file mode 100644 index 000000000..042740e16 --- /dev/null +++ b/pipeline/pipeline-core/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ParameterizedRunner.java @@ -0,0 +1,53 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.execution.runner; + +import java.io.IOException; +import java.io.Serializable; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; + +/** + * An {@link ArDoCoRunner} that takes a record of type {@code } to set itself up. + * + * @param a record containing the parameters required to set up the runner + */ +public abstract class ParameterizedRunner extends ArDoCoRunner implements Serializable { + private static final Logger logger = LoggerFactory.getLogger(ParameterizedRunner.class); + + protected ParameterizedRunner(String projectName, T parameters) { + super(projectName); + setUp(parameters); + } + + /** + * Sets up the runner using {@link #initializePipelineSteps}. {@link #isSetUp} must return true, if successful. + * + * @param p Contains the parameters used during setup + * @return List of AbstractPipelineSteps this runner consists of + */ + private List setUp(T p) { + try { + var arDoCo = getArDoCo(); + var pipelineSteps = initializePipelineSteps(p); + pipelineSteps.forEach(arDoCo::addPipelineStep); + isSetUp = true; + return pipelineSteps; + } catch (IOException e) { + logger.error("Problem in initialising pipeline when loading data (IOException)", e.getCause()); + isSetUp = false; + return List.of(); + } + } + + /** + * Initializes and returns the pipeline steps according to the supplied parameters + * + * @param p the supplied parameters + * @throws IOException can occur when loading data + */ + public abstract List initializePipelineSteps(T p) throws IOException; +} diff --git a/pipeline/pipeline-erid/.gitignore b/pipeline/pipeline-erid/.gitignore new file mode 100644 index 000000000..abaf7f22e --- /dev/null +++ b/pipeline/pipeline-erid/.gitignore @@ -0,0 +1,39 @@ +target/ +src/test/resources/testout/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/pipeline/pipeline-erid/pom.xml b/pipeline/pipeline-erid/pom.xml new file mode 100644 index 000000000..97660d5d0 --- /dev/null +++ b/pipeline/pipeline-erid/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + io.github.ardoco.core + pipeline + ${revision} + ../pom.xml + + + pipeline-erid + + + + io.github.ardoco.core + connection-generator + ${revision} + + + io.github.ardoco.core + diagram-connection-generator + ${revision} + compile + + + io.github.ardoco.core + diagram-inconsistency-checker + ${revision} + compile + + + io.github.ardoco.core + diagram-recognition + ${revision} + + + io.github.ardoco.core + inconsistency-detection + ${revision} + + + io.github.ardoco.core + model-provider + ${revision} + + + io.github.ardoco.core + pipeline-core + ${revision} + + + io.github.ardoco.core + pipeline-core + ${revision} + test-jar + test + + + io.github.ardoco.core + recommendation-generator + ${revision} + + + io.github.ardoco.core + tests-inconsistency + ${revision} + test-jar + test + + + io.github.ardoco.core + tests-tlr + ${revision} + test-jar + test + + + io.github.ardoco.core + text-extraction + ${revision} + + + io.github.ardoco.core + text-preprocessing + ${revision} + + + diff --git a/pipeline/pipeline-erid/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForERID.java b/pipeline/pipeline-erid/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForERID.java new file mode 100644 index 000000000..7cd4f2426 --- /dev/null +++ b/pipeline/pipeline-erid/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForERID.java @@ -0,0 +1,62 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.execution; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; + +import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData; +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.ParameterizedRunner; +import edu.kit.kastel.mcse.ardoco.core.inconsistency.InconsistencyChecker; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; +import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.DiagramConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.DiagramInconsistencyChecker; +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +public class ArDoCoForERID extends ParameterizedRunner { + public record Parameters(DiagramProject diagramProject, File inputText, File inputModelArchitecture, ArchitectureModelType inputArchitectureModelType, + SortedMap additionalConfigs, File outputDir) { + } + + public ArDoCoForERID(String projectName, Parameters parameters) { + super(projectName, parameters); + } + + @Override + public List initializePipelineSteps(Parameters p) { + var pipelineSteps = new ArrayList(); + + ArDoCo arDoCo = getArDoCo(); + var dataRepository = arDoCo.getDataRepository(); + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + + var text = CommonUtilities.readInputText(p.inputText); + if (text.isBlank()) { + throw new IllegalArgumentException("Cannot deal with empty input text. Maybe there was an error reading the file."); + } + DataRepositoryHelper.putInputText(dataRepository, text); + pipelineSteps.add(TextPreprocessingAgent.get(p.additionalConfigs, dataRepository)); + + ArCoTLModelProviderAgent arCoTLModelProviderAgent = ArCoTLModelProviderAgent.get(p.inputModelArchitecture, p.inputArchitectureModelType, null, + p.additionalConfigs, dataRepository); + pipelineSteps.add(arCoTLModelProviderAgent); + dataRepository.addData(InputDiagramData.ID, new InputDiagramData(p.diagramProject.getDiagramData())); + pipelineSteps.add(DiagramRecognition.get(p.additionalConfigs, dataRepository)); + pipelineSteps.add(RecommendationGenerator.get(p.additionalConfigs, dataRepository)); + pipelineSteps.add(new DiagramConnectionGenerator(p.additionalConfigs, dataRepository)); + pipelineSteps.add(new DiagramInconsistencyChecker(p.additionalConfigs, dataRepository)); + pipelineSteps.add(ConnectionGenerator.get(p.additionalConfigs, dataRepository)); + pipelineSteps.add(InconsistencyChecker.get(p.additionalConfigs, dataRepository)); + + return pipelineSteps; + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/HoldBackRunResultsProducerERID.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/HoldBackRunResultsProducerERID.java new file mode 100644 index 000000000..09b7a0804 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/HoldBackRunResultsProducerERID.java @@ -0,0 +1,72 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import java.util.ArrayList; +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData; +import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner; +import edu.kit.kastel.mcse.ardoco.core.inconsistency.InconsistencyChecker; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.baseline.InconsistencyBaseline; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.inconsistencyhelper.HoldBackArCoTLModelProvider; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.inconsistencyhelper.HoldBackRunResultsProducer; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.DiagramConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.DiagramInconsistencyChecker; +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.DiagramRecognitionMock; +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +/** + * {@link HoldBackRunResultsProducer} that has been adapted to use the ERID pipeline with the {@link DiagramRecognition} or {@link DiagramRecognitionMock} + * stage. + */ +public class HoldBackRunResultsProducerERID extends HoldBackRunResultsProducer { + boolean useDiagramRecognitionMock; + + /** + * Sole constructor of the class. + * + * @param useDiagramRecognitionMock Whether {@link DiagramRecognitionMock} should be used + */ + public HoldBackRunResultsProducerERID(boolean useDiagramRecognitionMock) { + this.useDiagramRecognitionMock = useDiagramRecognitionMock; + } + + @Override + protected DataRepository runUnshared(GoldStandardProject goldStandardProject, HoldBackArCoTLModelProvider holdElementsBackModelConnector, + DataRepository preRunDataRepository, boolean useInconsistencyBaseline) { + var diagramProject = DiagramProject.getFromName(goldStandardProject.getProjectName()).orElseThrow(); + return new AnonymousRunner(goldStandardProject.getProjectName(), preRunDataRepository) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) { + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + var pipelineSteps = new ArrayList(); + + pipelineSteps.add(holdElementsBackModelConnector.get(additionalConfigs, dataRepository)); + if (useDiagramRecognitionMock) { + pipelineSteps.add(new DiagramRecognitionMock(diagramProject, additionalConfigs, dataRepository)); + } else { + dataRepository.addData(InputDiagramData.ID, new InputDiagramData(diagramProject.getDiagramData())); + pipelineSteps.add(DiagramRecognition.get(additionalConfigs, dataRepository)); + } + pipelineSteps.add(RecommendationGenerator.get(additionalConfigs, dataRepository)); + pipelineSteps.add(new DiagramConnectionGenerator(additionalConfigs, dataRepository)); + pipelineSteps.add(new DiagramInconsistencyChecker(additionalConfigs, dataRepository)); + pipelineSteps.add(ConnectionGenerator.get(additionalConfigs, dataRepository)); + + if (useInconsistencyBaseline) { + pipelineSteps.add(new InconsistencyBaseline(dataRepository)); + } else { + pipelineSteps.add(InconsistencyChecker.get(additionalConfigs, dataRepository)); + } + + return pipelineSteps; + } + }.runWithoutSaving(); + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionBaselineERIDIT.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionBaselineERIDIT.java new file mode 100644 index 000000000..5accb9548 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionBaselineERIDIT.java @@ -0,0 +1,82 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.InconsistencyDetectionEvaluationIT; + +/** + * Performs the inconsistency detection using the standard ArDoCo baseline. + */ +@Disabled +public class InconsistencyDetectionBaselineERIDIT extends InconsistencyDetectionEvaluationIT { + @DisplayName("Evaluating MME-Inconsistency Detection (Mock)") + @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + @Override + protected void missingModelElementInconsistencyIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection (Historic) (Mock)") + @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(2) + @Override + protected void missingModelElementInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyHistoricIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection Baseline (Mock)") + @ParameterizedTest(name = "Evaluating Baseline for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(5) + @Override + protected void missingModelElementInconsistencyBaselineIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyBaselineIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection Baseline (Historical) (Mock)") + @ParameterizedTest(name = "Evaluating Baseline for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(6) + @Override + protected void missingModelElementInconsistencyBaselineHistoricIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyBaselineHistoricIT(goldStandardProject); + } + + /** + * Tests the inconsistency detection for undocumented model elements on all {@link Project projects}. + * + * @param goldStandardProject Project that gets inserted automatically with the enum {@link Project}. + */ + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies (Mock)") + @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(10) + @Override + protected void missingTextInconsistencyIT(GoldStandardProject goldStandardProject) { + super.missingTextInconsistencyIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies (Historical) (Mock)") + @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(11) + @Override + protected void missingTextInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + super.missingTextInconsistencyHistoricIT(goldStandardProject); + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionEvaluationEridIT.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionEvaluationEridIT.java new file mode 100644 index 000000000..87b458ea6 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionEvaluationEridIT.java @@ -0,0 +1,93 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.InconsistencyDetectionEvaluationIT; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.inconsistencyhelper.HoldBackRunResultsProducer; +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagramsWithTLR; + +/** + * Performs the inconsistency detection using ERID with the diagram recognition. + */ +public class InconsistencyDetectionEvaluationEridIT extends InconsistencyDetectionEvaluationIT { + @Override + protected HoldBackRunResultsProducer getHoldBackRunResultsProducer() { + return new HoldBackRunResultsProducerERID(false); + } + + @DisplayName("Evaluating MME-Inconsistency Detection") + @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + @Override + protected void missingModelElementInconsistencyIT(GoldStandardProject goldStandardProject) { + if (goldStandardProject instanceof GoldStandardDiagramsWithTLR goldStandardDiagramProject) + runMissingModelElementInconsistencyEval(goldStandardProject, goldStandardDiagramProject.getExpectedMMEResults()); + else + throw new IllegalArgumentException("Invalid method source"); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection (Historic)") + @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(2) + @Override + protected void missingModelElementInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + if (goldStandardProject instanceof GoldStandardDiagramsWithTLR goldStandardDiagramProject) + runMissingModelElementInconsistencyEval(goldStandardProject, goldStandardDiagramProject.getExpectedMMEResults()); + else + throw new IllegalArgumentException("Invalid method source"); + } + + @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection Baseline") + @ParameterizedTest(name = "Evaluating Baseline for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(5) + @Override + protected void missingModelElementInconsistencyBaselineIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyBaselineIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection Baseline (Historical)") + @ParameterizedTest(name = "Evaluating Baseline for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(6) + @Override + protected void missingModelElementInconsistencyBaselineHistoricIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyBaselineHistoricIT(goldStandardProject); + } + + /** + * Tests the inconsistency detection for undocumented model elements on all {@link Project projects}. + * + * @param goldStandardProject Project that gets inserted automatically with the enum {@link Project}. + */ + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies") + @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(10) + @Override + protected void missingTextInconsistencyIT(GoldStandardProject goldStandardProject) { + super.missingTextInconsistencyIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies (Historical)") + @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(11) + @Override + protected void missingTextInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + super.missingTextInconsistencyHistoricIT(goldStandardProject); + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionWithMockEridIT.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionWithMockEridIT.java new file mode 100644 index 000000000..94564f009 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/InconsistencyDetectionWithMockEridIT.java @@ -0,0 +1,93 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.InconsistencyDetectionEvaluationIT; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.inconsistencyhelper.HoldBackRunResultsProducer; +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagramsWithTLR; + +/** + * Performs the inconsistency detection using ERID with the diagram recognition mock. + */ +public class InconsistencyDetectionWithMockEridIT extends InconsistencyDetectionEvaluationIT { + @Override + protected HoldBackRunResultsProducer getHoldBackRunResultsProducer() { + return new HoldBackRunResultsProducerERID(true); + } + + @DisplayName("Evaluating MME-Inconsistency Detection (Mock)") + @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + @Override + protected void missingModelElementInconsistencyIT(GoldStandardProject goldStandardProject) { + if (goldStandardProject instanceof GoldStandardDiagramsWithTLR goldStandardDiagramProject) + runMissingModelElementInconsistencyEval(goldStandardProject, goldStandardDiagramProject.getExpectedMMEResultsWithMock()); + else + throw new IllegalArgumentException("Invalid method source"); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection (Historic) (Mock)") + @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(2) + @Override + protected void missingModelElementInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + if (goldStandardProject instanceof GoldStandardDiagramsWithTLR goldStandardDiagramProject) + runMissingModelElementInconsistencyEval(goldStandardProject, goldStandardDiagramProject.getExpectedMMEResultsWithMock()); + else + throw new IllegalArgumentException("Invalid method source"); + } + + @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection Baseline (Mock)") + @ParameterizedTest(name = "Evaluating Baseline for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(5) + @Override + protected void missingModelElementInconsistencyBaselineIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyBaselineIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") + @DisplayName("Evaluating MME-Inconsistency Detection Baseline (Historical) (Mock)") + @ParameterizedTest(name = "Evaluating Baseline for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(6) + @Override + protected void missingModelElementInconsistencyBaselineHistoricIT(GoldStandardProject goldStandardProject) { + super.missingModelElementInconsistencyBaselineHistoricIT(goldStandardProject); + } + + /** + * Tests the inconsistency detection for undocumented model elements on all {@link Project projects}. + * + * @param goldStandardProject Project that gets inserted automatically with the enum {@link Project}. + */ + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies (Mock)") + @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(10) + @Override + protected void missingTextInconsistencyIT(GoldStandardProject goldStandardProject) { + super.missingTextInconsistencyIT(goldStandardProject); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies (Historical) (Mock)") + @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(11) + @Override + protected void missingTextInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + super.missingTextInconsistencyHistoricIT(goldStandardProject); + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/SadSamTraceabilityLinkRecoveryEvaluationERID.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/SadSamTraceabilityLinkRecoveryEvaluationERID.java new file mode 100644 index 000000000..3bde940c8 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/SadSamTraceabilityLinkRecoveryEvaluationERID.java @@ -0,0 +1,112 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData; +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; +import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.execution.ConfigurationHelper; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.SadSamTraceabilityLinkRecoveryEvaluation; +import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; +import edu.kit.kastel.mcse.ardoco.core.textextraction.DiagramBackedTextStateStrategy; +import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; +import edu.kit.kastel.mcse.ardoco.core.textextraction.TextStateImpl; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.DiagramConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.DiagramInconsistencyChecker; +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.DiagramRecognitionMock; +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition; +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagramsWithTLR; + +/** + * Pipeline definition for the SAD SAM TLR evaluation using ERID with and without the diagram recognition mock. + */ +public class SadSamTraceabilityLinkRecoveryEvaluationERID extends SadSamTraceabilityLinkRecoveryEvaluation { + private final boolean useDiagramRecognitionMock; + + /** + * Creates a new SAD SAM TLR evaluation using ERID. + * + * @param useDiagramRecognitionMock Whether {@link DiagramRecognitionMock} should be used + */ + public SadSamTraceabilityLinkRecoveryEvaluationERID(boolean useDiagramRecognitionMock) { + this.useDiagramRecognitionMock = useDiagramRecognitionMock; + } + + @Override + protected ArDoCoResult runTraceLinkEvaluation(GoldStandardDiagramsWithTLR project) { + return super.runTraceLinkEvaluation(project); + } + + @Override + protected ArDoCoRunner getAndSetupRunner(GoldStandardDiagramsWithTLR project) { + var additionalConfigs = ConfigurationHelper.loadAdditionalConfigs(project.getAdditionalConfigurationsFile()); + + String name = project.getProjectName(); + File inputModelArchitecture = project.getModelFile(); + File inputText = project.getTextFile(); + File outputDir = new File(TraceLinkEvaluationEridIT.OUTPUT); + + var runner = new AnonymousRunner(name) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) throws IOException { + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + var pipelineSteps = new ArrayList(); + + var text = CommonUtilities.readInputText(inputText); + if (text.isBlank()) { + throw new IllegalArgumentException("Cannot deal with empty input text. Maybe there was an error reading the file."); + } + DataRepositoryHelper.putInputText(dataRepository, text); + pipelineSteps.add(TextPreprocessingAgent.get(additionalConfigs, dataRepository)); + + ArCoTLModelProviderAgent arCoTLModelProviderAgent = ArCoTLModelProviderAgent.get(inputModelArchitecture, ArchitectureModelType.PCM, null, + additionalConfigs, dataRepository); + pipelineSteps.add(arCoTLModelProviderAgent); + + if (useDiagramRecognitionMock) { + pipelineSteps.add(new DiagramRecognitionMock(project, additionalConfigs, dataRepository)); + } else { + dataRepository.addData(InputDiagramData.ID, new InputDiagramData(project.getDiagramData())); + pipelineSteps.add(DiagramRecognition.get(project.getAdditionalConfigurations(), dataRepository)); + } + + var diagramBackedTextStateStrategy = new DiagramBackedTextStateStrategy(dataRepository); + var textState = new TextStateImpl(diagramBackedTextStateStrategy); + dataRepository.addData(TextState.ID, textState); + pipelineSteps.add(TextExtraction.get(additionalConfigs, dataRepository)); + + pipelineSteps.add(RecommendationGenerator.get(additionalConfigs, dataRepository)); + pipelineSteps.add(new DiagramConnectionGenerator(additionalConfigs, dataRepository)); + pipelineSteps.add(new DiagramInconsistencyChecker(additionalConfigs, dataRepository)); + pipelineSteps.add(ConnectionGenerator.get(additionalConfigs, dataRepository)); + + return pipelineSteps; + } + }; + runner.setOutputDirectory(outputDir); + return runner; + } + + @Override + protected ExpectedResults getExpectedResults(GoldStandardDiagramsWithTLR project) { + if (useDiagramRecognitionMock) { + return project.getExpectedSadSamResultsWithMock(); + } + return project.getExpectedSadSamResults(); + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/TraceLinkEvaluationEridIT.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/TraceLinkEvaluationEridIT.java new file mode 100644 index 000000000..d158fad87 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/TraceLinkEvaluationEridIT.java @@ -0,0 +1,70 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT; +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagramsWithTLR; + +/** + * Performs the SAD SAM TLR using ERID with the diagram recognition. + */ +public class TraceLinkEvaluationEridIT extends TraceLinkEvaluationIT { + protected static String OUTPUT = TraceLinkEvaluationIT.OUTPUT; + + @DisplayName("Evaluate SAD-SAM") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(20) + @Override + protected void evaluateSadSamTlrIT(GoldStandardDiagramsWithTLR project) { + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluationERID(false); + var results = evaluation.runTraceLinkEvaluation(project); + Assertions.assertNotNull(results); + } + + @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") + @DisplayName("Evaluate TLR (Historical)") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(21) + @Override + protected void evaluateSadSamTlrHistoricalIT(GoldStandardDiagramsWithTLR project) { + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluationERID(false); + var results = evaluation.runTraceLinkEvaluation(project); + Assertions.assertNotNull(results); + } + + @Override + @Disabled + protected void evaluateSadSamCodeTlrIT(CodeProject codeProject) { + } + + @Override + @Disabled + protected void evaluateSadSamCodeTlrFullIT(CodeProject project) { + } + + @Override + @Disabled + protected void evaluateSamCodeTlrFullIT(CodeProject project) { + } + + @Override + @Disabled + protected void compareSadSamTlRForPcmAndUmlIT(Project project) { + } + + @Override + @Disabled + protected void evaluateSamCodeTlrIT(CodeProject project) { + } +} diff --git a/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/TraceLinkEvaluationWithMockEridIT.java b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/TraceLinkEvaluationWithMockEridIT.java new file mode 100644 index 000000000..541715c2f --- /dev/null +++ b/pipeline/pipeline-erid/src/test/java/edu/kit/kastel/mcse/ardoco/erid/tests/integration/TraceLinkEvaluationWithMockEridIT.java @@ -0,0 +1,66 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.tests.integration; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT; +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagramsWithTLR; + +/** + * Performs the SAD SAM TLR using ERID with the diagram recognition mock. + */ +class TraceLinkEvaluationWithMockEridIT extends TraceLinkEvaluationIT { + @DisplayName("Evaluate SAD-SAM") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(20) + @Override + protected void evaluateSadSamTlrIT(GoldStandardDiagramsWithTLR project) { + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluationERID(true); + var results = evaluation.runTraceLinkEvaluation(project); + Assertions.assertNotNull(results); + } + + @DisplayName("Evaluate TLR (Historical)") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(21) + @Override + protected void evaluateSadSamTlrHistoricalIT(GoldStandardDiagramsWithTLR project) { + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluationERID(true); + var results = evaluation.runTraceLinkEvaluation(project); + Assertions.assertNotNull(results); + } + + @Override + @Disabled + protected void evaluateSadSamCodeTlrIT(CodeProject codeProject) { + } + + @Override + @Disabled + protected void evaluateSadSamCodeTlrFullIT(CodeProject project) { + } + + @Override + @Disabled + protected void evaluateSamCodeTlrFullIT(CodeProject project) { + } + + @Override + @Disabled + protected void compareSadSamTlRForPcmAndUmlIT(Project project) { + } + + @Override + @Disabled + protected void evaluateSamCodeTlrIT(CodeProject project) { + } +} diff --git a/pipeline/pipeline-erid/src/test/resources/simplelogger.properties b/pipeline/pipeline-erid/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..4c830e180 --- /dev/null +++ b/pipeline/pipeline-erid/src/test/resources/simplelogger.properties @@ -0,0 +1,27 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +org.slf4j.simpleLogger.defaultLogLevel=info +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +org.slf4j.simpleLogger.showDateTime=false +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z +# Set to true if you want to output the current thread name. +# Defaults to true. +#org.slf4j.simpleLogger.showThreadName=true +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +org.slf4j.simpleLogger.showLogName=true +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +org.slf4j.simpleLogger.showShortLogName=true diff --git a/pipeline/pipeline-id/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForInconsistencyDetection.java b/pipeline/pipeline-id/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForInconsistencyDetection.java index 529e62523..23f9873df 100644 --- a/pipeline/pipeline-id/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForInconsistencyDetection.java +++ b/pipeline/pipeline-id/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForInconsistencyDetection.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution.runner; import java.io.File; @@ -10,7 +10,7 @@ import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; import edu.kit.kastel.mcse.ardoco.core.inconsistency.InconsistencyChecker; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; diff --git a/pipeline/pipeline-lissa/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForLiSSA.java b/pipeline/pipeline-lissa/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForLiSSA.java index e401558af..461e3dd28 100644 --- a/pipeline/pipeline-lissa/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForLiSSA.java +++ b/pipeline/pipeline-lissa/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/runner/ArDoCoForLiSSA.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution.runner; import java.io.File; @@ -10,7 +10,7 @@ import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; import edu.kit.kastel.mcse.ardoco.core.inconsistency.InconsistencyChecker; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; diff --git a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadCodeTraceabilityLinkRecovery.java b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadCodeTraceabilityLinkRecovery.java index 3ea495578..289ca9147 100644 --- a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadCodeTraceabilityLinkRecovery.java +++ b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadCodeTraceabilityLinkRecovery.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution; import java.io.File; @@ -9,7 +9,7 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; diff --git a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamCodeTraceabilityLinkRecovery.java b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamCodeTraceabilityLinkRecovery.java index 11657eca3..8e21e4ec1 100644 --- a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamCodeTraceabilityLinkRecovery.java +++ b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamCodeTraceabilityLinkRecovery.java @@ -1,12 +1,9 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution; import java.io.File; import java.util.SortedMap; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; import edu.kit.kastel.mcse.ardoco.core.codetraceability.SadSamCodeTraceabilityLinkRecovery; import edu.kit.kastel.mcse.ardoco.core.codetraceability.SamCodeTraceabilityLinkRecovery; @@ -14,13 +11,12 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; public class ArDoCoForSadSamCodeTraceabilityLinkRecovery extends ArDoCoRunner { - private static final Logger logger = LoggerFactory.getLogger(ArDoCoForSadSamCodeTraceabilityLinkRecovery.class); public ArDoCoForSadSamCodeTraceabilityLinkRecovery(String projectName) { super(projectName); diff --git a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamTraceabilityLinkRecovery.java b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamTraceabilityLinkRecovery.java index e88294c3e..1b76733c4 100644 --- a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamTraceabilityLinkRecovery.java +++ b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSadSamTraceabilityLinkRecovery.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution; import java.io.File; @@ -9,7 +9,7 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; diff --git a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSamCodeTraceabilityLinkRecovery.java b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSamCodeTraceabilityLinkRecovery.java index f4efbd824..b4cf768ac 100644 --- a/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSamCodeTraceabilityLinkRecovery.java +++ b/pipeline/pipeline-tlr/src/main/java/edu/kit/kastel/mcse/ardoco/core/execution/ArDoCoForSamCodeTraceabilityLinkRecovery.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.execution; import java.io.File; @@ -7,7 +7,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; import edu.kit.kastel.mcse.ardoco.core.codetraceability.SamCodeTraceabilityLinkRecovery; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; public class ArDoCoForSamCodeTraceabilityLinkRecovery extends ArDoCoRunner { diff --git a/pipeline/pom.xml b/pipeline/pom.xml index 57075f39f..68abddf9c 100644 --- a/pipeline/pom.xml +++ b/pipeline/pom.xml @@ -46,6 +46,7 @@ pipeline-core + pipeline-erid pipeline-id pipeline-lissa pipeline-tlr @@ -96,5 +97,16 @@ pipeline-lissa + + + erid + + false + + + pipeline-core + pipeline-erid + + diff --git a/pom.xml b/pom.xml index 13f11c064..0bc58848a 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,8 @@ 0.41.0-SNAPSHOT ${revision} - + UTF-8 + UTF-8 UTF-8 21 ${java.version} @@ -94,6 +95,7 @@ 3.25.8 2.24.1 5.2.0 + 3.3.0 1.5.2 ArDoCo_Core @@ -105,6 +107,7 @@ report/target/site/jacoco-aggregate/jacoco.xml -Xmx4g -Xss256m 4.5.6 + all,-missing @@ -114,7 +117,6 @@ jackson-annotations ${jackson.version} - com.fasterxml.jackson.core jackson-core @@ -252,6 +254,11 @@ + + com.google.errorprone + error_prone_core + ${error-prone.version} + org.jetbrains.kotlin kotlin-reflect @@ -479,6 +486,11 @@ org.apache.maven.plugins maven-javadoc-plugin 3.6.3 + + + en_US + ${doclint.options} + attach-javadocs diff --git a/report/pom.xml b/report/pom.xml index a6f0a8027..80a097763 100644 --- a/report/pom.xml +++ b/report/pom.xml @@ -5,6 +5,7 @@ io.github.ardoco.core parent ${revision} + ../pom.xml report @@ -34,12 +35,24 @@ ${revision} compile + + io.github.ardoco.core + diagram-connection-generator + ${revision} + compile + io.github.ardoco.core diagram-consistency ${revision} compile + + io.github.ardoco.core + diagram-inconsistency-checker + ${revision} + compile + io.github.ardoco.core diagram-recognition @@ -64,6 +77,12 @@ ${revision} compile + + io.github.ardoco.core + pipeline-erid + ${revision} + compile + io.github.ardoco.core pipeline-id diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/CodeTraceabilityStateImpl.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/CodeTraceabilityStateImpl.java index cd3fc5d43..9a32c87a1 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/CodeTraceabilityStateImpl.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/CodeTraceabilityStateImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability; import java.util.Collection; @@ -18,8 +18,12 @@ @Deterministic public class CodeTraceabilityStateImpl extends AbstractState implements CodeTraceabilityState { - private transient MutableList samCodeTraceLinks = Lists.mutable.empty(); - private transient MutableList transitiveTraceLinks = Lists.mutable.empty(); + private MutableList samCodeTraceLinks = Lists.mutable.empty(); + private MutableList transitiveTraceLinks = Lists.mutable.empty(); + + public CodeTraceabilityStateImpl() { + super(); + } @Override public boolean addSamCodeTraceLink(SamCodeTraceLink traceLink) { diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadCodeTraceabilityLinkRecovery.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadCodeTraceabilityLinkRecovery.java index 5ea44f06f..c90da7ad0 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadCodeTraceabilityLinkRecovery.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadCodeTraceabilityLinkRecovery.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability; import java.util.List; diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadSamCodeTraceabilityLinkRecovery.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadSamCodeTraceabilityLinkRecovery.java index c486c42ad..bffa5e1b4 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadSamCodeTraceabilityLinkRecovery.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SadSamCodeTraceabilityLinkRecovery.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability; import java.util.List; diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SamCodeTraceabilityLinkRecovery.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SamCodeTraceabilityLinkRecovery.java index 4042e0f6a..791584460 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SamCodeTraceabilityLinkRecovery.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/SamCodeTraceabilityLinkRecovery.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability; import java.util.List; diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArCoTLInformant.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArCoTLInformant.java index f9a9e078e..8d498a515 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArCoTLInformant.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArCoTLInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability.informants; import java.util.Arrays; @@ -20,7 +20,7 @@ public ArCoTLInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { var dataRepository = getDataRepository(); var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); var samCodeTraceabilityState = DataRepositoryHelper.getCodeTraceabilityState(dataRepository); diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArchitectureLinkToCodeLinkTransformerInformant.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArchitectureLinkToCodeLinkTransformerInformant.java index d500afa7b..f62ec9690 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArchitectureLinkToCodeLinkTransformerInformant.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/ArchitectureLinkToCodeLinkTransformerInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability.informants; import java.util.ArrayList; @@ -30,7 +30,7 @@ public ArchitectureLinkToCodeLinkTransformerInformant(DataRepository dataReposit } @Override - public void run() { + public void process() { MutableSet sadCodeTracelinks = Sets.mutable.empty(); ModelStates modelStatesData = DataRepositoryHelper.getModelStatesData(getDataRepository()); diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/TraceLinkCombiner.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/TraceLinkCombiner.java index 5cd566086..007192f51 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/TraceLinkCombiner.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/TraceLinkCombiner.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability.informants; import java.util.SortedMap; @@ -27,7 +27,7 @@ public TraceLinkCombiner(DataRepository dataRepository) { } @Override - public void run() { + public void process() { MutableSet transitiveTraceLinks = Sets.mutable.empty(); CodeTraceabilityState codeTraceabilityState = DataRepositoryHelper.getCodeTraceabilityState(getDataRepository()); ModelStates modelStatesData = DataRepositoryHelper.getModelStatesData(getDataRepository()); diff --git a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/arcotl/computation/Confidence.java b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/arcotl/computation/Confidence.java index 8678eff4d..0d4753906 100644 --- a/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/arcotl/computation/Confidence.java +++ b/stages/code-traceability/src/main/java/edu/kit/kastel/mcse/ardoco/core/codetraceability/informants/arcotl/computation/Confidence.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.codetraceability.informants.arcotl.computation; import java.util.NoSuchElementException; @@ -91,7 +91,7 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (obj == null || getClass() != obj.getClass()) { + if (!(obj instanceof Confidence)) { return false; } Confidence other = (Confidence) obj; diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionGenerator.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionGenerator.java index 51f60fbd6..9e23c5094 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionGenerator.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionGenerator.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator; import java.util.List; @@ -13,9 +13,8 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractExecutionStage; /** - * The ModelConnectionAgent runs different analyzers and solvers. This agent creates recommendations as well as - * matchings between text and model. The order is important: All connections should run after the recommendations have - * been made. + * The ModelConnectionAgent runs different analyzers and solvers. This agent creates recommendations as well as matchings between text and model. The order is + * important: All connections should run after the recommendations have been made. */ public class ConnectionGenerator extends AbstractExecutionStage { diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStateImpl.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStateImpl.java index d5384e45b..cd4712dbb 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStateImpl.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStateImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator; import org.eclipse.collections.api.factory.Lists; @@ -18,12 +18,13 @@ */ public class ConnectionStateImpl extends AbstractState implements ConnectionState { - private transient MutableList instanceLinks; + private MutableList instanceLinks; /** * Creates a new connection state. */ public ConnectionStateImpl() { + super(); instanceLinks = Lists.mutable.empty(); } diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStatesImpl.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStatesImpl.java index 9c055dcc4..39b202516 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStatesImpl.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/ConnectionStatesImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator; import java.util.EnumMap; diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ProjectNameFilterAgent.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ProjectNameFilterAgent.java index 430b5cba8..1889c7bab 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ProjectNameFilterAgent.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ProjectNameFilterAgent.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.agents; import java.util.List; @@ -9,9 +9,8 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; /** - * This agent should look for {@link RecommendedInstance RecommendedInstances} that contain the - * project's name and "filters" them by adding a heavy negative probability, thus making the - * {@link RecommendedInstance} extremely improbable. + * This agent should look for {@link RecommendedInstance RecommendedInstances} that contain the project's name and "filters" them by adding a heavy negative + * probability, thus making the {@link RecommendedInstance} extremely improbable. */ public class ProjectNameFilterAgent extends PipelineAgent { diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ReferenceAgent.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ReferenceAgent.java index fef93439a..b60a844d5 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ReferenceAgent.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/agents/ReferenceAgent.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.agents; import java.util.List; @@ -8,8 +8,7 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; /** - * The reference solver finds instances mentioned in the text extraction state as names. If it founds some similar names - * it creates recommendations. + * The reference solver finds instances mentioned in the text extraction state as names. If it founds some similar names it creates recommendations. */ public class ReferenceAgent extends PipelineAgent { diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ExtractionDependentOccurrenceInformant.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ExtractionDependentOccurrenceInformant.java index aa008ed1d..20a2705e7 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ExtractionDependentOccurrenceInformant.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ExtractionDependentOccurrenceInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.informants; import java.util.SortedMap; @@ -9,7 +9,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -28,7 +27,7 @@ public ExtractionDependentOccurrenceInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var text = DataRepositoryHelper.getAnnotatedText(dataRepository); var textState = DataRepositoryHelper.getTextState(dataRepository); @@ -55,7 +54,7 @@ private void searchForName(LegacyModelExtractionState modelState, TextState text if (posTagIsUndesired(word) && !wordStartsWithCapitalLetter(word)) { return; } - var instanceNameIsSimilar = modelState.getInstances().anySatisfy(i -> SimilarityUtils.isWordSimilarToModelInstance(word, i)); + var instanceNameIsSimilar = modelState.getInstances().anySatisfy(i -> getMetaData().getSimilarityUtils().isWordSimilarToModelInstance(word, i)); if (instanceNameIsSimilar) { textState.addNounMapping(word, MappingKind.NAME, this, probability); } @@ -75,7 +74,8 @@ private boolean posTagIsUndesired(Word word) { * value is taken as reference. */ private void searchForType(LegacyModelExtractionState modelState, TextState textState, Word word) { - var instanceTypeIsSimilar = modelState.getInstances().anySatisfy(i -> SimilarityUtils.isWordSimilarToModelInstanceType(word, i)); + var similarityUtils = getMetaData().getSimilarityUtils(); + var instanceTypeIsSimilar = modelState.getInstances().anySatisfy(i -> similarityUtils.isWordSimilarToModelInstanceType(word, i)); if (instanceTypeIsSimilar) { textState.addNounMapping(word, MappingKind.TYPE, this, probability); } diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/InstantConnectionInformant.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/InstantConnectionInformant.java index abb0bd16c..0fcb16d6d 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/InstantConnectionInformant.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/InstantConnectionInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.informants; import java.util.SortedMap; @@ -9,7 +9,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.models.ModelInstance; import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationState; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -25,7 +24,7 @@ public InstantConnectionInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); var recommendationStates = DataRepositoryHelper.getRecommendationStates(dataRepository); @@ -48,8 +47,9 @@ public void run() { private void findNamesOfModelInstancesInSupposedMappings(LegacyModelExtractionState modelState, RecommendationState recommendationState, ConnectionState connectionState) { var recommendedInstances = recommendationState.getRecommendedInstances(); + var similarityUtils = getMetaData().getSimilarityUtils(); for (ModelInstance instance : modelState.getInstances()) { - var mostLikelyRi = SimilarityUtils.getMostRecommendedInstancesToInstanceByReferences(instance, recommendedInstances); + var mostLikelyRi = similarityUtils.getMostRecommendedInstancesToInstanceByReferences(instance, recommendedInstances); for (var recommendedInstance : mostLikelyRi) { var riProbability = recommendedInstance.getTypeMappings().isEmpty() ? probabilityWithoutType : probability; @@ -62,7 +62,7 @@ private void createLinksForEqualOrSimilarRecommendedInstances(LegacyModelExtract ConnectionState connectionState) { for (var recommendedInstance : recommendationState.getRecommendedInstances()) { var sameInstances = modelState.getInstances() - .select(instance -> SimilarityUtils.isRecommendedInstanceSimilarToModelInstance(recommendedInstance, instance)); + .select(instance -> getMetaData().getSimilarityUtils().isRecommendedInstanceSimilarToModelInstance(recommendedInstance, instance)); sameInstances.forEach(instance -> connectionState.addToLinks(recommendedInstance, instance, this, probability)); } } diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/NameTypeConnectionInformant.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/NameTypeConnectionInformant.java index 8553f620f..912bf4728 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/NameTypeConnectionInformant.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/NameTypeConnectionInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.informants; import java.util.SortedMap; @@ -18,7 +18,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -37,7 +36,7 @@ public NameTypeConnectionInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var text = DataRepositoryHelper.getAnnotatedText(dataRepository); var textState = DataRepositoryHelper.getTextState(dataRepository); @@ -71,7 +70,7 @@ private void checkForNameBeforeType(TextState textExtractionState, Word word, Le var preWord = word.getPreWord(); - var similarTypes = CommonUtilities.getSimilarTypes(word, modelState); + var similarTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!similarTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -101,7 +100,7 @@ private void checkForNameAfterType(TextState textExtractionState, Word word, Leg var after = word.getNextWord(); - var sameLemmaTypes = CommonUtilities.getSimilarTypes(word, modelState); + var sameLemmaTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!sameLemmaTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -125,7 +124,7 @@ private void checkForNortBeforeType(TextState textExtractionState, Word word, Le var preWord = word.getPreWord(); - var sameLemmaTypes = CommonUtilities.getSimilarTypes(word, modelState); + var sameLemmaTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!sameLemmaTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -150,7 +149,7 @@ private void checkForNortAfterType(TextState textExtractionState, Word word, Leg var after = word.getNextWord(); - var sameLemmaTypes = CommonUtilities.getSimilarTypes(word, modelState); + var sameLemmaTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!sameLemmaTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -205,7 +204,8 @@ private ModelInstance tryToIdentify(TextState textExtractionState, ImmutableList } var text = word.getText(); - matchingInstances = matchingInstances.select(i -> SimilarityUtils.areWordsOfListsSimilar(i.getNameParts(), Lists.immutable.with(text))); + matchingInstances = matchingInstances.select(i -> getMetaData().getSimilarityUtils() + .areWordsOfListsSimilar(i.getNameParts(), Lists.immutable.with(text))); if (!matchingInstances.isEmpty()) { return matchingInstances.get(0); diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ProjectNameInformant.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ProjectNameInformant.java index 407d1406c..50577d22d 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ProjectNameInformant.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ProjectNameInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.informants; import java.util.Objects; @@ -15,13 +15,13 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Word; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; /** - * This informant looks for (parts of) the project's name within RecommendedInstances and if it finds the project's name, influences the probability of the + * This informant looks for (parts of) the project's name within RecommendedInstances and if it finds the project's name, influences the + * probability of the * RecommendedInstance negatively because it then should not be a recommended instance. */ public class ProjectNameInformant extends Informant { @@ -32,7 +32,7 @@ public class ProjectNameInformant extends Informant { /** * Constructs a new instance of the {@link ProjectNameInformant} with the given data repository. - * + * * @param dataRepository the data repository */ public ProjectNameInformant(DataRepository dataRepository) { @@ -40,7 +40,7 @@ public ProjectNameInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var projectName = DataRepositoryHelper.getProjectPipelineData(dataRepository).getProjectName(); var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); @@ -51,6 +51,7 @@ public void run() { var recommendationState = recommendationStates.getRecommendationState(metamodel); checkForProjectNameInRecommendedInstances(projectName, recommendationState); + checkForProjectNameInRecommendedInstances(projectName.toLowerCase(), recommendationState); } } @@ -72,7 +73,7 @@ private void checkWordsInNounMapping(String projectName, RecommendedInstance rec if (projectName.contains(wordText)) { var words = expandWordForName(projectName, word); var expandedWord = concatenateWords(words); - if (SimilarityUtils.areWordsSimilar(projectName, expandedWord)) { + if (getMetaData().getSimilarityUtils().areWordsSimilar(projectName, expandedWord)) { recommendedInstance.addProbability(this, penalty); } } diff --git a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ReferenceInformant.java b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ReferenceInformant.java index be2729f1d..b92aac70f 100644 --- a/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ReferenceInformant.java +++ b/stages/connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/connectiongenerator/informants/ReferenceInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.connectiongenerator.informants; import java.util.SortedMap; @@ -12,7 +12,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -27,7 +26,7 @@ public ReferenceInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var textState = DataRepositoryHelper.getTextState(dataRepository); var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); @@ -57,7 +56,7 @@ private void findRecommendedInstancesFromNounMappingsThatAreSimilarToInstances(L private ImmutableList getSimilarNounMappings(ModelInstance instance, TextState textState) { return textState.getNounMappingsOfKind(MappingKind.NAME) - .select(nounMapping -> SimilarityUtils.isNounMappingSimilarToModelInstance(nounMapping, instance)); + .select(nounMapping -> getMetaData().getSimilarityUtils().isNounMappingSimilarToModelInstance(nounMapping, instance)); } @Override diff --git a/stages/diagram-connection-generator/.gitignore b/stages/diagram-connection-generator/.gitignore new file mode 100644 index 000000000..5ff6309b7 --- /dev/null +++ b/stages/diagram-connection-generator/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/stages/diagram-connection-generator/pom.xml b/stages/diagram-connection-generator/pom.xml new file mode 100644 index 000000000..cad2e9387 --- /dev/null +++ b/stages/diagram-connection-generator/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + io.github.ardoco.core + stages + ${revision} + ../pom.xml + + diagram-connection-generator + + + + io.github.ardoco.core + common + ${revision} + compile + + + io.github.ardoco.core + diagram-recognition + ${revision} + + + io.github.ardoco.core + pipeline-tlr + ${revision} + compile + + + io.github.ardoco.core + tests-base + ${revision} + test + + + io.github.ardoco.core + tests-resources + ${revision} + compile + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.slf4j + slf4j-simple + test + + + diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagramconnectiongenerator/DiagramConnectionState.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagramconnectiongenerator/DiagramConnectionState.java new file mode 100644 index 000000000..7beae18c0 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagramconnectiongenerator/DiagramConnectionState.java @@ -0,0 +1,160 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramWordTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; +import edu.kit.kastel.mcse.ardoco.core.configuration.IConfigurable; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; +import edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi; + +/** + * This state holds the {@link LinkBetweenDeAndRi} trace links. It also provides functions to convert them to {@link DiagramWordTraceLink DiaWordTraceLinks}. + */ +public interface DiagramConnectionState extends IConfigurable { + Logger logger = LoggerFactory.getLogger(DiagramConnectionState.class); + + /** + * Returns all diagram links. + * + * @return immutable set of diagram links + */ + + Set getLinksBetweenDeAndRi(); + + /** + * Returns all diagram links to a specific diagram element. + * + * @param diagramElement the element + * @return a potentially empty set of trace links + */ + default Set getLinksBetweenDeAndRi(DiagramElement diagramElement) { + return new LinkedHashSet<>(getLinksBetweenDeAndRi().stream().filter(d -> d.getDiagramElement().equals(diagramElement)).toList()); + } + + /** + * Returns all diagram links to a specific recommended instance. + * + * @param recommendedInstance the element + * @return a potentially empty set of trace links + */ + default Set getLinksBetweenDeAndRi(RecommendedInstance recommendedInstance) { + return new LinkedHashSet<>(getLinksBetweenDeAndRi().stream().filter(d -> d.getRecommendedInstance().equals(recommendedInstance)).toList()); + } + + /** + * Creates a new diagram link using the specified parameters and adds it to the state. Returns true if a link with the same properties wasn't already + * contained by the state. The confidenceMap should contain a value for every word covered by this link. + * + * @param recommendedInstance recommended instance + * @param diagramElement diagram element + * @param projectName name of the project + * @param claimant the claimant responsible for the link + * @param confidenceMap confidence map containing the confidences + * @return true, if the link wasn't already contained, false else + */ + boolean addToLinksBetweenDeAndRi(RecommendedInstance recommendedInstance, DiagramElement diagramElement, String projectName, Claimant claimant, + SortedMap confidenceMap); + + /** + * Trys to add the diagram link to the state. Returns true if a link with the same properties wasn't already contained by the state. + * + * @return true, if the link wasn't already contained, false else + */ + boolean addToLinksBetweenDeAndRi(LinkBetweenDeAndRi linkBetweenDeAndRi); + + /** + * Removes the specified diagram link from the state. Returns true of the link was contained. + * + * @param linkBetweenDeAndRi diagram link + * @return true, if the link was contained, false else + */ + boolean removeFromLinksBetweenDeAndRi(LinkBetweenDeAndRi linkBetweenDeAndRi); + + /** + * {@return the confidence threshold for filtering out diagram-to-word trace links} + */ + double getConfidenceThreshold(); + + /** + * {@return a set of diagram-to-word trace links} A {@link DiagramWordTraceLink} is created for every word covered by a {@link LinkBetweenDeAndRi}. Low + * confidence links are filtered out. + */ + default Set getWordTraceLinks() { + var traceLinks = new LinkedHashSet(); + for (var linkBetweenDeAndRi : getLinksBetweenDeAndRi()) { + traceLinks.addAll(linkBetweenDeAndRi.toTraceLinks()); + } + var aboveThreshold = traceLinks.stream().filter(diaWordTraceLink -> diaWordTraceLink.getConfidence() >= getConfidenceThreshold()).toList(); + logger.debug("Removed {} Word Trace Links due to low confidence", traceLinks.size() - aboveThreshold.size()); + return new LinkedHashSet<>(aboveThreshold); + } + + /** + * {@return a set of diagram-to-sentence trace links} If the sentence is covered by multiple diagram-to-word trace links of the same diagram element, the + * diagram-to-word trace links are grouped into diagram-to-sentence trace links. + */ + default Set getTraceLinks() { + return getByEqualDEAndSentence(getWordTraceLinks()); + } + + /** + * {@return a set of diagram-to-word trace links} If multiple trace links point to the same word, the diagram-to-word trace link with the highest confidence + * is chosen. + */ + default Set getMostSpecificWordTraceLinks() { + var allLinks = new LinkedHashSet(); + var sameWord = getWordTraceLinks().stream().collect(Collectors.groupingBy(tl -> tl.getWord().getPosition())); + sameWord.remove(-1); + allLinks.addAll(sameWord.values().stream().flatMap(tls -> getHighestConfidenceTraceLinks(tls).stream()).toList()); + return new LinkedHashSet<>(allLinks); + } + + /** + * {@return a set of diagram-to-sentence trace links} If multiple trace links point to the same word, the diagram-to-word trace link with the highest + * confidence is chosen. If the sentence is covered by multiple diagram-to-word trace links of the same diagram element, the diagram-to-word trace links are + * grouped into diagram-to-sentence trace links. + */ + default Set getMostSpecificTraceLinks() { + return getByEqualDEAndSentence(getMostSpecificWordTraceLinks()); + } + + /** + * {@return the set of diagram-to-word trace links with the highest confidence} + * + * @param traceLinks the diagram-to-word trace links + */ + private List getHighestConfidenceTraceLinks(List traceLinks) { + var max = traceLinks.stream().max(DiagramWordTraceLink.CONFIDENCE_COMPARATOR).map(DiagramWordTraceLink::getConfidence).orElse(Double.MAX_VALUE); + var allMaxima = traceLinks.stream().filter(tl -> Double.compare(tl.getConfidence(), max) >= 0).toList(); + allMaxima.forEach(m -> m.addRelated(allMaxima)); + return allMaxima; + } + + /** + * {@return the set of diagram-to-sentence trace links} If a diagram element has multiple diagram-to-word trace links pointing to the same sentence, the + * sentence with the highest confidence is chosen as representative. + * + * @param traceLinks the diagram-to-word trace links + */ + private Set getByEqualDEAndSentence(Set traceLinks) { + var sameDEAndSentence = traceLinks.stream() + .collect(Collectors.groupingBy(l -> new Pair<>(l.getDiagramElement(), l.getSentenceNo()), LinkedHashMap::new, Collectors.toList())); + return new LinkedHashSet<>(sameDEAndSentence.values() + .stream() + .map(l -> l.stream().max(DiagramWordTraceLink.CONFIDENCE_COMPARATOR).orElseThrow()) + .toList()); + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagramconnectiongenerator/DiagramConnectionStates.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagramconnectiongenerator/DiagramConnectionStates.java new file mode 100644 index 000000000..d7809d09c --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagramconnectiongenerator/DiagramConnectionStates.java @@ -0,0 +1,19 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; + +/** + * Contains the {@link DiagramConnectionState} for each {@link Metamodel}. + */ +public interface DiagramConnectionStates extends PipelineStepData { + String ID = "DiagramConnectionStates"; + + /** + * {@return the state for the specified metamodel} + * + * @param mm the metamodel + */ + DiagramConnectionState getDiagramConnectionState(Metamodel mm); +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/models/tracelinks/LinkBetweenDeAndRi.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/models/tracelinks/LinkBetweenDeAndRi.java new file mode 100644 index 000000000..d91f4492e --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/models/tracelinks/LinkBetweenDeAndRi.java @@ -0,0 +1,139 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramWordTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.EndpointTuple; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; + +/** + * This class represents a trace link between a {@link DiagramElement} and a {@link RecommendedInstance}. + */ +public class LinkBetweenDeAndRi extends EndpointTuple implements Claimant, Comparable { + private final RecommendedInstance recommendedInstance; + private final DiagramElement diagramElement; + private final String projectName; + private final Claimant claimant; + private final SortedMap confidenceMap; + + /** + * Creates a new trace link for the given project with the confidence map provided by the claimant. + * + * @param recommendedInstance the recommended instance + * @param diagramElement the diagram element + * @param projectName the name of the project + * @param claimant the {@link Claimant} responsible for the creation of this link + * @param confidenceMap the confidence + */ + public LinkBetweenDeAndRi(RecommendedInstance recommendedInstance, DiagramElement diagramElement, String projectName, Claimant claimant, + SortedMap confidenceMap) { + super(recommendedInstance, diagramElement); + + this.recommendedInstance = recommendedInstance; + this.diagramElement = diagramElement; + this.projectName = projectName; + this.claimant = claimant; + this.confidenceMap = confidenceMap; + } + + /** + * {@return the traced recommended instance} + */ + public RecommendedInstance getRecommendedInstance() { + return recommendedInstance; + } + + /** + * {@return the traced diagram element} + */ + public DiagramElement getDiagramElement() { + return diagramElement; + } + + /** + * {@return the confidence for a given word} {@link Double#MIN_VALUE}, if the word is not part of the traced recommended instance. + * + * @param word the word + */ + public double getConfidence(Word word) { + return confidenceMap.getOrDefault(word, Double.MIN_VALUE); + } + + /** + * {@return a map of confidences for each word that is part of the recommended instance} + */ + public SortedMap getConfidenceMap() { + return new TreeMap<>(confidenceMap); + } + + /** + * Sets the confidence for a specific word. Does not check whether the word is contained by the recommended instance. + */ + public void setConfidence(Word word, double confidence) { + confidenceMap.put(word, confidence); + } + + /** + * {@return the overall confidence in the link} The confidence is calculated by aggregating the confidence map values using a {@link AggregationFunctions}. + * + * @param aggregationFunction an aggregation function + */ + public double getConfidence(AggregationFunctions aggregationFunction) { + return aggregationFunction.applyAsDouble(getConfidenceMap().values()); + } + + @Override + public String toString() { + return String.format("[%s]-[%s]-[%s]-[%s]", recommendedInstance.getName(), diagramElement, confidenceMap.values().stream().max(Double::compareTo), + claimant.getClass().getSimpleName()); + } + + @Override + public int compareTo(LinkBetweenDeAndRi o) { + if (equals(o)) + return 0; + var comp = getRecommendedInstance().getName().compareTo(o.getRecommendedInstance().getName()); + if (comp == 0) { + return getDiagramElement().compareTo(o.getDiagramElement()); + } + return comp; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof LinkBetweenDeAndRi other) { + return diagramElement.equals(other.diagramElement) && recommendedInstance.equals(other.recommendedInstance) && claimant.equals(other.claimant); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(diagramElement, recommendedInstance, claimant); + } + + /** + * Returns an immutable set of tracelinks between the diagram element of this link and every word that is contained in the recommended instance's name + * mappings. Can be empty. + * + * @return immutable set of diagram text tracelinks + */ + public Set toTraceLinks() { + return new LinkedHashSet<>(getConfidenceMap().entrySet() + .stream() + .map(e -> new DiagramWordTraceLink(getDiagramElement(), e.getKey(), projectName, e.getValue(), this)) + .toList()); + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionGenerator.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionGenerator.java new file mode 100644 index 000000000..423aec450 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionGenerator.java @@ -0,0 +1,55 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator; + +import java.util.List; +import java.util.SortedMap; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.ExecutionStage; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.agents.DiagramConnectionAgent; + +/** + * This stage is responsible for the creation of {@link edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi} trace links. + */ +public class DiagramConnectionGenerator extends ExecutionStage { + + /** + * Sole constructor for the stage. + * + * @param additionalConfigs the additional configs that should be applied + * @param dataRepository the data repository that should be used + */ + public DiagramConnectionGenerator(SortedMap additionalConfigs, DataRepository dataRepository) { + super(List.of(new DiagramConnectionAgent(dataRepository)), "DiagramConnectionGenerator", dataRepository, additionalConfigs); + } + + @Override + protected void initializeState() { + logger.info("Creating DiagramConnectionGenerator States"); + var diagramConnectionStates = new DiagramConnectionStatesImpl(); + getDataRepository().addData(DiagramConnectionStates.ID, diagramConnectionStates); + } + + private UnicodeCharacterMatchFunctions previousCharacterMatchFunction; + + /** + * Saves the previous character match function and sets a character match function capable of matching homoglyphs. + */ + @Override + protected void before() { + super.before(); + previousCharacterMatchFunction = dataRepository.getGlobalConfiguration().getWordSimUtils().getCharacterMatchFunction(); + dataRepository.getGlobalConfiguration().getWordSimUtils().setCharacterMatchFunction(UnicodeCharacterMatchFunctions.EQUAL_OR_HOMOGLYPH); + } + + /** + * Sets the character match function back to the previous function. + */ + @Override + protected void after() { + dataRepository.getGlobalConfiguration().getWordSimUtils().setCharacterMatchFunction(previousCharacterMatchFunction); + super.after(); + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionStateImpl.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionStateImpl.java new file mode 100644 index 000000000..ce42c4559 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionStateImpl.java @@ -0,0 +1,75 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.SortedMap; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; +import edu.kit.kastel.mcse.ardoco.core.data.AbstractState; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionState; +import edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi; + +/** + * @see DiagramConnectionState + */ +public class DiagramConnectionStateImpl extends AbstractState implements DiagramConnectionState { + @Configurable + private double confidenceThreshold = 0.4; + + private final LinkedHashSet linksBetweenDeAndRi = new LinkedHashSet<>(); + + public DiagramConnectionStateImpl() { + super(); + } + + @Override + public Set getLinksBetweenDeAndRi() { + return new LinkedHashSet<>(linksBetweenDeAndRi); + } + + @Override + public boolean addToLinksBetweenDeAndRi(RecommendedInstance recommendedInstance, DiagramElement diagramElement, String textIdentifier, Claimant claimant, + SortedMap confidenceMap) { + var newDL = new LinkBetweenDeAndRi(recommendedInstance, diagramElement, textIdentifier, claimant, confidenceMap); + var added = linksBetweenDeAndRi.add(newDL); + if (added) + return true; + + for (var dl : linksBetweenDeAndRi) { + if (dl.equals(newDL)) { + confidenceMap.forEach((word, conf) -> dl.setConfidence(word, Math.max(dl.getConfidence(word), conf))); + } + } + return false; + } + + @Override + public boolean addToLinksBetweenDeAndRi(LinkBetweenDeAndRi linkBetweenDeAndRi) { + var added = linksBetweenDeAndRi.add(linkBetweenDeAndRi); + + if (added) + return true; + + for (var dl : linksBetweenDeAndRi) { + if (dl.equals(linkBetweenDeAndRi)) { + linkBetweenDeAndRi.getConfidenceMap().forEach((word, conf) -> dl.setConfidence(word, Math.max(dl.getConfidence(word), conf))); + } + } + return false; + } + + @Override + public boolean removeFromLinksBetweenDeAndRi(LinkBetweenDeAndRi linkBetweenDeAndRi) { + return linksBetweenDeAndRi.remove(linkBetweenDeAndRi); + } + + @Override + public double getConfidenceThreshold() { + return confidenceThreshold; + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionStatesImpl.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionStatesImpl.java new file mode 100644 index 000000000..ff8ad19e0 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/DiagramConnectionStatesImpl.java @@ -0,0 +1,29 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator; + +import java.util.EnumMap; +import java.util.Map; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.data.AbstractState; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionState; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; + +/** + * @see DiagramConnectionStates + */ +public class DiagramConnectionStatesImpl extends AbstractState implements DiagramConnectionStates { + private final Map diagramConnectionStates = new EnumMap<>(Metamodel.class); + + public DiagramConnectionStatesImpl() { + super(); + for (Metamodel mm : Metamodel.values()) { + diagramConnectionStates.put(mm, new DiagramConnectionStateImpl()); + } + } + + @Override + public DiagramConnectionState getDiagramConnectionState(Metamodel mm) { + return diagramConnectionStates.get(mm); + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/agents/DiagramConnectionAgent.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/agents/DiagramConnectionAgent.java new file mode 100644 index 000000000..5c0c81d4d --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/agents/DiagramConnectionAgent.java @@ -0,0 +1,26 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.agents; + +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.informants.DiagramAsModelInformant; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.informants.DiagramTextInformant; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.informants.LinkBetweenDeAndRiProbabilityFilter; + +/** + * This agent is responsible for creating and filtering the {@link edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi} trace links. + */ +public class DiagramConnectionAgent extends PipelineAgent { + /** + * Sole constructor of the agent. Runs {@link DiagramAsModelInformant}, {@link DiagramTextInformant}, and {@link LinkBetweenDeAndRiProbabilityFilter} in + * that order. + * + * @param dataRepository the data repository that should be used + */ + public DiagramConnectionAgent(DataRepository dataRepository) { + super(List.of(new DiagramAsModelInformant(dataRepository), new DiagramTextInformant(dataRepository), new LinkBetweenDeAndRiProbabilityFilter( + dataRepository)), DiagramConnectionAgent.class.getSimpleName(), dataRepository); + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/DiagramAsModelInformant.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/DiagramAsModelInformant.java new file mode 100644 index 000000000..9ff716c12 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/DiagramAsModelInformant.java @@ -0,0 +1,118 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.informants; + +import java.util.ArrayList; +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Box; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramUtil; +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.api.models.ModelInstance; +import edu.kit.kastel.mcse.ardoco.core.api.models.ModelInstanceImpl; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationState; +import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.data.ProjectPipelineData; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionState; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; +import edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi; + +/** + * This informant creates temporary {@link ModelInstance ModelInstances} from each diagram element and uses ArDoCo's SAD SAM TLR approach, but instead of + * creating a SAD SAM trace link for a temporary model instance and recommended instance, a {@link LinkBetweenDeAndRi} is created for the source of the + * temporary model instance. The temporary model instances are created using the + * {@link edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.agents.DiagramReferenceAgent diagram element references}. + */ +public class DiagramAsModelInformant extends Informant { + @Configurable + private int minSimilarSurfaceWords = 1; + private String projectName; + + public DiagramAsModelInformant(DataRepository dataRepository) { + super(DiagramAsModelInformant.class.getSimpleName(), dataRepository); + } + + @Override + public void process() { + var dataRepository = getDataRepository(); + this.projectName = dataRepository.getData(ProjectPipelineData.ID, ProjectPipelineData.class).orElseThrow().getProjectName(); + var diagramState = dataRepository.getData(DiagramRecognitionState.ID, DiagramRecognitionState.class).orElseThrow(); + var recommendationStates = DataRepositoryHelper.getRecommendationStates(dataRepository); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + for (var mm : Metamodel.values()) { + var recommendationState = recommendationStates.getRecommendationState(mm); + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(mm); + + findTextOfDiagramInstancesInSupposedMappings(diagramState, recommendationState, diagramConnectionState); + createLinksForEqualOrSimilarRecommendedInstances(diagramState, recommendationState, diagramConnectionState); + } + } + + private void findTextOfDiagramInstancesInSupposedMappings(DiagramRecognitionState diagramState, RecommendationState recommendationState, + DiagramConnectionState diagramConnectionState) { + var basedOnIncreasingMinimalProportionalThreshold = new ArrayList(); + var recommendedInstances = recommendationState.getRecommendedInstances(); + var diagrams = diagramState.getDiagrams(); + for (Diagram diagram : diagrams) { + var diagramModelInstances = diagramToModelInstances(diagram); + for (Pair pair : diagramModelInstances) { + var mostLikelyRi = getMetaData().getSimilarityUtils().getMostRecommendedInstancesToInstanceByReferences(pair.second(), recommendedInstances); + for (var recommendedInstance : mostLikelyRi) { + basedOnIncreasingMinimalProportionalThreshold.add(new LinkBetweenDeAndRi(recommendedInstance, pair.first(), projectName, this, DiagramUtil + .calculateSimilarityMap(getMetaData().getWordSimUtils(), pair.first(), recommendedInstance))); + } + } + } + logger.debug("Found {} diagram links based on increasing minimal proportional threshold", basedOnIncreasingMinimalProportionalThreshold.size()); + basedOnIncreasingMinimalProportionalThreshold.forEach(diagramConnectionState::addToLinksBetweenDeAndRi); + } + + private void createLinksForEqualOrSimilarRecommendedInstances(DiagramRecognitionState diagramState, RecommendationState recommendationState, + DiagramConnectionState diagramConnectionState) { + var basedOnOverallSimilarity = new ArrayList(); + var basedOnSurfaceWords = new ArrayList(); + var diagrams = diagramState.getDiagrams(); + for (Diagram diagram : diagrams) { + var diagramModelInstances = diagramToModelInstances(diagram); + var ris = recommendationState.getRecommendedInstances(); + for (var recommendedInstance : ris) { + for (Pair pair : diagramModelInstances) { + //Add based on overall similarity + if (getMetaData().getSimilarityUtils().isRecommendedInstanceSimilarToModelInstance(recommendedInstance, pair.second())) { + basedOnOverallSimilarity.add(new LinkBetweenDeAndRi(recommendedInstance, pair.first(), projectName, this, DiagramUtil + .calculateSimilarityMap(getMetaData().getWordSimUtils(), pair.first(), recommendedInstance))); + } + //Add based on surface words + var similarSurfaceWords = getMetaData().getSimilarityUtils().getSimilarSurfaceWords(recommendedInstance, pair.second()); + if (similarSurfaceWords.size() >= minSimilarSurfaceWords) { + basedOnSurfaceWords.add(new LinkBetweenDeAndRi(recommendedInstance, pair.first(), projectName, this, DiagramUtil.calculateSimilarityMap( + getMetaData().getWordSimUtils(), pair.first(), recommendedInstance))); + } + } + } + } + logger.debug("Found {} diagram links based on overall similarity", basedOnOverallSimilarity.size()); + logger.debug("Found {} diagram links based on surface words", basedOnSurfaceWords.size()); + basedOnOverallSimilarity.forEach(diagramConnectionState::addToLinksBetweenDeAndRi); + basedOnSurfaceWords.forEach(diagramConnectionState::addToLinksBetweenDeAndRi); + } + + private List> diagramToModelInstances(Diagram diagram) { + //Create model instances so we can reuse a lot of code + var instances = new ArrayList>(); + + var boxes = diagram.getBoxes(); + for (Box box : boxes) { + var names = box.getReferences(); + + names.forEach(name -> instances.add(new Pair<>(box, new ModelInstanceImpl(name, "", Integer.toString(name.hashCode()))))); + } + + return instances; + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/DiagramTextInformant.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/DiagramTextInformant.java new file mode 100644 index 000000000..5315d4b77 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/DiagramTextInformant.java @@ -0,0 +1,81 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.informants; + +import java.util.Locale; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramUtil; +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationState; +import edu.kit.kastel.mcse.ardoco.core.common.util.AbbreviationDisambiguationHelper; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.data.ProjectPipelineData; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionState; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; + +/** + * This informant searches for text in diagram elements that is an initialism of a recommended instances name and creates a + * {@link edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi} between them. + */ +public class DiagramTextInformant extends Informant { + /** + * The percentage of characters in a word that need to be uppercase for a word to be considered an initialism candidate. + */ + @Configurable + private double initialismThreshold = 0.5; + private String projectName; + + public DiagramTextInformant(DataRepository dataRepository) { + super(DiagramTextInformant.class.getSimpleName(), dataRepository); + } + + @Override + public void process() { + var dataRepository = getDataRepository(); + this.projectName = dataRepository.getData(ProjectPipelineData.ID, ProjectPipelineData.class).orElseThrow().getProjectName(); + var diagramState = dataRepository.getData(DiagramRecognitionState.ID, DiagramRecognitionState.class).orElseThrow(); + var recommendationStates = DataRepositoryHelper.getRecommendationStates(dataRepository); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + for (var mm : Metamodel.values()) { + var recommendationState = recommendationStates.getRecommendationState(mm); + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(mm); + + createLinksBasedOnDiagramElements(diagramState, recommendationState, diagramConnectionState); + } + } + + /** + * Uses {@link AbbreviationDisambiguationHelper#isInitialismOf(String, String, double)} to determine if the text in a + * {@link edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement} {@link edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.TextBox} is + * an initialism of a recommended instances name. For example, consider a diagram element with the text box "SSM" and a recommended instance with "Secure + * Storage Manager". The function identifies "SSM" as an initialism of the recommended instance name and thus creates a trace link between them. + * + * @param diagramState the diagram recognition state + * @param recommendationState the recommendation state + * @param diagramConnectionState the diagram connection state + */ + private void createLinksBasedOnDiagramElements(DiagramRecognitionState diagramState, RecommendationState recommendationState, + DiagramConnectionState diagramConnectionState) { + var diagrams = diagramState.getDiagrams(); + for (Diagram diagram : diagrams) { + var boxes = diagram.getBoxes(); + for (var box : boxes) { + var texts = box.getTexts(); + for (var tBox : texts) { + var ris = recommendationState.getRecommendedInstances(); + for (var recommendedInstance : ris) { + if (AbbreviationDisambiguationHelper.isInitialismOf(recommendedInstance.getName().toLowerCase(Locale.ENGLISH), tBox.getText() + .toLowerCase(Locale.ENGLISH), initialismThreshold)) { + diagramConnectionState.addToLinksBetweenDeAndRi(recommendedInstance, box, projectName, this, DiagramUtil.calculateSimilarityMap( + getMetaData().getWordSimUtils(), box, recommendedInstance)); + } + } + } + } + } + } +} diff --git a/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/LinkBetweenDeAndRiProbabilityFilter.java b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/LinkBetweenDeAndRiProbabilityFilter.java new file mode 100644 index 000000000..87a346aa0 --- /dev/null +++ b/stages/diagram-connection-generator/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagramconnectiongenerator/informants/LinkBetweenDeAndRiProbabilityFilter.java @@ -0,0 +1,48 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.informants; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions; +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionState; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; + +/** + * This informant filters the diagram elements and recommended instances that are below a configurable confidence threshold. + */ +public class LinkBetweenDeAndRiProbabilityFilter extends Informant { + @Configurable + private double confidenceThreshold = 0.75; + + public LinkBetweenDeAndRiProbabilityFilter(DataRepository dataRepository) { + super(LinkBetweenDeAndRiProbabilityFilter.class.getSimpleName(), dataRepository); + } + + @Override + public void process() { + var dataRepository = getDataRepository(); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + for (var mm : Metamodel.values()) { + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(mm); + + filterByProbability(diagramConnectionState); + } + } + + /** + * Calculates the overall confidence in each trace link using {@link AggregationFunctions#MAX} and removes all below a certain threshold. + * + * @param diagramConnectionState the diagram connection state + */ + private void filterByProbability(DiagramConnectionState diagramConnectionState) { + var belowThreshold = diagramConnectionState.getLinksBetweenDeAndRi() + .stream() + .filter(linkBetweenDeAndRi -> linkBetweenDeAndRi.getConfidence(AggregationFunctions.MAX) < confidenceThreshold) + .toList(); + logger.info("Removed {} Diagram Links due to low confidence", belowThreshold.size()); + belowThreshold.forEach(b -> logger.info(b.toString())); + belowThreshold.forEach(diagramConnectionState::removeFromLinksBetweenDeAndRi); + } +} diff --git a/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/ConfigurationTest.java b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/ConfigurationTest.java new file mode 100644 index 000000000..77c538c0c --- /dev/null +++ b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/ConfigurationTest.java @@ -0,0 +1,31 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import edu.kit.kastel.mcse.ardoco.core.tests.eval.ConfigurationTestBase; + +public class ConfigurationTest extends ConfigurationTestBase { + @Override + protected void assertFalse(boolean result, String message) { + Assertions.assertFalse(result, message); + } + + @Override + protected void fail(String message) { + Assertions.fail(message); + } + + @Override + @Test + public void showCurrentConfiguration() throws Exception { + super.showCurrentConfiguration(); + } + + @Override + @Test + public void testValidityOfConfigurableFields() { + super.testValidityOfConfigurableFields(); + } +} diff --git a/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/PreTestRunner.java b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/PreTestRunner.java new file mode 100644 index 000000000..c5bd2694d --- /dev/null +++ b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/PreTestRunner.java @@ -0,0 +1,69 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData; +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.ParameterizedRunner; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; +import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; +import edu.kit.kastel.mcse.ardoco.core.textextraction.DiagramBackedTextStateStrategy; +import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; +import edu.kit.kastel.mcse.ardoco.core.textextraction.TextStateImpl; +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.DiagramRecognitionMock; +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +public class PreTestRunner extends ParameterizedRunner { + public PreTestRunner(String projectName, Parameters parameters) { + super(projectName, parameters); + } + + public record Parameters(DiagramProject diagramProject, File outputDir, boolean useMockDiagrams) { + } + + @Override + public List initializePipelineSteps(Parameters p) throws IOException { + var pipelineSteps = new ArrayList(); + + ArDoCo arDoCo = getArDoCo(); + var dataRepository = arDoCo.getDataRepository(); + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + + var text = CommonUtilities.readInputText(p.diagramProject.getTextFile()); + if (text.isBlank()) { + throw new IllegalArgumentException("Cannot deal with empty input text. Maybe there was an " + "error reading the file."); + } + DataRepositoryHelper.putInputText(dataRepository, text); + pipelineSteps.add(TextPreprocessingAgent.get(p.diagramProject.getAdditionalConfigurations(), dataRepository)); + + ArCoTLModelProviderAgent arCoTLModelProviderAgent = ArCoTLModelProviderAgent.get(p.diagramProject.getModelFile(), ArchitectureModelType.PCM, null, + p.diagramProject.getAdditionalConfigurations(), dataRepository); + pipelineSteps.add(arCoTLModelProviderAgent); + + if (p.useMockDiagrams) { + pipelineSteps.add(new DiagramRecognitionMock(p.diagramProject, p.diagramProject.getAdditionalConfigurations(), dataRepository)); + } else { + dataRepository.addData(InputDiagramData.ID, new InputDiagramData(p.diagramProject.getDiagramData())); + pipelineSteps.add(DiagramRecognition.get(p.diagramProject.getAdditionalConfigurations(), dataRepository)); + } + + var diagramBackedTextStateStrategy = new DiagramBackedTextStateStrategy(dataRepository); + var textState = new TextStateImpl(diagramBackedTextStateStrategy); + dataRepository.addData(TextState.ID, textState); + pipelineSteps.add(TextExtraction.get(p.diagramProject.getAdditionalConfigurations(), dataRepository)); + + pipelineSteps.add(RecommendationGenerator.get(p.diagramProject.getAdditionalConfigurations(), dataRepository)); + return pipelineSteps; + } +} diff --git a/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/Results.java b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/Results.java new file mode 100644 index 000000000..dc831b8b3 --- /dev/null +++ b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/Results.java @@ -0,0 +1,242 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramGoldStandardTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramTextTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramWordTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.text.Text; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +public record Results(DiagramProject project, SortedSet truePositives, SortedSet falsePositives, + SortedSet falseNegatives, long TN, ExpectedResults expectedResults, SortedSet all) + implements Serializable { + + private static final Logger logger = LoggerFactory.getLogger(Results.class); + + public static Results create(GlobalConfiguration globalConfiguration, DiagramProject project, Text text, Set wordTraceLinks, + ExpectedResults expected) { + var allGoldStandardTraceLinks = project.getDiagramTraceLinksAsMap(text.getSentences().toList()); + TreeSet goldStandard = new TreeSet<>(allGoldStandardTraceLinks.entrySet() + .stream() + .filter(e -> e.getKey().isActualPositive()) + .flatMap(e -> e.getValue().stream()) + .toList()); + + var totalSentences = text.getSentences().size(); + var totalDiagramElements = project.getDiagramsGoldStandard().stream().flatMap(d -> d.getBoxes().stream()).toList().size(); + var total = totalSentences * totalDiagramElements; + var traceLinks = new TreeSet<>(wordTraceLinks); + var tpLinks = intersection(globalConfiguration, traceLinks, goldStandard); + var fpLinks = difference(globalConfiguration, traceLinks, goldStandard); + fpLinks.forEach(fp -> fp.addRelated(allGoldStandardTraceLinks.values() + .stream() + .flatMap(Collection::stream) + .filter(oth -> fp.similar(globalConfiguration, oth)) + .toList())); + var fnLinks = difference(globalConfiguration, goldStandard, traceLinks); + var TP = tpLinks.size(); + var FP = fpLinks.size(); + var FN = fnLinks.size(); + var TN = total - TP - FP - FN; + + return new Results(project, tpLinks, fpLinks, fnLinks, TN, expected, traceLinks); + } + + private static BigDecimal toBD(double a) { + if (Double.isNaN(a)) + return BigDecimal.valueOf(-1337); + return BigDecimal.valueOf(a).setScale(2, RoundingMode.HALF_UP); + } + + public static TreeSet intersection(GlobalConfiguration globalConfiguration, Set a, + Set b) { + return a.stream() + .filter(fromA -> b.stream().anyMatch(fromB -> fromB.similar(globalConfiguration, fromA))) + .collect(Collectors.toCollection(TreeSet::new)); + } + + public static TreeSet difference(GlobalConfiguration globalConfiguration, Set a, + Set b) { + return a.stream() + .filter(fromA -> b.stream().noneMatch(fromB -> fromB.similar(globalConfiguration, fromA))) + .collect(Collectors.toCollection(TreeSet::new)); + } + + public double precision() { + return TP() / (double) (TP() + FP()); + } + + public double deltaPrecision() { + return toBD(precision()).subtract(toBD(expectedResults().precision())).doubleValue(); + } + + public double recall() { + return TP() / (double) (TP() + FN()); + } + + public double deltaRecall() { + return toBD(recall()).subtract(toBD(expectedResults().recall())).doubleValue(); + } + + public double f1() { + return 2 * TP() / (double) (2 * TP() + FP() + FN()); + } + + public double deltaF1() { + return toBD(f1()).subtract(toBD(expectedResults().f1())).doubleValue(); + } + + public double accuracy() { + return (TP() + TN()) / (double) (TP() + TN() + FP() + FN()); + } + + public double deltaAccuracy() { + return toBD(accuracy()).subtract(toBD(expectedResults().accuracy())).doubleValue(); + } + + public double specificity() { + return TN() / (double) (TN() + FP()); + } + + public double deltaSpecificity() { + return toBD(specificity()).subtract(toBD(expectedResults().specificity())).doubleValue(); + } + + public double phiCoefficient() { + return (TP() * TN() - FP() * FN()) / Math.sqrt(approachPositives() * approachNegatives() * goldStandardPositives() * goldStandardNegatives()); + } + + public double deltaPhiCoefficient() { + return toBD(phiCoefficient()).subtract(toBD(expectedResults().phiCoefficient())).doubleValue(); + } + + public double phiNormalized() { + var R1 = Math.sqrt(approachPositives() * goldStandardNegatives()); + var R2 = Math.sqrt(goldStandardPositives() * approachNegatives()); + var phiMax = goldStandardPositives() >= approachPositives() ? R1 / R2 : R2 / R1; + return phiCoefficient() / phiMax; + } + + public long TP() { + return truePositives.size(); + } + + public long FP() { + return falsePositives.size(); + } + + public long FN() { + return falseNegatives.size(); + } + + /** + * {@return Number of positives in the gold standard} + */ + public long goldStandardPositives() { + return TP() + FN(); + } + + /** + * {@return Number of positives generated by the approach} + */ + public long approachPositives() { + return TP() + FP(); + } + + /** + * {@return Number of negatives in the gold standard} + */ + public long goldStandardNegatives() { + return TN() + FP(); + } + + /** + * {@return Number of negatives generated by the approach} + */ + public long approachNegatives() { + return TN() + FN(); + } + + public long totalPredictions() { + return approachPositives() + approachNegatives(); + } + + @Override + public String toString() { + return String.format(Locale.US, + "TP:%d, FP:%d, TN:%d, FN:%d, P:%.2f(%.2f), R:%.2f(%.2f), F1:%.2f(%.2f), Acc:%.2f(%.2f), Spec:%.2f(%.2f), φ:%.2f(%.2f), φN:%.2f", TP(), FP(), + TN(), FN(), precision(), deltaPrecision(), recall(), deltaRecall(), f1(), deltaF1(), accuracy(), deltaAccuracy(), specificity(), + deltaSpecificity(), phiCoefficient(), deltaPhiCoefficient(), phiNormalized()); + } + + public boolean asExpected() { + var asExpected = true; + if (toBD(expectedResults().precision()).compareTo(toBD(precision())) > 0) + asExpected = false; + if (toBD(expectedResults().recall()).compareTo(toBD(recall())) > 0) + asExpected = false; + if (toBD(expectedResults().f1()).compareTo(toBD(f1())) > 0) + asExpected = false; + if (toBD(expectedResults().accuracy()).compareTo(toBD(accuracy())) > 0) + asExpected = false; + if (toBD(expectedResults().phiCoefficient()).compareTo(toBD(phiCoefficient())) > 0) + asExpected = false; + if (toBD(expectedResults().specificity()).compareTo(toBD(specificity())) > 0) + asExpected = false; + if (asExpected) { + logger.info("Results are as expected. " + toString()); + } else { + logger.warn("Results are not as expected! " + toString()); + } + return asExpected; + } + + public Map mapOfMetrics() { + var map = new LinkedHashMap(); + map.put("P", precision()); + map.put("R", recall()); + map.put("F1", f1()); + map.put("Acc", accuracy()); + map.put("Spec", specificity()); + map.put("Phi", phiCoefficient()); + map.put("PhiN", phiNormalized()); + return map; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof Results other) { + return Objects.equals(this.TN(), other.TN()) && Objects.equals(this.truePositives(), other.truePositives()) && Objects.equals(this.falsePositives(), + other.falsePositives()) && Objects.equals(this.falseNegatives(), other.falseNegatives()); + } + return false; + } + + public boolean equalsByConfusionMatrix(Results other) { + if (other == null) + return false; + if (this == other) + return true; + return Objects.equals(this.TN(), other.TN()) && Objects.equals(this.truePositives().size(), other.truePositives().size()) && Objects.equals(this + .falsePositives() + .size(), other.falsePositives().size()) && Objects.equals(this.falseNegatives().size(), other.falseNegatives().size()); + } + + @Override + public int hashCode() { + return Objects.hash(this.TN, this.truePositives(), this.falsePositives(), this.falseNegatives()); + } +} diff --git a/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramConnectionGeneratorEvaluationIT.java b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramConnectionGeneratorEvaluationIT.java new file mode 100644 index 000000000..01396d5f6 --- /dev/null +++ b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramConnectionGeneratorEvaluationIT.java @@ -0,0 +1,97 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.integration; + +import java.io.File; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; +import edu.kit.kastel.mcse.ardoco.tests.PreTestRunner; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +/** + * This class is used for evaluating ERID's diagram-to-sentences TLR capabilities using the automatically extracted diagrams + * ({@link edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition DiagramRecognition}). + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DiagramConnectionGeneratorEvaluationIT extends DiagramConnectionGeneratorTest { + private static final Logger logger = LoggerFactory.getLogger(DiagramConnectionGeneratorEvaluationIT.class); + private static final String OUTPUT_DIR = "src/test/resources/testout"; + + @Override + protected ExpectedResults getExpectedResults(DiagramProject project) { + return project.getExpectedDiagramSentenceTlrResults(); + } + + @Override + protected DataRepository runPreTestRunner(DiagramProject project) { + logger.info("Run PreTestRunner for {}", project.name()); + var params = new PreTestRunner.Parameters(project, new File(OUTPUT_DIR), false); + var runner = new PreTestRunner(project.name(), params); + return runner.runWithoutSaving(); + } + + @Override + @Test + @Disabled + void evaluateAll() { + super.evaluateAll(); + } + + @Override + @Test + @Disabled + void teammatesTest() { + runComparable(DiagramProject.TEAMMATES, false); + } + + @Override + @Test + @Disabled + void teammatesHistTest() { + runComparable(DiagramProject.TEAMMATES_HISTORICAL, false); + } + + @Override + @Test + @Disabled + void teastoreTest() { + runComparable(DiagramProject.TEASTORE, false); + } + + @Override + @Test + @Disabled + void teastoreHistTest() { + runComparable(DiagramProject.TEASTORE_HISTORICAL, false); + } + + @Override + @Test + @Disabled + void bbbTest() { + runComparable(DiagramProject.BIGBLUEBUTTON, false); + } + + @Override + @Test + @Disabled + void bbbHistTest() { + runComparable(DiagramProject.BIGBLUEBUTTON_HISTORICAL, false); + } + + @Override + @Test + @Disabled + void msTest() { + runComparable(DiagramProject.MEDIASTORE, false); + } +} diff --git a/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramConnectionGeneratorTest.java b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramConnectionGeneratorTest.java new file mode 100644 index 000000000..a3bf6c690 --- /dev/null +++ b/stages/diagram-connection-generator/src/test/java/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramConnectionGeneratorTest.java @@ -0,0 +1,233 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.integration; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.eclipse.collections.impl.factory.SortedMaps; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.api.PreprocessingData; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.TraceType; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.DiagramConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.tests.PreTestRunner; +import edu.kit.kastel.mcse.ardoco.tests.Results; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; +import edu.kit.kastel.mcse.ardoco.tests.eval.StageTest; + +/** + * This class is used for evaluating ERID's diagram-to-sentences TLR capabilities using the manually extracted diagrams + * ({@link edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.DiagramRecognitionMock DiagramRecognitionMock}). + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DiagramConnectionGeneratorTest extends StageTest { + private static final Logger logger = LoggerFactory.getLogger(DiagramConnectionGeneratorTest.class); + private static final String OUTPUT_DIR = "src/test/resources/testout"; + + public DiagramConnectionGeneratorTest() { + super(new DiagramConnectionGenerator(SortedMaps.mutable.empty(), new DataRepository()), DiagramProject.values()); + } + + protected ExpectedResults getExpectedResults(DiagramProject project) { + return project.getExpectedDiagramSentenceTlrResultsWithMock(); + } + + @Override + protected Results runComparable(DiagramProject project, SortedMap additionalConfigurations, boolean cachePreRun) { + var dataRepository = run(project, additionalConfigurations, cachePreRun); + var text = dataRepository.getData(PreprocessingData.ID, PreprocessingData.class).orElseThrow().getText(); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(project.getMetamodel()); + var linksBetweenDeAndRi = diagramConnectionState.getLinksBetweenDeAndRi().stream().sorted().collect(Collectors.toCollection(TreeSet::new)); + var traceLinks = new TreeSet<>(diagramConnectionState.getTraceLinks()); + var mostSpecificTraceLinks = new TreeSet<>(diagramConnectionState.getMostSpecificTraceLinks()); + var globalConfiguration = dataRepository.getGlobalConfiguration(); + var altResult = Results.create(globalConfiguration, project, text, mostSpecificTraceLinks, getExpectedResults(project)); + + var commonNoun = altResult.falsePositives().stream().filter(w -> { + var related = w.getRelatedGSLinks(); + return !related.isEmpty() && related.stream().allMatch(g -> g.getTraceType().equals(TraceType.COMMON_NOUN)); + }).toList(); + var sharedStem = altResult.falsePositives().stream().filter(w -> { + var related = w.getRelatedGSLinks(); + return !related.isEmpty() && related.stream().allMatch(g -> g.getTraceType().equals(TraceType.SHARED_STEM)); + }).toList(); + var otherEntity = altResult.falsePositives().stream().filter(w -> { + var related = w.getRelatedGSLinks(); + return !related.isEmpty() && related.stream().allMatch(g -> g.getTraceType().equals(TraceType.OTHER_ENTITY)); + }).toList(); + var coreference = altResult.falseNegatives().stream().filter(w -> w.getTraceType().equals(TraceType.ENTITY_COREFERENCE)).toList(); + + logger.debug( + "{} Diagram Links, {} Trace Links, {} Most Specific Trace Links, {} Common Noun " + "FP, " + "{} Shared Stem FP, {} Other Entity FP, {} Coreference FN", + linksBetweenDeAndRi.size(), traceLinks.size(), mostSpecificTraceLinks.size(), commonNoun.size(), sharedStem.size(), otherEntity.size(), + coreference.size()); + + var cacheID = "Results-" + project.name(); + var prevResults = getCached(cacheID, Results.class); + + if (prevResults != null) { + var dAll = Results.difference(globalConfiguration, altResult.all(), prevResults.all()); + var dTP = Results.difference(globalConfiguration, altResult.truePositives(), prevResults.truePositives()); + var dFP = Results.difference(globalConfiguration, altResult.falsePositives(), prevResults.falsePositives()); + var dFN = Results.difference(globalConfiguration, altResult.falseNegatives(), prevResults.falseNegatives()); + var dTN = Results.difference(globalConfiguration, prevResults.falsePositives(), altResult.falsePositives()); + } + + if (!altResult.equalsByConfusionMatrix(prevResults)) + debugCacheIfCachingFlag(cacheID, altResult); + + return altResult; + } + + @Override + protected DataRepository runPreTestRunner(DiagramProject project) { + logger.info("Run PreTestRunner for {}", project.name()); + var params = new PreTestRunner.Parameters(project, new File(OUTPUT_DIR), true); + var runner = new PreTestRunner(project.name(), params); + return runner.runWithoutSaving(); + } + + @Override + protected DataRepository runTestRunner(DiagramProject project, SortedMap additionalConfigurations, DataRepository preRunDataRepository) { + logger.info("Run TestRunner for {}", project.name()); + var combinedConfigs = new TreeMap<>(project.getAdditionalConfigurations()); + combinedConfigs.putAll(additionalConfigurations); + return new AnonymousRunner(project.name(), preRunDataRepository) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) { + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + var pipelineSteps = new ArrayList(); + pipelineSteps.add(new DiagramConnectionGenerator(combinedConfigs, dataRepository)); + return pipelineSteps; + } + }.runWithoutSaving(); + } + + @DisplayName("Evaluate Diagram Connection Generator") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + void evaluateNonHistoricalDiagramRecognition(DiagramProject project) { + assertTrue(runComparable(project).asExpected()); + } + + @DisplayName("Evaluate Diagram Connection Generator (Historical)") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(2) + void evaluateHistoricalDiagramRecognition(DiagramProject project) { + assertTrue(runComparable(project).asExpected()); + } + + @Disabled + @Test + void evaluateAll() { + var projects = new ArrayList<>(DiagramProject.getNonHistoricalProjects()); + projects.addAll(DiagramProject.getHistoricalProjects()); + var results = new ArrayList(); + for (var project : projects) { + results.add(runComparable(project)); + } + var avg = new LinkedHashMap(); + var avgWeighted = new LinkedHashMap(); + var totalGoldStandardPositives = 0L; + for (var result : results) { + totalGoldStandardPositives += result.goldStandardPositives(); + } + for (var result : results) { + var weight = result.goldStandardPositives() / (double) totalGoldStandardPositives; + var metrics = result.mapOfMetrics(); + for (var metric : metrics.entrySet()) { + var key = metric.getKey(); + var value = metric.getValue(); + var weightedByGoldStandardAndAmountOfProjects = value * weight; + var weightedByAmountOfProjects = value / projects.size(); + var oldAvg = avg.getOrDefault(key, 0.0); + var oldWeighted = avgWeighted.getOrDefault(key, 0.0); + avg.put(key, oldAvg + weightedByAmountOfProjects); + avgWeighted.put(key, oldWeighted + weightedByGoldStandardAndAmountOfProjects); + } + } + System.out.println("Project & " + String.join(" & ", avg.keySet()) + "\\\\"); + results.stream() + .map(r -> r.project().getAlias() + " & " + r.mapOfMetrics() + .values() + .stream() + .map(d -> String.format(Locale.ENGLISH, "%.2f", d)) + .collect(Collectors.joining(" & ")) + "\\\\") + .forEach(System.out::println); + System.out.println("Average & " + avg.values().stream().map(d -> String.format(Locale.ENGLISH, "%.2f", d)).collect(Collectors.joining(" & ")) + "\\\\"); + System.out.println("w. Average & " + avgWeighted.values() + .stream() + .map(d -> String.format(Locale.ENGLISH, "%.2f", d)) + .collect(Collectors.joining(" & ")) + "\\\\"); + } + + @Disabled + @Test + void teammatesTest() { + runComparable(DiagramProject.TEAMMATES, false); + } + + @Disabled + @Test + void teammatesHistTest() { + runComparable(DiagramProject.TEAMMATES_HISTORICAL, false); + } + + @Disabled + @Test + void teastoreTest() { + runComparable(DiagramProject.TEASTORE, false); + } + + @Disabled + @Test + void teastoreHistTest() { + runComparable(DiagramProject.TEASTORE_HISTORICAL, false); + } + + @Disabled + @Test + void bbbTest() { + runComparable(DiagramProject.BIGBLUEBUTTON, false); + } + + @Disabled + @Test + void bbbHistTest() { + runComparable(DiagramProject.BIGBLUEBUTTON_HISTORICAL, false); + } + + @Disabled + @Test + void msTest() { + runComparable(DiagramProject.MEDIASTORE, false); + } +} diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/DiagramConsistency.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/DiagramConsistency.java index 784a1bfa6..c54fa2232 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/DiagramConsistency.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/DiagramConsistency.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency; import java.io.File; @@ -19,7 +19,7 @@ import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.agents.DiagramProviderAgent; import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; /** * A runner that checks the consistency of a diagram and the represented architecture/code model. diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramElementOccurrenceFinderInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramElementOccurrenceFinderInformant.java index d2c0632d2..cd6b93827 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramElementOccurrenceFinderInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramElementOccurrenceFinderInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.Set; @@ -72,7 +72,7 @@ public DiagramElementOccurrenceFinderInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { if (this.skip) { return; } diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelInconsistencyInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelInconsistencyInformant.java index af25c0e23..e6de3b3a6 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelInconsistencyInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelInconsistencyInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.ArrayList; @@ -107,7 +107,7 @@ private static void checkRulesForLooseEntities(Map entities, Con } @Override - public void run() { + public void process() { DataRepository data = this.getDataRepository(); ModelStates models = data.getData(ModelStates.ID, ModelStates.class).orElse(null); diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelLinkInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelLinkInformant.java index 8398e43fc..b4e37d7e0 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelLinkInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramModelLinkInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.Map; @@ -79,7 +79,7 @@ public DiagramModelLinkInformant(DataRepository data) { } @Override - public void run() { + public void process() { if (this.skip) { return; } diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramProviderInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramProviderInformant.java index 6bd0d7d6f..e119d1002 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramProviderInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramProviderInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.io.File; @@ -9,9 +9,14 @@ import java.util.Optional; import java.util.SortedMap; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JsonMappingException; + import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.DiagramState; -import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.JsonMapping; import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram; +import edu.kit.kastel.mcse.ardoco.core.common.JsonHandling; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.DiagramStateImpl; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -51,14 +56,28 @@ public static Diagram load(File file) throws IOException { try (InputStream stream = new FileInputStream(file)) { String text = new String(stream.readAllBytes(), StandardCharsets.UTF_8); - diagram = JsonMapping.OBJECT_MAPPER.readValue(text, DiagramImpl.class); + var oom = JsonHandling.createObjectMapper(); + oom.setInjectableValues(new InjectableValues() { + @Override + public Object findInjectableValue(Object o, DeserializationContext deserializationContext, BeanProperty beanProperty, Object o1) + throws JsonMappingException { + if (beanProperty.getType().getRawClass() != Diagram.class) + throw new JsonMappingException(deserializationContext.getParser(), "Could not inject value into " + beanProperty.getName()); + Object parent = deserializationContext.getParser().getParsingContext().getParent().getCurrentValue(); + if (!(parent instanceof DiagramImpl parentDiagram)) + throw new JsonMappingException(deserializationContext.getParser(), "Could not inject value into " + beanProperty.getName()); + return parentDiagram; + } + }); + + diagram = oom.readValue(text, DiagramImpl.class); } return diagram; } @Override - public void run() { + public void process() { Diagram diagram = null; try { diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyGroupingInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyGroupingInformant.java index 157509a4b..12838f4b0 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyGroupingInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyGroupingInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.ArrayList; @@ -30,7 +30,7 @@ public InconsistencyGroupingInformant(DataRepository data) { } @Override - public void run() { + public void process() { DataRepository data = this.getDataRepository(); DiagramMatchingModelSelectionState selection = data.getData(DiagramMatchingModelSelectionState.ID, DiagramMatchingModelSelectionStateImpl.class) diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyRefinementInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyRefinementInformant.java index 454134377..3307ac6d2 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyRefinementInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/InconsistencyRefinementInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.ArrayList; @@ -63,7 +63,7 @@ private static Map getEntityNames(ModelStates models, ModelType } @Override - public void run() { + public void process() { DataRepository data = this.getDataRepository(); ModelStates models = data.getData(ModelStates.ID, ModelStates.class).orElse(null); diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/OccurrenceToDecisionInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/OccurrenceToDecisionInformant.java index 76d3e4364..272c170d8 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/OccurrenceToDecisionInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/OccurrenceToDecisionInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.LinkedHashMap; @@ -49,7 +49,7 @@ public OccurrenceToDecisionInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { if (this.skip) { return; } diff --git a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/WeightedSimilarityInformant.java b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/WeightedSimilarityInformant.java index 5f86e08d1..ba3630345 100644 --- a/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/WeightedSimilarityInformant.java +++ b/stages/diagram-consistency/src/main/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/WeightedSimilarityInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; import java.util.Map; @@ -39,7 +39,7 @@ public WeightedSimilarityInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { if (this.skip) { return; } diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/DiagramLoadingTest.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/DiagramLoadingTest.java index 9c2611b0f..48bdc33f9 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/DiagramLoadingTest.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/DiagramLoadingTest.java @@ -1,10 +1,17 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation; -import static org.junit.jupiter.api.Assertions.*; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Queue; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -12,12 +19,18 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JsonMappingException; + import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.Extractions; -import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.JsonMapping; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram; import edu.kit.kastel.mcse.ardoco.core.api.models.Entity; import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.architecture.ArchitectureComponent; import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.architecture.ArchitectureItem; import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.code.CodeItem; +import edu.kit.kastel.mcse.ardoco.core.common.JsonHandling; import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data.DiagramProject; import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data.stage1.Element; import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data.stage1.ElementIdentification; @@ -54,7 +67,20 @@ void loadDiagram(DiagramProject project) throws IOException { String text = project.getDiagram(); assertDoesNotThrow(() -> { - var diagram = JsonMapping.OBJECT_MAPPER.readValue(text, DiagramImpl.class); + var oom = JsonHandling.createObjectMapper(); + oom.setInjectableValues(new InjectableValues() { + @Override + public Object findInjectableValue(Object o, DeserializationContext deserializationContext, BeanProperty beanProperty, Object o1) + throws JsonMappingException { + if (beanProperty.getType().getRawClass() != Diagram.class) + throw new JsonMappingException(deserializationContext.getParser(), "Could not inject value into " + beanProperty.getName()); + Object parent = deserializationContext.getParser().getParsingContext().getParent().getCurrentValue(); + if (!(parent instanceof DiagramImpl parentDiagram)) + throw new JsonMappingException(deserializationContext.getParser(), "Could not inject value into " + beanProperty.getName()); + return parentDiagram; + } + }); + var diagram = oom.readValue(text, DiagramImpl.class); assertNotNull(diagram); }); } @@ -68,7 +94,7 @@ void loadStageOne(DiagramProject project) throws IOException { AtomicReference identification = new AtomicReference<>(); assertDoesNotThrow(() -> { - identification.set(JsonMapping.OBJECT_MAPPER.readValue(text, ElementIdentification.class)); + identification.set(createObjectMapper().readValue(text, ElementIdentification.class)); assertNotNull(identification.get()); }); @@ -106,7 +132,7 @@ void loadStageTwo(DiagramProject project) throws IOException { String text = project.getLinkingStage(); assertDoesNotThrow(() -> { - var object = JsonMapping.OBJECT_MAPPER.readValue(text, ElementLinks.class); + var object = createObjectMapper().readValue(text, ElementLinks.class); assertNotNull(object); }); } @@ -118,7 +144,7 @@ void loadStageThree(DiagramProject project) throws IOException { String text = project.getValidationStage(); assertDoesNotThrow(() -> { - var object = JsonMapping.OBJECT_MAPPER.readValue(text, DiagramInconsistencies.class); + var object = createObjectMapper().readValue(text, DiagramInconsistencies.class); assertNotNull(object); }); } diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/EvaluationTestBase.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/EvaluationTestBase.java index 4c2a25c9c..74adda17d 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/EvaluationTestBase.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/EvaluationTestBase.java @@ -93,7 +93,7 @@ protected static List getDistinctDiagrams() { } protected static ArchitectureModel getArchitectureModel(DiagramProject project) { - File model = project.getSourceProject().getProject().getModelFile(ArchitectureModelType.UML); + File model = project.getSourceProject().getModelFile(ArchitectureModelType.UML); UmlExtractor extractor = new UmlExtractor(model.getAbsolutePath()); ArchitectureModel architectureModel = extractor.extractModel(); diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/MapMetrics.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/MapMetrics.java index be2b94962..76270dbfa 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/MapMetrics.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/MapMetrics.java @@ -1,10 +1,11 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation; +import java.util.IdentityHashMap; +import java.util.Map; import java.util.Objects; import org.eclipse.collections.api.bimap.MutableBiMap; -import org.eclipse.collections.impl.bimap.mutable.HashBiMap; /** * Metrics of the diagram-model matching process. @@ -20,7 +21,7 @@ * @param * The type of the model elements. */ -public record MapMetrics(MutableBiMap truePositives, MutableBiMap falsePositives, MutableBiMap falseNegatives) implements Metrics { +public record MapMetrics(Map truePositives, Map falsePositives, Map falseNegatives) implements Metrics { /** * Create a new metrics object from the expected and actual links. * @@ -34,10 +35,10 @@ public record MapMetrics(MutableBiMap truePositives, MutableBiMap MapMetrics from(MutableBiMap expected, MutableBiMap actual) { - MutableBiMap truePositives = new HashBiMap<>(); - MutableBiMap falsePositives = new HashBiMap<>(); - MutableBiMap falseNegatives = new HashBiMap<>(); + public static MapMetrics from(Map expected, MutableBiMap actual) { + Map truePositives = new IdentityHashMap<>(); + Map falsePositives = new IdentityHashMap<>(); + Map falseNegatives = new IdentityHashMap<>(); for (var entry : actual.entrySet()) { if (Objects.equals(expected.get(entry.getKey()), entry.getValue())) { diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/SyntheticDiagramMatchingEvaluationTest.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/SyntheticDiagramMatchingEvaluationTest.java index 4b175537d..30fd148c9 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/SyntheticDiagramMatchingEvaluationTest.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/SyntheticDiagramMatchingEvaluationTest.java @@ -1,9 +1,10 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation; import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.io.IOException; +import java.util.IdentityHashMap; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -129,7 +130,7 @@ private void doIterationsOnRefactoredModel(AnnotatedDiagram diagram, Anno MutableBiMap, Vertex> expectedLinks = new HashBiMap<>(); for (Map.Entry> link : refactoredModel.links().inverse().entrySet()) { - Box box = diagram.links().inverse().get(link.getKey()); + Box box = inverse(diagram.links()).get(link.getKey()); Vertex vertex = boxToVertex.get(box); expectedLinks.put(vertex, link.getValue()); } @@ -141,6 +142,16 @@ private void doIterationsOnRefactoredModel(AnnotatedDiagram diagram, Anno } } + private static Map inverse(Map map) { + Map result = new IdentityHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (result.put(entry.getValue(), entry.getKey()) != null) { + throw new IllegalStateException("Duplicate value: " + entry.getValue()); + } + } + return result; + } + private MutableBiMap, Vertex> match(DirectedMultigraph, Edge> a, DirectedMultigraph, Edge> b) { SimilarityFloodingAlgorithm, Vertex, Label> algorithm = new SimilarityFloodingAlgorithm<>( SyntheticDiagramMatchingEvaluationTest.DEFAULT_EPSILON, 10000, PropagationCoefficientFormula.getInverseProductFormula(), FixpointFormula diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/AnnotatedDiagram.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/AnnotatedDiagram.java index 98e69d446..103ba5963 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/AnnotatedDiagram.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/AnnotatedDiagram.java @@ -1,11 +1,14 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data; import java.io.File; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.Function; import org.eclipse.collections.api.bimap.MutableBiMap; @@ -36,7 +39,7 @@ * @param * The element type of the model. */ -public record AnnotatedDiagram(Diagram diagram, MutableBiMap links, Set> inconsistencies) { +public record AnnotatedDiagram(Diagram diagram, Map links, Set> inconsistencies) { /** * Create an annotated diagram from a diagram. * @@ -61,7 +64,7 @@ public static AnnotatedDiagram createFrom(Diagram diagram) { * @return The diagram. */ public static AnnotatedDiagram createFrom(String source, ArchitectureModel model) { - Diagram diagram = new DiagramImpl(new File(source)); + Diagram diagram = new DiagramImpl(source, new File(source)); MutableBiMap links = new HashBiMap<>(); Transformations.transform(model, (item) -> { @@ -81,8 +84,8 @@ public static AnnotatedDiagram createFrom(String source, Archi * @return The diagram. */ public static AnnotatedDiagram createFrom(String source, CodeModel model) { - Diagram diagram = new DiagramImpl(new File(source)); - MutableBiMap links = new HashBiMap<>(); + Diagram diagram = new DiagramImpl(source, new File(source)); + SortedMap links = new TreeMap<>(); Transformations.transform(model, (item) -> { Box box = DiagramUtility.addBox(diagram, item.getName()); @@ -90,7 +93,12 @@ public static AnnotatedDiagram createFrom(String source, CodeModel mod return box; }, (from, to) -> DiagramUtility.addConnector(diagram, from, to), (child, parent) -> parent.addContainedBox(child)); - return new AnnotatedDiagram<>(diagram, links.inverse(), new LinkedHashSet<>()); + Map identityInverse = new IdentityHashMap<>(); + for (Map.Entry entry : links.entrySet()) { + identityInverse.put(entry.getValue(), entry.getKey()); + } + + return new AnnotatedDiagram<>(diagram, identityInverse, new LinkedHashSet<>()); } /** @@ -102,7 +110,7 @@ public static AnnotatedDiagram createFrom(String source, CodeModel mod * @return The diagram. */ public static AnnotatedDiagram createFrom(String source, AnnotatedGraph graph) { - Diagram diagram = new DiagramImpl(new File(source)); + Diagram diagram = new DiagramImpl(source, new File(source)); Map, Box> vertexToBox = new LinkedHashMap<>(); for (Vertex vertex : graph.graph().vertexSet()) { diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/DiagramProject.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/DiagramProject.java index 897b7c789..0e417e387 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/DiagramProject.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/evaluation/data/DiagramProject.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data; import java.io.File; @@ -83,7 +83,7 @@ public File getDiagramFile() { URL resource = this.getDeclaringClass().getResource(this.getPath(DIAGRAM_FILE_NAME)); if (resource == null) { - throw new RuntimeException("Could not find diagram file for " + this.name()); + throw new IllegalArgumentException("Could not find diagram file for " + this.name()); } return new File(resource.getFile()); diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramConsistencyTest.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramConsistencyTest.java index f3dc02913..d99bacfc1 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramConsistencyTest.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/DiagramConsistencyTest.java @@ -1,11 +1,24 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; -import static org.junit.jupiter.api.Assertions.*; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.stream.IntStream; @@ -23,7 +36,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.DiagramState; import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.DiagramUtility; import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.Extractions; -import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.JsonMapping; import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.inconsistencies.Inconsistency; import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.inconsistencies.InconsistencyType; import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Box; @@ -67,8 +79,8 @@ class DiagramConsistencyTest extends EvaluationTestBase { DiagramProject.MEDIA_STORE, new double[] { 0.82, 0.90, 0.86 }); private final static Map expectedStage3 = Map.of(DiagramProject.BIG_BLUE_BUTTON, new double[] { 1.00, 1.00, 1.00 }, - DiagramProject.TEAMMATES_ARCHITECTURE, new double[] { 0.78, 0.78, 0.78 }, DiagramProject.TEAMMATES_PACKAGES, new double[] { 0.71, 0.92, 0.80 }, - DiagramProject.TEAMMATES_UI, new double[] { 0.74, 0.85, 0.80 }, DiagramProject.TEA_STORE, new double[] { 0.60, 0.47, 0.53 }, + DiagramProject.TEAMMATES_ARCHITECTURE, new double[] { 0.76, 0.76, 0.76 }, DiagramProject.TEAMMATES_PACKAGES, new double[] { 0.71, 0.92, 0.80 }, + DiagramProject.TEAMMATES_UI, new double[] { 0.73, 0.85, 0.78 }, DiagramProject.TEA_STORE, new double[] { 0.60, 0.47, 0.53 }, DiagramProject.MEDIA_STORE, new double[] { 0.79, 0.73, 0.76 }); private static Decision rateStage1Decision(DiagramProject project, DiagramMatchingModelSelectionState selection) { @@ -93,6 +105,7 @@ private static Decision rateStage1Decision(DiagramProject project, DiagramMatchi @Disabled("No assertions, serves as evaluation runner only") void evaluateStage1WithInitialParameters(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdArchitecture", String.valueOf(0.5)); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdCode", String.valueOf(0.75)); config.put("DiagramElementOccurrenceFinderInformant::similarityFunction", String.valueOf( @@ -112,6 +125,7 @@ void evaluateStage1WithInitialParameters(DiagramProject project) throws IOExcept @Disabled("No assertions, serves as evaluation runner only") void evaluateStage1WithInitialParametersWithJaccard(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdArchitecture", String.valueOf(0.5)); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdCode", String.valueOf(0.75)); config.put("DiagramElementOccurrenceFinderInformant::similarityFunction", String.valueOf( @@ -173,6 +187,7 @@ void evaluateStage1CodeThresholdParameterForJaccard() throws IOException { private void evaluateStage1Parameter(String parameter, DiagramElementOccurrenceFinderInformant.TextSimilarityFunction textSimilarityFunction) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdArchitecture", String.valueOf(0.5)); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdCode", String.valueOf(0.75)); config.put("DiagramElementOccurrenceFinderInformant::similarityFunction", String.valueOf(textSimilarityFunction)); @@ -209,6 +224,7 @@ private void evaluateStage1Parameter(String parameter, DiagramElementOccurrenceF @MethodSource("getDiagrams") void evaluateStage1VersionFinalJaccard(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdArchitecture", String.valueOf(0.6)); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdCode", String.valueOf(0.8)); config.put("DiagramElementOccurrenceFinderInformant::similarityFunction", String.valueOf( @@ -230,6 +246,7 @@ void evaluateStage1VersionFinalJaccard(DiagramProject project) throws IOExceptio @MethodSource("getDiagrams") void evaluateStage1VersionFinalLevenshtein(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdArchitecture", String.valueOf(0.5)); config.put("DiagramElementOccurrenceFinderInformant::similarityThresholdCode", String.valueOf(0.8)); config.put("DiagramElementOccurrenceFinderInformant::similarityFunction", String.valueOf( @@ -260,6 +277,7 @@ private void assertStage1Result(Map allExpected, Diagr @Disabled("No assertions, serves as evaluation runner only") void evaluateStage2Initial(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramModelLinkInformant::textSimilarityThreshold", String.valueOf(0.5)); config.put("DiagramModelLinkInformant::similarityThreshold", String.valueOf(Double.NEGATIVE_INFINITY)); @@ -286,6 +304,7 @@ private void writeStage2Result(DiagramProject project, Metrics metrics) throws I @Disabled("No assertions, serves as evaluation runner only") void evaluateStage2WithSimilarityThreshold(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramModelLinkInformant::textSimilarityThreshold", String.valueOf(0.5)); config.put("DiagramModelLinkInformant::similarityThreshold", String.valueOf(0.1)); @@ -300,6 +319,7 @@ void evaluateStage2WithSimilarityThreshold(DiagramProject project) throws IOExce @MethodSource("getDiagrams") void evaluateStage2TunedParameters(DiagramProject project) throws IOException { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); Metrics metrics = this.runAndEvaluateStage2(project, config); this.writeStage2Result(project, metrics); @@ -321,6 +341,7 @@ private void assertStage2And3Result(DiagramProject project, Map config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramModelLinkInformant::maxIterations", String.valueOf(0)); Metrics metrics = this.runAndEvaluateStage2(project, config); @@ -335,6 +356,7 @@ void evaluateStage2ZeroIterations(DiagramProject project) throws IOException { void evaluateStage2Epsilon() throws IOException { List epsilons = IntStream.range(1, 50).mapToDouble(i -> 0.05 * i).boxed().toList(); SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramModelLinkInformant::epsilon", String.valueOf(1.0)); config.put("DiagramModelLinkInformant::textSimilarityThreshold", String.valueOf(0.5)); config.put("DiagramModelLinkInformant::similarityThreshold", String.valueOf(0.1)); @@ -347,6 +369,7 @@ void evaluateStage2Epsilon() throws IOException { void evaluateStage2Levenshtein() throws IOException { List thresholds = IntStream.range(0, 50).mapToDouble(i -> 0.02 * i).boxed().toList(); SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramModelLinkInformant::epsilon", String.valueOf(1.0)); config.put("DiagramModelLinkInformant::textSimilarityThreshold", String.valueOf(0.5)); config.put("DiagramModelLinkInformant::similarityThreshold", String.valueOf(0.1)); @@ -359,6 +382,7 @@ void evaluateStage2Levenshtein() throws IOException { void evaluateStage2SimilarityThreshold() throws IOException { List thresholds = IntStream.range(0, 50).mapToDouble(i -> 0.02 * i).boxed().toList(); SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); config.put("DiagramModelLinkInformant::epsilon", String.valueOf(1.0)); config.put("DiagramModelLinkInformant::textSimilarityThreshold", String.valueOf(0.5)); config.put("DiagramModelLinkInformant::similarityThreshold", String.valueOf(0.1)); @@ -371,7 +395,7 @@ private void evaluateImpactOfParameterOnStage2(String parameter, List va try { return this.runAndEvaluateStage2(project, config); } catch (IOException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } }); } @@ -405,7 +429,7 @@ private void evaluateImpactOnStage(List values, int repetition, BiFuncti private DiagramConsistency setupAndRun(DiagramProject project, Refactoring refactoring, double ratio, SortedMap config, Consumer setup) throws IOException { String name = project.name().toLowerCase(Locale.ROOT); - File inputArchitectureModel = project.getSourceProject().getProject().getModelFile(ArchitectureModelType.UML); + File inputArchitectureModel = project.getSourceProject().getModelFile(ArchitectureModelType.UML); File inputCodeModel = new File(Objects.requireNonNull(project.getSourceProject().getCodeModelDirectory())).getAbsoluteFile(); File inputDiagram = this.getDiagramFile(project, refactoring, ratio); File outputDir = new File(PIPELINE_OUTPUT); @@ -456,7 +480,7 @@ private DecisionInfo runAndEvaluateStage1(DiagramProject project, SortedMap refact } File refactored = File.createTempFile("temp", ".json"); - JsonMapping.OBJECT_MAPPER.writeValue(refactored, diagram.diagram()); + createObjectMapper().writeValue(refactored, diagram.diagram()); return refactored; } @@ -517,10 +541,11 @@ void evaluateStage2StabilityToRename() throws IOException { List ratios = IntStream.range(0, 50).mapToDouble(i -> 0.02 * i).boxed().toList(); this.evaluateImpactOnStage(ratios, 10, (project, ratio) -> { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); try { return this.runAndEvaluateStage2(project, new Rename<>(), ratio, config); } catch (IOException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } }); } @@ -532,10 +557,11 @@ void evaluateStage2StabilityToDisconnect() throws IOException { List ratios = IntStream.range(0, 50).mapToDouble(i -> 0.02 * i).boxed().toList(); this.evaluateImpactOnStage(ratios, 10, (project, ratio) -> { SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); try { return this.runAndEvaluateStage2(project, new Disconnect<>(), ratio, config); } catch (IOException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } }); } @@ -558,7 +584,7 @@ private Metrics runAndEvaluateStage2(DiagramProject project, Refactoring expectedLinks = JsonMapping.OBJECT_MAPPER.readValue(project.getLinkingStage(), ElementLinks.class).toBiMap(); + MutableBiMap expectedLinks = createObjectMapper().readValue(project.getLinkingStage(), ElementLinks.class).toBiMap(); MutableBiMap foundLinks = runner.getDataRepository() .getData(DiagramModelLinkState.ID, DiagramModelLinkState.class) .orElseThrow() @@ -589,7 +615,7 @@ void evaluateAllStages(DiagramProject project) throws IOException { private Metrics runAndEvaluateStage3(DiagramProject project, Refactoring refactoring, double ratio, SortedMap config, boolean skipPreviousStages) throws IOException { - MutableBiMap expectedLinks = JsonMapping.OBJECT_MAPPER.readValue(project.getLinkingStage(), ElementLinks.class).toBiMap(); + MutableBiMap expectedLinks = createObjectMapper().readValue(project.getLinkingStage(), ElementLinks.class).toBiMap(); if (skipPreviousStages) { config.put("WeightedSimilarityInformant::skip", String.valueOf(true)); @@ -621,7 +647,7 @@ private Metrics runAndEvaluateStage3(DiagramProject project, Refactoring> expected = JsonMapping.OBJECT_MAPPER.readValue(project.getValidationStage(), DiagramInconsistencies.class) + List> expected = createObjectMapper().readValue(project.getValidationStage(), DiagramInconsistencies.class) .toInconsistencies(diagram, model); Map boxes = DiagramUtility.getBoxes(diagram); @@ -660,6 +686,7 @@ private void writeStage3Result(DiagramProject project, Metrics result) throws IO void evaluateStage3Epsilon() throws IOException { List epsilons = IntStream.range(1, 50).mapToDouble(i -> 0.05 * i).boxed().toList(); SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); this.evaluateImpactOfStage2ParameterOnStage3("epsilon", epsilons, config); } @@ -669,6 +696,7 @@ void evaluateStage3Epsilon() throws IOException { void evaluateStage3Levenshtein() throws IOException { List thresholds = IntStream.range(0, 50).mapToDouble(i -> 0.02 * i).boxed().toList(); SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); this.evaluateImpactOfStage2ParameterOnStage3("textSimilarityThreshold", thresholds, config); } @@ -678,6 +706,7 @@ void evaluateStage3Levenshtein() throws IOException { void evaluateStage3SimilarityThreshold() throws IOException { List thresholds = IntStream.range(0, 50).mapToDouble(i -> 0.02 * i).boxed().toList(); SortedMap config = new TreeMap<>(); + config.put("DiagramRecognition::enabledAgents", "DiagramRecognitionAgent"); this.evaluateImpactOfStage2ParameterOnStage3("similarityThreshold", thresholds, config); } @@ -687,7 +716,7 @@ private void evaluateImpactOfStage2ParameterOnStage3(String parameter, List counts = new LinkedHashMap<>(); for (var inconsistency : inconsistencies.inconsistencies()) { diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/Stage1SyntheticDiagramTest.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/Stage1SyntheticDiagramTest.java index a2f1d20ae..eaa48f7ff 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/Stage1SyntheticDiagramTest.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/Stage1SyntheticDiagramTest.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + import java.io.File; import java.io.IOException; import java.util.List; @@ -20,7 +22,6 @@ import org.junit.jupiter.params.provider.MethodSource; import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.DiagramMatchingModelSelectionState; -import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.JsonMapping; import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram; import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; import edu.kit.kastel.mcse.ardoco.core.api.models.CodeModelType; @@ -98,7 +99,7 @@ void examineStage1Base(DiagramProject project) throws IOException { this.writer.write(String.format("%s: r: %s, u: %s, d: %s%n", result.expected().type(), result.actual().ratio(), result.actual() .percentageOfUnnecessaryLinks(), difference)); } catch (IOException e) { - throw new RuntimeException(e); + throw new IllegalStateException(e); } }; @@ -108,7 +109,7 @@ void examineStage1Base(DiagramProject project) throws IOException { private Result examineStage1(DiagramProject project, GeneralModelType generalModelType) throws IOException { String name = project.name().toLowerCase(Locale.ROOT); - File inputArchitectureModel = project.getSourceProject().getProject().getModelFile(ArchitectureModelType.UML); + File inputArchitectureModel = project.getSourceProject().getModelFile(ArchitectureModelType.UML); File inputCodeModel = new File(Objects.requireNonNull(project.getSourceProject().getCodeModelDirectory())).getAbsoluteFile(); File inputDiagram = File.createTempFile("temp", ".json"); File outputDir = new File(PIPELINE_OUTPUT); @@ -123,7 +124,7 @@ private Result examineStage1(DiagramProject project, GeneralModelType generalMod Decision expected = new Decision(generalModelType, (double) expectedLinks.size() / syntheticDiagram.getBoxes().size(), 0.0); - JsonMapping.OBJECT_MAPPER.writeValue(inputDiagram, syntheticDiagram); + createObjectMapper().writeValue(inputDiagram, syntheticDiagram); DiagramConsistency runner = new DiagramConsistency(name); diff --git a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/SyntheticTestBase.java b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/SyntheticTestBase.java index 518882bdd..769052bd2 100644 --- a/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/SyntheticTestBase.java +++ b/stages/diagram-consistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/diagramconsistency/informants/SyntheticTestBase.java @@ -1,8 +1,9 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.diagramconsistency.informants; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; import static org.apache.commons.lang3.ClassUtils.getSimpleName; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.io.File; import java.io.IOException; @@ -13,7 +14,6 @@ import java.util.SortedMap; import java.util.TreeMap; -import edu.kit.kastel.mcse.ardoco.core.api.diagramconsistency.common.JsonMapping; import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Box; import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; import edu.kit.kastel.mcse.ardoco.core.api.models.CodeModelType; @@ -27,7 +27,14 @@ import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.MetricsStats; import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data.AnnotatedDiagram; import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.data.DiagramProject; -import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.*; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Connect; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Create; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Delete; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Disconnect; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Move; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Refactoring; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.RefactoringBundle; +import edu.kit.kastel.mcse.ardoco.core.diagramconsistency.evaluation.refactoring.Rename; class SyntheticTestBase extends EvaluationTestBase { protected static final int REFACTORING_TYPE_COUNT = 6; @@ -127,7 +134,7 @@ protected void doEvaluationIterations(ExaminationDescription description, int it private Metrics examineOnSyntheticDiagram(ExaminationDescription description) throws IOException { String name = description.project().name().toLowerCase(Locale.ROOT); - File inputArchitectureModel = description.project().getSourceProject().getProject().getModelFile(ArchitectureModelType.UML); + File inputArchitectureModel = description.project().getSourceProject().getModelFile(ArchitectureModelType.UML); File inputCodeModel = new File(Objects.requireNonNull(description.project().getSourceProject().getCodeModelDirectory())).getAbsoluteFile(); File inputDiagram = File.createTempFile("temp", ".json"); File outputDir = new File(PIPELINE_OUTPUT); @@ -156,7 +163,7 @@ private Metrics examineOnSyntheticDiagram(ExaminationDescription description) th default -> throw new IllegalArgumentException("Unexpected value: " + description.generalModelType()); } - JsonMapping.OBJECT_MAPPER.writeValue(inputDiagram, syntheticDiagram.diagram()); + createObjectMapper().writeValue(inputDiagram, syntheticDiagram.diagram()); DiagramConsistency runner = new DiagramConsistency(name); this.setupExamination(modelType, runner.getDataRepository()); diff --git a/stages/diagram-consistency/src/test/resources/.gitignore b/stages/diagram-consistency/src/test/resources/.gitignore index edba3d3fe..39c5339c5 100644 --- a/stages/diagram-consistency/src/test/resources/.gitignore +++ b/stages/diagram-consistency/src/test/resources/.gitignore @@ -1 +1,2 @@ test_out +pipeline_out diff --git a/stages/diagram-inconsistency-checker/.gitignore b/stages/diagram-inconsistency-checker/.gitignore new file mode 100644 index 000000000..5ff6309b7 --- /dev/null +++ b/stages/diagram-inconsistency-checker/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/stages/diagram-inconsistency-checker/pom.xml b/stages/diagram-inconsistency-checker/pom.xml new file mode 100644 index 000000000..281b33d31 --- /dev/null +++ b/stages/diagram-inconsistency-checker/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + io.github.ardoco.core + stages + ${revision} + ../pom.xml + + diagram-inconsistency-checker + + + + io.github.ardoco.core + common + ${revision} + compile + + + io.github.ardoco.core + diagram-connection-generator + ${revision} + compile + + + io.github.ardoco.core + diagram-recognition + ${revision} + test + + + io.github.ardoco.core + inconsistency-detection + ${revision} + compile + + + io.github.ardoco.core + tests-base + ${revision} + compile + + + io.github.ardoco.core + tests-resources + ${revision} + compile + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.slf4j + slf4j-simple + test + + + diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagraminconsistency/DiagramInconsistencyState.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagraminconsistency/DiagramInconsistencyState.java new file mode 100644 index 000000000..852d4c265 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagraminconsistency/DiagramInconsistencyState.java @@ -0,0 +1,36 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency; + +import java.util.Set; + +import edu.kit.kastel.mcse.ardoco.core.api.inconsistency.Inconsistency; +import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; + +/** + * This state holds the diagram element {@link Inconsistency Inconsistencies} + * {@link edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MDEInconsistency MDEInconsistency} and + * {@link edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MTDEInconsistency MTDEInconsistency}. + */ +public interface DiagramInconsistencyState extends PipelineStepData { + String ID = "DiagramInconsistencyState"; + + /** + * {@return the inconsistencies of the given type} + * + * @param type inconsistency type + */ + Set getInconsistencies(Class type); + + /** + * {@return the set of inconsistencies discovered by this stage} + */ + Set getInconsistencies(); + + /** + * Adds an inconsistency + * + * @param inconsistency instance + * @return true, if the inconsistency was added to the state, false else + */ + boolean addInconsistency(Inconsistency inconsistency); +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagraminconsistency/DiagramInconsistencyStates.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagraminconsistency/DiagramInconsistencyStates.java new file mode 100644 index 000000000..a6961b2bf --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/api/diagraminconsistency/DiagramInconsistencyStates.java @@ -0,0 +1,19 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.data.PipelineStepData; + +/** + * Contains the {@link DiagramInconsistencyState} for each {@link Metamodel}. + */ +public interface DiagramInconsistencyStates extends PipelineStepData { + String ID = "DiagramInconsistencyStates"; + + /** + * {@return the state for the given metamodel} + * + * @param mm the metamodel + */ + DiagramInconsistencyState getDiagramInconsistencyState(Metamodel mm); +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyChecker.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyChecker.java new file mode 100644 index 000000000..1efba3638 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyChecker.java @@ -0,0 +1,60 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency; + +import java.util.List; +import java.util.SortedMap; + +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimUtils; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.ExecutionStage; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyStates; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.agents.DiagramInconsistencyAgent; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.agents.RecommendedInstancesConfidenceAgent; + +/** + * This stage is responsible for creating the inconsistencies and uses them to adjust the confidence of recommended instances. + */ +public class DiagramInconsistencyChecker extends ExecutionStage { + private final WordSimUtils wordSimUtils; + + /** + * Sole constructor of the stage. + * + * @param additionalConfigs the additional configs that should be used + * @param dataRepository the data repository that should be used + */ + public DiagramInconsistencyChecker(SortedMap additionalConfigs, DataRepository dataRepository) { + super(List.of(new DiagramInconsistencyAgent(dataRepository), new RecommendedInstancesConfidenceAgent(dataRepository)), DiagramInconsistencyChecker.class + .getSimpleName(), dataRepository, additionalConfigs); + this.wordSimUtils = dataRepository.getGlobalConfiguration().getWordSimUtils(); + } + + @Override + protected void initializeState() { + logger.info("Creating DiagramInconsistency States"); + var diagramInconsistencyStates = new DiagramInconsistencyStatesImpl(); + getDataRepository().addData(DiagramInconsistencyStates.ID, diagramInconsistencyStates); + } + + private UnicodeCharacterMatchFunctions previousCharacterMatchFunction; + + /** + * Saves the previous character match function and sets a character match function capable of matching homoglyphs. + */ + @Override + protected void before() { + super.before(); + previousCharacterMatchFunction = wordSimUtils.getCharacterMatchFunction(); + wordSimUtils.setCharacterMatchFunction(UnicodeCharacterMatchFunctions.EQUAL_OR_HOMOGLYPH); + } + + /** + * Sets the character match function back to the previous function. + */ + @Override + protected void after() { + wordSimUtils.setCharacterMatchFunction(previousCharacterMatchFunction); + super.after(); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyStateImpl.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyStateImpl.java new file mode 100644 index 000000000..27d384cb3 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyStateImpl.java @@ -0,0 +1,31 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import edu.kit.kastel.mcse.ardoco.core.api.inconsistency.Inconsistency; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyState; + +/** + * @see DiagramInconsistencyState + */ +public class DiagramInconsistencyStateImpl implements DiagramInconsistencyState { + private final Set inconsistencies = new HashSet<>(); + + @Override + public Set getInconsistencies(Class type) { + return inconsistencies.stream().filter(type::isInstance).map(type::cast).collect(Collectors.toUnmodifiableSet()); + } + + @Override + public Set getInconsistencies() { + return Set.copyOf(inconsistencies); + } + + @Override + public boolean addInconsistency(Inconsistency inconsistency) { + return inconsistencies.add(inconsistency); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyStatesImpl.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyStatesImpl.java new file mode 100644 index 000000000..3ecd721a1 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyStatesImpl.java @@ -0,0 +1,27 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency; + +import java.util.EnumMap; +import java.util.Map; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyState; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyStates; + +/** + * @see DiagramInconsistencyStates + */ +public class DiagramInconsistencyStatesImpl implements DiagramInconsistencyStates { + private final Map diagramInconsistencyStates = new EnumMap<>(Metamodel.class); + + public DiagramInconsistencyStatesImpl() { + for (Metamodel mm : Metamodel.values()) { + diagramInconsistencyStates.put(mm, new DiagramInconsistencyStateImpl()); + } + } + + @Override + public DiagramInconsistencyState getDiagramInconsistencyState(Metamodel mm) { + return diagramInconsistencyStates.get(mm); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/agents/DiagramInconsistencyAgent.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/agents/DiagramInconsistencyAgent.java new file mode 100644 index 000000000..ae281f66c --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/agents/DiagramInconsistencyAgent.java @@ -0,0 +1,22 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.agents; + +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.informants.MDEInconsistencyInformant; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.informants.MTDEInconsistencyInformant; + +/** + * This agent is responsible for the creation of the diagram element {@link edu.kit.kastel.mcse.ardoco.core.api.inconsistency.Inconsistency Inconsistencies} + * such as the {@link edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MDEInconsistency MDEInconsistency} and + * {@link edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MTDEInconsistency MTDEInconsistency}. + */ +public class DiagramInconsistencyAgent extends PipelineAgent { + + public DiagramInconsistencyAgent(DataRepository dataRepository) { + super(List.of(new MDEInconsistencyInformant(dataRepository), new MTDEInconsistencyInformant(dataRepository)), DiagramInconsistencyAgent.class + .getSimpleName(), dataRepository); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/agents/RecommendedInstancesConfidenceAgent.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/agents/RecommendedInstancesConfidenceAgent.java new file mode 100644 index 000000000..0477d63fc --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/agents/RecommendedInstancesConfidenceAgent.java @@ -0,0 +1,18 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.agents; + +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.informants.InfluenceByInconsistenciesInformant; + +/** + * This agent adjust the confidence of recommended instances according to how they are affected by the inconsistencies + */ +public class RecommendedInstancesConfidenceAgent extends PipelineAgent { + + public RecommendedInstancesConfidenceAgent(DataRepository dataRepository) { + super(List.of(new InfluenceByInconsistenciesInformant(dataRepository)), RecommendedInstancesConfidenceAgent.class.getSimpleName(), dataRepository); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/InfluenceByInconsistenciesInformant.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/InfluenceByInconsistenciesInformant.java new file mode 100644 index 000000000..ef2af2aa6 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/InfluenceByInconsistenciesInformant.java @@ -0,0 +1,86 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.informants; + +import java.util.Set; +import java.util.stream.Collectors; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyStates; +import edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MDEInconsistency; + +/** + * This informant punishes {@link MDEInconsistency MDEInconsistencies} by lowering the confidence of the recommended instance and rewards consistency by + * increasing the confidence. The adjustment is based on a configurable {@link #maximumReward} and {@link #maximumPunishment}. + */ +public class InfluenceByInconsistenciesInformant extends Informant { + + @Configurable + private double maximumReward = .55; + + @Configurable + private double maximumPunishment = -10d; + + /** + * Creates a new informant that acts on the specified data repository + * + * @param dataRepository the data repository + */ + public InfluenceByInconsistenciesInformant(DataRepository dataRepository) { + super(InfluenceByInconsistenciesInformant.class.getSimpleName(), dataRepository); + } + + @Override + public void process() { + var dataRepository = getDataRepository(); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + var diagramInconsistencyStates = dataRepository.getData(DiagramInconsistencyStates.ID, DiagramInconsistencyStates.class).orElseThrow(); + var recommendationStates = DataRepositoryHelper.getRecommendationStates(dataRepository); + var metamodels = Metamodel.values(); + for (var mm : metamodels) { + var diagramInconsistencyState = diagramInconsistencyStates.getDiagramInconsistencyState(mm); + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(mm); + var allRecommendedInstances = recommendationStates.getRecommendationState(mm).getRecommendedInstances(); + var coveredRecommendedInstances = allRecommendedInstances.stream() + .filter(ri -> !diagramConnectionState.getLinksBetweenDeAndRi(ri).isEmpty()) + .distinct() + .toList(); + var mdeInconsistencies = diagramInconsistencyState.getInconsistencies(MDEInconsistency.class); + var linksBetweenDeAndRi = allRecommendedInstances.stream() + .flatMap(ri -> diagramConnectionState.getLinksBetweenDeAndRi(ri).stream()) + .collect(Collectors.toSet()); + var coverage = coveredRecommendedInstances.size() / (double) allRecommendedInstances.size(); + logger.info("Recommended Instances coverage {}%, Covered RIs {}, All RIs {}", coverage * 100, coveredRecommendedInstances.size(), + allRecommendedInstances.size()); + punishInconsistency(coverage, mdeInconsistencies); + rewardConsistency(coverage, linksBetweenDeAndRi); + } + } + + private void punishInconsistency(double coverage, Set mdeInconsistencySet) { + mdeInconsistencySet.forEach(mde -> { + var old = mde.recommendedInstance().getProbability(); + //Punish more if coverage is high + var probability = clamp(old + maximumPunishment * coverage, 0.0, 1.0); + mde.recommendedInstance().addProbability(this, probability); + }); + } + + private void rewardConsistency(double coverage, Set linkBetweenDeAndRiSet) { + linkBetweenDeAndRiSet.forEach(dl -> { + var old = dl.getRecommendedInstance().getProbability(); + //Reward more if coverage is low + var probability = clamp(old + maximumReward * (1.0 - coverage), 0.0, 1.0); + dl.getRecommendedInstance().addProbability(this, probability); + }); + } + + private double clamp(double value, double min, double max) { + return Math.max(Math.min(value, max), min); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/MDEInconsistencyInformant.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/MDEInconsistencyInformant.java new file mode 100644 index 000000000..181aa9c68 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/MDEInconsistencyInformant.java @@ -0,0 +1,47 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.informants; + +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyStates; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MDEInconsistency; + +/** + * This informant is responsible for finding the {@link MDEInconsistency MDEInconsistencies} + */ +public class MDEInconsistencyInformant extends Informant { + /** + * Creates a new informant that acts on the specified data repository + * + * @param dataRepository the data repository + */ + public MDEInconsistencyInformant(DataRepository dataRepository) { + super(MDEInconsistencyInformant.class.getSimpleName(), dataRepository); + } + + /** + * Creates an {@link MDEInconsistency} for each {@link edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance RecommendedInstance} + * that is not the endpoint of a {@link edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi LinkBetweenDeAndRi}. + */ + @Override + public void process() { + var dataRepository = getDataRepository(); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + var diagramInconsistencyStates = dataRepository.getData(DiagramInconsistencyStates.ID, DiagramInconsistencyStates.class).orElseThrow(); + var recommendationStates = DataRepositoryHelper.getRecommendationStates(dataRepository); + for (var mm : Metamodel.values()) { + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(mm); + var diagramInconsistencyState = diagramInconsistencyStates.getDiagramInconsistencyState(mm); + var allRecommendedInstances = recommendationStates.getRecommendationState(mm).getRecommendedInstances(); + var uncoveredRecommendedInstances = allRecommendedInstances.stream() + .filter(ri -> diagramConnectionState.getLinksBetweenDeAndRi(ri).isEmpty()) + .distinct() + .toList(); + + uncoveredRecommendedInstances.forEach(ri -> diagramInconsistencyState.addInconsistency(new MDEInconsistency(ri))); + } + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/MTDEInconsistencyInformant.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/MTDEInconsistencyInformant.java new file mode 100644 index 000000000..cffb0cdd8 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/informants/MTDEInconsistencyInformant.java @@ -0,0 +1,47 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.informants; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState; +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.erid.api.diagramconnectiongenerator.DiagramConnectionStates; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyStates; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MTDEInconsistency; + +/** + * This informant is responsible for finding the {@link MTDEInconsistency MTDEInconsistencies} + */ +public class MTDEInconsistencyInformant extends Informant { + /** + * Creates a new informant that acts on the specified data repository + * + * @param dataRepository the data repository + */ + public MTDEInconsistencyInformant(DataRepository dataRepository) { + super(MTDEInconsistencyInformant.class.getSimpleName(), dataRepository); + } + + /** + * Creates an {@link MTDEInconsistency} for each {@link edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement DiagramElement} that is not + * the endpoint of a {@link edu.kit.kastel.mcse.ardoco.erid.api.models.tracelinks.LinkBetweenDeAndRi LinkBetweenDeAndRi}. + */ + @Override + public void process() { + var dataRepository = getDataRepository(); + var diagramRecognitionState = dataRepository.getData(DiagramRecognitionState.ID, DiagramRecognitionState.class).orElseThrow(); + var diagramConnectionStates = dataRepository.getData(DiagramConnectionStates.ID, DiagramConnectionStates.class).orElseThrow(); + var diagramInconsistencyStates = dataRepository.getData(DiagramInconsistencyStates.ID, DiagramInconsistencyStates.class).orElseThrow(); + for (var mm : Metamodel.values()) { + var diagramConnectionState = diagramConnectionStates.getDiagramConnectionState(mm); + var diagramInconsistencyState = diagramInconsistencyStates.getDiagramInconsistencyState(mm); + var allDiagramElements = diagramRecognitionState.getDiagrams().stream().flatMap(d -> d.getBoxes().stream()).distinct().toList(); + var uncoveredDiagramElements = allDiagramElements.stream() + .filter(de -> diagramConnectionState.getLinksBetweenDeAndRi(de).isEmpty()) + .distinct() + .toList(); + + uncoveredDiagramElements.forEach(de -> diagramInconsistencyState.addInconsistency(new MTDEInconsistency(de))); + } + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/types/MDEInconsistency.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/types/MDEInconsistency.java new file mode 100644 index 000000000..06619c7c7 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/types/MDEInconsistency.java @@ -0,0 +1,44 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types; + +import java.util.Locale; + +import org.eclipse.collections.api.collection.ImmutableCollection; +import org.eclipse.collections.api.factory.Lists; + +import edu.kit.kastel.mcse.ardoco.core.api.inconsistency.Inconsistency; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; + +/** + * {@link MDEInconsistency} stands for Missing Diagram Element Inconsistency and should be created for each {@link RecommendedInstance} that is not part of a + * {@link edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram Diagram}. + * + * @param recommendedInstance the {@link RecommendedInstance} + */ +public record MDEInconsistency(RecommendedInstance recommendedInstance) implements Inconsistency, Comparable { + private static final String type = "MissingDiagramElement"; + + @Override + public String getReason() { + return String.format(Locale.ENGLISH, "Text indicates an Entity \"%s\" (confidence: %.2f) that is not contained by a diagram.", recommendedInstance + .getName(), recommendedInstance.getProbability()); + } + + @Override + public String getType() { + return type; + } + + @Override + public ImmutableCollection toFileOutput() { + String[] entry = { getType(), recommendedInstance.getName(), Double.toString(recommendedInstance.getProbability()) }; + var list = Lists.mutable.empty(); + list.add(entry); + return list.toImmutable(); + } + + @Override + public int compareTo(MDEInconsistency o) { + return recommendedInstance().getName().compareTo(o.recommendedInstance().getName()); + } +} diff --git a/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/types/MTDEInconsistency.java b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/types/MTDEInconsistency.java new file mode 100644 index 000000000..d47e2cce5 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/main/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/types/MTDEInconsistency.java @@ -0,0 +1,44 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types; + +import java.util.Locale; + +import org.eclipse.collections.api.collection.ImmutableCollection; +import org.eclipse.collections.api.factory.Lists; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.inconsistency.Inconsistency; + +/** + * {@link MTDEInconsistency} stands for Missing Text for Diagram Element Inconsistency and should be created for each {@link DiagramElement} that is not part of + * the {@link edu.kit.kastel.mcse.ardoco.core.api.text.Text Text}. + * + * @param diagramElement the {@link DiagramElement} + */ +public record MTDEInconsistency(DiagramElement diagramElement) implements Inconsistency, Comparable { + private static final String type = "MissingTextForDiagramElement"; + + @Override + public String getReason() { + return String.format(Locale.US, "Diagram \"%s\" contains a Diagram Element \"%s\" that seems to be undocumented.", diagramElement.getDiagram() + .getResourceName(), diagramElement.getName()); + } + + @Override + public String getType() { + return type; + } + + @Override + public ImmutableCollection toFileOutput() { + String[] entry = { getType(), diagramElement.getName() }; + var list = Lists.mutable.empty(); + list.add(entry); + return list.toImmutable(); + } + + @Override + public int compareTo(MTDEInconsistency o) { + return diagramElement().compareTo(o.diagramElement()); + } +} diff --git a/stages/diagram-inconsistency-checker/src/test/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyCheckerTest.java b/stages/diagram-inconsistency-checker/src/test/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyCheckerTest.java new file mode 100644 index 000000000..3a5e9f285 --- /dev/null +++ b/stages/diagram-inconsistency-checker/src/test/java/edu/kit/kastel/mcse/ardoco/erid/diagraminconsistency/DiagramInconsistencyCheckerTest.java @@ -0,0 +1,161 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.eclipse.collections.impl.factory.SortedMaps; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData; +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; +import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; +import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; +import edu.kit.kastel.mcse.ardoco.erid.api.diagraminconsistency.DiagramInconsistencyStates; +import edu.kit.kastel.mcse.ardoco.erid.diagramconnectiongenerator.DiagramConnectionGenerator; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MDEInconsistency; +import edu.kit.kastel.mcse.ardoco.erid.diagraminconsistency.types.MTDEInconsistency; +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.DiagramRecognitionMock; +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; +import edu.kit.kastel.mcse.ardoco.tests.eval.StageTest; + +class DiagramInconsistencyCheckerTest extends StageTest { + private static final Logger logger = LoggerFactory.getLogger(DiagramInconsistencyCheckerTest.class); + private static final boolean useMockDiagrams = true; + + public record Results(SortedSet mdeInconsistencies, SortedSet mtdeInconsistencies) { + @Override + public String toString() { + return String.format("MissingDiagramElements: %d, MissingTextForDiagramElements: %d", mdeInconsistencies().size(), mtdeInconsistencies().size()); + } + } + + public DiagramInconsistencyCheckerTest() { + super(new DiagramInconsistencyChecker(SortedMaps.mutable.empty(), new DataRepository()), DiagramProject.values()); + } + + @Override + protected Results runComparable(DiagramProject project, SortedMap additionalConfigurations, boolean cachePreRun) { + var dataRepository = run(project, additionalConfigurations, cachePreRun); + var diagramInconsistencyStates = dataRepository.getData(DiagramInconsistencyStates.ID, DiagramInconsistencyStates.class).orElseThrow(); + var diagramInconsistencyState = diagramInconsistencyStates.getDiagramInconsistencyState(project.getMetamodel()); + + var mdeInconsistencies = new TreeSet<>(diagramInconsistencyState.getInconsistencies(MDEInconsistency.class)); + var mtdeInconsistencies = new TreeSet<>(diagramInconsistencyState.getInconsistencies(MTDEInconsistency.class)); + + var result = new DiagramInconsistencyCheckerTest.Results(mdeInconsistencies, mtdeInconsistencies); + + logger.info(result.toString()); + + return result; + } + + @Override + protected DataRepository runPreTestRunner(DiagramProject project) { + logger.info("Run PreTestRunner for {}", project.name()); + return new AnonymousRunner(project.name()) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) throws IOException { + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + var pipelineSteps = new ArrayList(); + + var text = CommonUtilities.readInputText(project.getTextFile()); + if (text.isBlank()) { + throw new IllegalArgumentException("Cannot deal with empty input text. Maybe there was an error reading the file."); + } + DataRepositoryHelper.putInputText(dataRepository, text); + pipelineSteps.add(TextPreprocessingAgent.get(project.getAdditionalConfigurations(), dataRepository)); + + ArCoTLModelProviderAgent arCoTLModelProviderAgent = ArCoTLModelProviderAgent.get(project.getModelFile(), project.getArchitectureModelType(), + null, project.getAdditionalConfigurations(), dataRepository); + pipelineSteps.add(arCoTLModelProviderAgent); + + if (useMockDiagrams) { + pipelineSteps.add(new DiagramRecognitionMock(project, project.getAdditionalConfigurations(), dataRepository)); + } else { + dataRepository.addData(InputDiagramData.ID, new InputDiagramData(project.getDiagramData())); + pipelineSteps.add(DiagramRecognition.get(project.getAdditionalConfigurations(), dataRepository)); + } + + pipelineSteps.add(TextExtraction.get(project.getAdditionalConfigurations(), dataRepository)); + pipelineSteps.add(RecommendationGenerator.get(project.getAdditionalConfigurations(), dataRepository)); + pipelineSteps.add(new DiagramConnectionGenerator(project.getAdditionalConfigurations(), dataRepository)); + + return pipelineSteps; + } + }.runWithoutSaving(); + } + + @Override + protected DataRepository runTestRunner(DiagramProject project, SortedMap additionalConfigurations, DataRepository preRunDataRepository) { + logger.info("Run TestRunner for {}", project.name()); + var combinedConfigs = new TreeMap<>(project.getAdditionalConfigurations()); + combinedConfigs.putAll(additionalConfigurations); + return new AnonymousRunner(project.name(), preRunDataRepository) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) { + dataRepository.getGlobalConfiguration().getWordSimUtils().setConsiderAbbreviations(true); + var pipelineSteps = new ArrayList(); + pipelineSteps.add(new DiagramInconsistencyChecker(combinedConfigs, dataRepository)); + return pipelineSteps; + } + }.runWithoutSaving(); + } + + @Disabled + @Test + void teammatesTest() { + runComparable(DiagramProject.TEAMMATES); + } + + @Disabled + @Test + void teammatesHistTest() { + runComparable(DiagramProject.TEAMMATES_HISTORICAL); + } + + @Disabled + @Test + void teastoreTest() { + runComparable(DiagramProject.TEASTORE); + } + + @Disabled + @Test + void teastoreHistTest() { + runComparable(DiagramProject.TEASTORE_HISTORICAL); + } + + @Disabled + @Test + void bbbTest() { + runComparable(DiagramProject.BIGBLUEBUTTON); + } + + @Disabled + @Test + void bbbHistTest() { + runComparable(DiagramProject.BIGBLUEBUTTON_HISTORICAL); + } + + @Disabled + @Test + void msTest() { + runComparable(DiagramProject.MEDIASTORE); + } +} diff --git a/stages/diagram-recognition/pom.xml b/stages/diagram-recognition/pom.xml index 057a93dc0..cfdb26f7d 100644 --- a/stages/diagram-recognition/pom.xml +++ b/stages/diagram-recognition/pom.xml @@ -8,6 +8,10 @@ diagram-recognition + + + ${java.version} + com.fasterxml.jackson.module @@ -24,6 +28,32 @@ common ${revision} + + io.github.ardoco.core + model-provider + ${revision} + test + + + io.github.ardoco.core + tests-base + ${revision} + + + io.github.ardoco.core + tests-resources + ${revision} + + + io.github.ardoco.core + text-extraction + ${revision} + + + io.github.ardoco.core + text-preprocessing + ${revision} + org.assertj assertj-core diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/core/textextraction/DiagramBackedNounMappingImpl.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/core/textextraction/DiagramBackedNounMappingImpl.kt new file mode 100644 index 000000000..4a7cbecc0 --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/core/textextraction/DiagramBackedNounMappingImpl.kt @@ -0,0 +1,21 @@ +package edu.kit.kastel.mcse.ardoco.core.textextraction + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping + +/** + * Represents a [NounMapping] where mappings are associated with a [DiagramElement]. + */ +class DiagramBackedNounMappingImpl(nounMapping: NounMappingImpl, private val diagramElement: DiagramElement?) : NounMappingImpl( + CREATION_TIME_COUNTER.incrementAndGet(), + nounMapping.words, + nounMapping.distribution.toSortedMap().toImmutable(), + nounMapping.referenceWords, + nounMapping.surfaceForms, + nounMapping.reference +) { + /** + * {@return the associated {@link DiagramElement}, {@link Optional#EMPTY} if none is associated} + */ + fun getDiagramElement(): DiagramElement? = diagramElement +} diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/core/textextraction/DiagramBackedTextStateStrategy.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/core/textextraction/DiagramBackedTextStateStrategy.kt new file mode 100644 index 000000000..49e58d241 --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/core/textextraction/DiagramBackedTextStateStrategy.kt @@ -0,0 +1,170 @@ +package edu.kit.kastel.mcse.ardoco.core.textextraction + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Box +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramUtil +import edu.kit.kastel.mcse.ardoco.core.api.text.Word +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonTextToolsConfig +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimUtils +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant +import org.eclipse.collections.api.factory.Lists +import org.eclipse.collections.api.factory.SortedSets +import org.eclipse.collections.api.list.ImmutableList +import org.slf4j.LoggerFactory + +/** + * Extends the [OriginalTextStateStrategy] by further dividing similar mappings by their similarity to the available + * [DiagramElements][DiagramElement]. For example, consider that both a package diagram and the text + * contain "routeplanner" and "routeplannerui". If a "routeplanner" noun mapping exists and we want to add "routeplannerui", a combined mapping would be created + * due to similarity. By additionally comparing to the diagram elements, we find that they are related to different package diagram elements, and thus probably + * shouldn't be contained by the same mapping. + */ +class DiagramBackedTextStateStrategy( + private val dataRepository: DataRepository +) : OriginalTextStateStrategy(dataRepository.globalConfiguration) { + private val wordSimUtils: WordSimUtils = globalConfiguration.wordSimUtils + private val similarityUtils: SimilarityUtils = globalConfiguration.similarityUtils + private lateinit var boxes: List + + /** + * Tries to add a mapping to the state using the existing parameters. Searches for similar mappings using the similarity metrics. Additionally, checks the + * relationship between mappings and the available [DiagramElements][DiagramElement] to further + * subdivide them. + */ + override fun addOrExtendNounMapping( + word: Word, + kind: MappingKind, + claimant: Claimant, + probability: Double, + surfaceForms: ImmutableList + ): NounMapping { + if (!this::boxes.isInitialized) { + val diagramRecognitionState = DataRepositoryHelper.getDiagramRecognitionState(dataRepository) + boxes = diagramRecognitionState.getDiagrams().flatMap { d: Diagram -> d.getBoxes() } + logger.debug("Loaded {} Boxes", boxes.size) + } + val disposableNounMapping = + NounMappingImpl( + System.currentTimeMillis(), + SortedSets.immutable.with(word), + kind, + claimant, + probability, + Lists.immutable.with(word), + surfaceForms + ) + val relatedToWordUnboxed = getMostSimilar(boxes, word) + for (existingNounMapping in getTextState().nounMappings) { + if (similarityUtils.areNounMappingsSimilar(disposableNounMapping, existingNounMapping) && + isDiagramElementMostSimilar( + boxes, + relatedToWordUnboxed, + existingNounMapping + ) + ) { + val mergedNounMapping = + DiagramBackedNounMappingImpl( + mergeNounMappingsStateless( + existingNounMapping, + disposableNounMapping, + disposableNounMapping.referenceWords, + disposableNounMapping.reference, + disposableNounMapping.getKind(), + claimant, + disposableNounMapping.probability + ), + relatedToWordUnboxed + ) + getTextState().removeNounMappingFromState(existingNounMapping, mergedNounMapping) + getTextState().removeNounMappingFromState(disposableNounMapping, mergedNounMapping) + getTextState().addNounMappingAddPhraseMapping(mergedNounMapping) + return mergedNounMapping + } + } + val diagramNM = + DiagramBackedNounMappingImpl( + disposableNounMapping, + getMostSimilar(boxes, disposableNounMapping) + ) + getTextState().addNounMappingAddPhraseMapping(diagramNM) + return diagramNM + } + + /** + * {@return whether the diagram element is the most similar to the noun mapping} + * + * @param diagramElements the diagram elements to search, if the noun mappings relation to the diagram elements is unknown + * @param candidate candidate for the most similar diagram element we want to check + * @param nounMapping the noun mapping + */ + private fun isDiagramElementMostSimilar( + diagramElements: List, + candidate: Box?, + nounMapping: NounMapping + ): Boolean { + return if (nounMapping is DiagramBackedNounMappingImpl) { + candidate == nounMapping.getDiagramElement() + } else { + val nounMapDE = getMostSimilar(diagramElements, nounMapping) + candidate == nounMapDE + } + } + + /** + * {@return the most similar diagram elements to the noun mapping} + * + * @param diagramElements the diagram elements to search + * @param nounMapping the mapping + */ + private fun getMostSimilar( + diagramElements: List, + nounMapping: NounMapping + ): Box? { + val nounMapPairs = + diagramElements + .map { box: Box -> DiagramUtil.calculateHighestSimilarity(wordSimUtils, nounMapping, box) to box } + .filter { p -> p.first >= CommonTextToolsConfig.DE_NM_SIMILARITY_THRESHOLD } + return nounMapPairs.maxWithOrNull(diagramElementSimilarity)?.second + } + + /** + * {@return the most similar diagram element to the provided word} + * + * @param diagramElements the diagram elements to search + * @param word the word + */ + private fun getMostSimilar( + diagramElements: List, + word: Word + ): Box? { + val wordPairs = + diagramElements + .map { box -> DiagramUtil.calculateHighestSimilarity(wordSimUtils, word, box) to box } + .filter { p -> p.first >= CommonTextToolsConfig.DE_WORD_SIMILARITY_THRESHOLD } + return wordPairs.maxWithOrNull(diagramElementSimilarity)?.second + } + + companion object { + private val logger = LoggerFactory.getLogger(DiagramBackedTextStateStrategy::class.java) + + /** + * Used to compare the similarity diagram element pairs. Using [java.util.stream.Stream.max] with this returns the diagram element with + * the highest similarity and shortest reference length. + */ + private var diagramElementSimilarity = + Comparator { p1: Pair, p2: Pair -> + val comp = p1.first.compareTo(p2.first) + if (comp == 0) { + // More "concise" diagram elements are preferable + return@Comparator p2.second.references.size.compareTo(p1.second.references.size) + } + comp + } + } +} diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/DiagramRecognitionMock.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/DiagramRecognitionMock.kt new file mode 100644 index 000000000..8fb8c0198 --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/DiagramRecognitionMock.kt @@ -0,0 +1,54 @@ +package edu.kit.kastel.mcse.ardoco.erid.diagramrecognition + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.UnicodeCharacterMatchFunctions +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimUtils +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.pipeline.ExecutionStage +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.agents.DiagramDisambiguationAgent +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.agents.DiagramReferenceAgent +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognitionStateImpl +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagrams +import java.util.SortedMap + +/** + * This stage is responsible for mocking the [edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition] stage. It populates the [DiagramRecognitionState] + * using the [GoldStandardDiagrams] gold standard. + */ +class DiagramRecognitionMock( + private val goldStandardProject: GoldStandardDiagrams?, + additionalConfigs: SortedMap?, + dataRepository: DataRepository? +) : + ExecutionStage( + listOf(DiagramDisambiguationAgent(dataRepository), DiagramReferenceAgent(dataRepository)), + DiagramRecognitionMock::class.java.getSimpleName(), + dataRepository, + additionalConfigs + ) { + private val wordSimUtils: WordSimUtils = getDataRepository().globalConfiguration.wordSimUtils + + override fun initializeState() { + logger.info("Creating DiagramRecognitionMock State") + val diagramRecognitionState = DiagramRecognitionStateImpl() + val diagrams = goldStandardProject?.getDiagramsGoldStandard() ?: listOf() + for (diagram in diagrams) { + logger.debug("Loaded Diagram {}", diagram.resourceName) + diagramRecognitionState.addDiagram(diagram) + } + getDataRepository().addData(DiagramRecognitionState.ID, diagramRecognitionState) + } + + private var previousCharacterMatchFunction: UnicodeCharacterMatchFunctions? = null + + override fun before() { + super.before() + previousCharacterMatchFunction = wordSimUtils.characterMatchFunction + wordSimUtils.characterMatchFunction = UnicodeCharacterMatchFunctions.EQUAL_OR_HOMOGLYPH + } + + override fun after() { + wordSimUtils.characterMatchFunction = previousCharacterMatchFunction + super.after() + } +} diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/agents/DiagramDisambiguationAgent.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/agents/DiagramDisambiguationAgent.kt new file mode 100644 index 000000000..4d09ce429 --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/agents/DiagramDisambiguationAgent.kt @@ -0,0 +1,16 @@ +package edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.agents + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.informants.DiagramDisambiguationInformant + +/** + * Responsible for disambiguating abbreviations in [DiagramElements][edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement]. + * + * @see DiagramDisambiguationInformant + */ +class DiagramDisambiguationAgent(dataRepository: DataRepository?) : PipelineAgent( + listOf(DiagramDisambiguationInformant(dataRepository)), + DiagramDisambiguationAgent::class.java.getSimpleName(), + dataRepository +) diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/agents/DiagramReferenceAgent.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/agents/DiagramReferenceAgent.kt new file mode 100644 index 000000000..c66fef8f7 --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/agents/DiagramReferenceAgent.kt @@ -0,0 +1,16 @@ +package edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.agents + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.informants.DiagramModelReferenceInformant + +/** + * Responsible for setting the references of [DiagramElements][edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement]. + * + * @see DiagramModelReferenceInformant + */ +class DiagramReferenceAgent(dataRepository: DataRepository?) : PipelineAgent( + listOf(DiagramModelReferenceInformant(dataRepository)), + DiagramReferenceAgent::class.java.getSimpleName(), + dataRepository +) diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/informants/DiagramDisambiguationInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/informants/DiagramDisambiguationInformant.kt new file mode 100644 index 000000000..cb6949656 --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/informants/DiagramDisambiguationInformant.kt @@ -0,0 +1,38 @@ +package edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.informants + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation +import edu.kit.kastel.mcse.ardoco.core.common.util.AbbreviationDisambiguationHelper +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant + +/** + * Responsible for disambiguating abbreviations that are contained in + * [DiagramElements][edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement] and their + * [TextBoxes][edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.TextBox]. + * + * @see AbbreviationDisambiguationHelper + */ +class DiagramDisambiguationInformant(dataRepository: DataRepository?) : Informant(DiagramDisambiguationInformant::class.java.getSimpleName(), dataRepository) { + /** + * Iterates over all diagram elements and their text boxes. Creates disambiguations for each contained abbreviation. + * + * @see AbbreviationDisambiguationHelper.getAbbreviationCandidates + * @see AbbreviationDisambiguationHelper.disambiguate + */ + public override fun process() { + val diagramRecognitionState = DataRepositoryHelper.getDiagramRecognitionState(dataRepository) + val boxes = diagramRecognitionState.getDiagrams().flatMap { d -> d.getBoxes() } + for (box in boxes) { + for (textBox in box.texts) { + val text = textBox.text + val abbreviations = AbbreviationDisambiguationHelper.getAbbreviationCandidates(text) + for (abbreviation in abbreviations) { + val disambiguation = AbbreviationDisambiguationHelper.disambiguate(abbreviation) + // TODO Add to transient abbreviation cache? Currently not sure about performance impact + diagramRecognitionState.addDisambiguation(Disambiguation(abbreviation, disambiguation.toTypedArray())) + } + } + } + } +} diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/informants/DiagramModelReferenceInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/informants/DiagramModelReferenceInformant.kt new file mode 100644 index 000000000..87bf5772c --- /dev/null +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/erid/diagramrecognition/informants/DiagramModelReferenceInformant.kt @@ -0,0 +1,242 @@ +package edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.informants + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Box +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.TextBox +import edu.kit.kastel.mcse.ardoco.core.api.models.ModelInstance +import edu.kit.kastel.mcse.ardoco.core.api.models.ModelStates +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper +import edu.kit.kastel.mcse.ardoco.core.common.util.DbPediaHelper +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant +import org.eclipse.collections.api.block.procedure.Procedure +import org.eclipse.collections.api.list.ImmutableList +import org.eclipse.collections.impl.factory.Lists +import kotlin.collections.LinkedHashMap +import kotlin.collections.LinkedHashSet + +/** + * Sets the references of [DiagramElements][edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement]. A reference is supposed to represent + * the elements as best as possible with as little unnecessary information as possible. Multiples reference can be calculated for a single element. The + * relationship between diagram elements and model elements is used to further reduce the set of references. + */ +class DiagramModelReferenceInformant(dataRepository: DataRepository?) : Informant(DiagramModelReferenceInformant::class.java.getSimpleName(), dataRepository) { + @Configurable + private var textBoxSimilarityThreshold = 0.5 + + /** + * Iterates over all diagram elements and sets their reference. + */ + public override fun process() { + val optModelStates = dataRepository.getData(ModelStates.ID, ModelStates::class.java) + if (optModelStates.isEmpty) { + val txt = String.format("%s couldn't be found, skipping informant", ModelStates::class.java.getSimpleName()) + logger.warn(txt) + return + } + val boxes: MutableList = + Lists.mutable.fromStream( + DataRepositoryHelper.getDiagramRecognitionState(dataRepository) + .getDiagrams() + .stream() + .flatMap { d: Diagram -> + d.getBoxes().stream() + } + ) + boxes.forEach( + Procedure { box: Box -> + setReferences( + box, + optModelStates.orElseThrow() + ) + } + ) + } + + /** + * Sets the references of each box. Individually calculates references for each text box first. Subsequently, calculates the most similar model instances + * for each text box and their references. If a text box is similar to a model instance, we assume that the diagram element may be the informal + * representation of the model instance. Thus, we remove all references except the references associated with the text box that is similar to the model + * instances. + * + * @param box the box + * @param modelStates the model states + */ + private fun setReferences( + box: Box, + modelStates: ModelStates + ) { + val modelIds = modelStates.modelIds() + for (model in modelIds) { + val modelState = modelStates.getModelExtractionState(model) + val instances = modelState.getInstances() + box.setReferences(listOf()) + val references = getReferencesPerTextBox(box) + val similar = similarModelInstance(instances, references) + similar.forEach { s -> logger.debug("{} similar to {}", box, s) } + + val isEmpty = similar.isEmpty() + for ((key, value) in references) { + if (isEmpty || similar.any { t: Triple -> t.first == key }) { + value.forEach { reference -> box.addReference(reference) } + } + } + } + } + + /** + * Tries to find the most similar model instance to all text boxes. If it exists, a triple is added to the list with the text box, the model instance and + * the similarity between them. + * + * @param modelInstances the model instances to search in + * @param references the reference map + * @return the list of triples, empty if no model instance is similar to any text box + */ + private fun similarModelInstance( + modelInstances: ImmutableList, + references: Map> + ): List> { + val list: MutableList> = Lists.mutable.of() + for ((textBox, textBoxRefs) in references) { + val pair = getMostSimilarModelInstance(modelInstances, textBox, textBoxRefs) + if (pair != null) { + list.add(Triple(textBox, pair.first, pair.second)) + } + } + return list + } + + /** + * Tries to find the most similar model instance to a particular text box. Compares both the entire text of the text box and the references to the instances + * full name. The most similar instance and its similarity value are encapsulated in a pair tuple. + * + * @param modelInstances the model instances + * @param textBox the text box + * @param references the references associated with the text box + * @return the pair of model instance and similarity or an empty optional if none exists. + */ + private fun getMostSimilarModelInstance( + modelInstances: ImmutableList, + textBox: TextBox, + references: Set + ): Pair? { + var max = Double.MIN_VALUE + val wordSimUtils = metaData.wordSimUtils + var mostSimilarModelInstance: ModelInstance? = null + for (instance in modelInstances) { + if (wordSimUtils.areWordsSimilar(textBox.text, instance.getFullName()) || + references.stream() + .anyMatch { ref: String -> + wordSimUtils.areWordsSimilar( + ref.lowercase(), + instance.getFullName().lowercase() + ) + } + ) { + val similarity = wordSimUtils.getSimilarity(textBox.text.lowercase(), instance.getFullName().lowercase()) + if (similarity > textBoxSimilarityThreshold && similarity > max) { + max = similarity + mostSimilarModelInstance = instance + } + } + } + return if (max > Double.MIN_VALUE) { + max to mostSimilarModelInstance + } else { + null + } + } + + /** + * {@return a map of references contained by the specified box} If a reference contains uppercase letters, its references take precedence over entirely + * lowercase references. + * + * @param box the box + */ + private fun getReferencesPerTextBox(box: Box): Map> { + val map = LinkedHashMap>() + val texts = box.texts + for (textBox in texts) { + map[textBox] = getReferences(textBox) + } + val atleastOneUpperCaseCharacterInTBox = + map.entries + .filter { (_, value): Map.Entry> -> + value.stream().anyMatch { s: String -> s != s.lowercase() } + }.associate { it.key to it.value } + return if (atleastOneUpperCaseCharacterInTBox.isNotEmpty()) atleastOneUpperCaseCharacterInTBox else map + } + + companion object { + /** + * Determines a set of possible references for a textBox. Tries to filter out technical terms using [DbPediaHelper]. + * + * @param textBox the textBox + * @return a set of possible names + */ + private fun getReferences(textBox: TextBox): Set { + val names = LinkedHashSet() + val text = textBox.text + if (!FILTER(text)) return names + val splitAndDecameled = processText(text).stream().filter(FILTER).toList() + val noBlank = splitAndDecameled.stream().map { s: String -> s.replace("\\s+".toRegex(), "") }.filter(FILTER).toList() + names.addAll(splitAndDecameled) + names.addAll(noBlank) + val atleastOneUpperCaseChar = names.filter { s: String -> s != s.lowercase() }.toSet() + return atleastOneUpperCaseChar.ifEmpty { names } + } + + private val FILTER = + { s: String? -> + !DbPediaHelper.isWordMarkupLanguage(s) && !DbPediaHelper.isWordProgrammingLanguage(s) && + !DbPediaHelper + .isWordSoftware(s) + } + + /** + * {@return a set of alternative texts extracted from the input text}. The text is processed with [.splitBracketsAndEnumerations] and + * [.getDeCameledText]. + * + * @param text the text + */ + private fun processText(text: String): Set { + val words = LinkedHashSet() + val split = splitBracketsAndEnumerations(text) + val deCameledSplit = + split.stream().map { word: String -> + getDeCameledText( + word + ) + }.toList() + words.addAll(split) + words.addAll(deCameledSplit) + words.remove("") + return words + } + + /** + * Splits the string around brackets and commas. The results are trimmed. Example: "Lorem (ipsum), Dolor, sit (Amet)" -> + * {"Lorem","ipsum","Dolor","sit","Amet"} + * + * @param text the text + * @return a non-empty list of splits + */ + private fun splitBracketsAndEnumerations(text: String): List { + return text.split("[,()]".toRegex()).dropLastWhile { it.isEmpty() } + .map { obj: String -> obj.trim { it <= ' ' } }.toList() + } + + /** + * Decamels the word and returns it as words joined by space. Example: "CamelCaseExample" -> "Camel Case Example", + * "example" -> "example", etc. + * + * @param word the word that should be decameled + * @return the decameled word + */ + private fun getDeCameledText(word: String): String { + return (word.split("(??, dataRepository: DataRepository? - ): DiagramRecognition? { + ): DiagramRecognition { val diagramDetection = DiagramRecognition(dataRepository!!) diagramDetection.applyConfiguration(additionalConfigs) return diagramDetection @@ -31,17 +47,29 @@ class DiagramRecognition(dataRepository: DataRepository) : AbstractExecutionStag } override fun initializeState() { - val inputDiagrams = dataRepository.getData(InputDiagramData.ID, InputDiagramData::class.java) + val diagramRecognitionState = DiagramRecognitionStateImpl() + dataRepository.addData(DiagramRecognitionState.ID, diagramRecognitionState) + + val inputDiagrams = + dataRepository.getData(InputDiagramData.ID, InputDiagramData::class.java) if (inputDiagrams.isEmpty) { return } - logger.info("Creating DiagramRecognition State") - val diagramRecognitionState = DiagramRecognitionStateImpl() - for (diagramFile in inputDiagrams.get().files) { - val diagram = DiagramImpl(diagramFile) - logger.debug("Loaded Diagram {}", diagramFile) - diagramRecognitionState.addDiagram(diagram) + logger.info("Initializing DiagramRecognition State") + for (diagramDatum in inputDiagrams.get().diagramData) { + val diagram = DiagramImpl(diagramDatum.first, diagramDatum.second) + diagramRecognitionState.addUnprocessedDiagram(diagram) } - dataRepository.addData(DiagramRecognitionState.ID, diagramRecognitionState) + } + + override fun before() { + super.before() + previousCharacterMatchFunction = wordSimUtils.characterMatchFunction + wordSimUtils.characterMatchFunction = UnicodeCharacterMatchFunctions.EQUAL_OR_HOMOGLYPH + } + + override fun after() { + wordSimUtils.characterMatchFunction = previousCharacterMatchFunction + super.after() } } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/DiagramRecognitionStateImpl.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/DiagramRecognitionStateImpl.kt index 6c82f2231..f63a9204b 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/DiagramRecognitionStateImpl.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/DiagramRecognitionStateImpl.kt @@ -1,13 +1,33 @@ package edu.kit.kastel.mcse.ardoco.lissa +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState class DiagramRecognitionStateImpl : DiagramRecognitionState { private val diagrams = mutableListOf() + private val unprocessedDiagrams = mutableListOf() + private val disambiguations = mutableListOf() + + override fun addUnprocessedDiagram(diagram: Diagram) { + unprocessedDiagrams.add(diagram) + } + + override fun getUnprocessedDiagrams(): List = unprocessedDiagrams.toList() + + override fun removeUnprocessedDiagram(diagram: Diagram): Boolean = + unprocessedDiagrams + .remove(diagram) + override fun addDiagram(diagram: Diagram) { diagrams.add(diagram) } override fun getDiagrams(): MutableList = diagrams.toMutableList() + + override fun addDisambiguation(disambiguation: Disambiguation): Boolean { + return disambiguations.add(disambiguation) + } + + override fun getDisambiguations(): MutableList = disambiguations.toMutableList() } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/CommonExtensions.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/CommonExtensions.kt index effa743e8..06a0aef62 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/CommonExtensions.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/CommonExtensions.kt @@ -7,8 +7,9 @@ import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.module.kotlin.registerKotlinModule fun createObjectMapper(): ObjectMapper { - val objectMapper: ObjectMapper = ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + val objectMapper: ObjectMapper = + ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) objectMapper.setVisibility( objectMapper.serializationConfig.defaultVisibilityChecker // .withFieldVisibility(JsonAutoDetect.Visibility.ANY) // @@ -25,7 +26,10 @@ fun List.with(other: E): List { return list.toList() } -fun Map.with(key: K, value: V): Map { +fun Map.with( + key: K, + value: V +): Map { val map = this.toMutableMap() map[key] = value return map.toMap() diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/RecognitionHelpers.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/RecognitionHelpers.kt index 94dc212de..b6af621e7 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/RecognitionHelpers.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/RecognitionHelpers.kt @@ -13,17 +13,25 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.awt.BasicStroke import java.awt.Color +import java.awt.Graphics import java.awt.Graphics2D +import java.awt.RenderingHints +import java.awt.Shape +import java.awt.font.GlyphVector +import java.awt.geom.AffineTransform +import java.awt.image.AffineTransformOp +import java.awt.image.BufferedImage import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.Double.max -import java.lang.Double.min -import java.util.UUID import java.util.concurrent.TimeUnit import javax.imageio.ImageIO +import kotlin.math.max +import kotlin.math.min -private val colors = listOf(Color.RED, Color.GREEN, Color.YELLOW, Color.BLUE, Color.BLACK, Color.ORANGE) +private var upscaleFactor = 1.0 +private val maxSize = 4096 +private val colors = listOf(Color.RED, Color.GREEN, Color(5, 93, 209), Color.YELLOW, Color.BLACK, Color.ORANGE) private val logger: Logger = LoggerFactory.getLogger("${DiagramRecognition::class.java.packageName}.Helpers") fun executeRequest( @@ -34,7 +42,8 @@ fun executeRequest( try { if (modifyTimeout) { val basicConfig = postRequest.config ?: RequestConfig.custom().build() - val newConfig = RequestConfig.copy(basicConfig).setResponseTimeout(5, TimeUnit.MINUTES).build() + val newConfig = + RequestConfig.copy(basicConfig).setResponseTimeout(5, TimeUnit.MINUTES).build() postRequest.config = newConfig } val content = it.execute(postRequest, BasicHttpClientResponseHandler()) @@ -49,38 +58,70 @@ fun executeRequest( fun visualize( imageStream: InputStream, diagram: Diagram, - destination: OutputStream + destination: OutputStream, + overlayScale: Float = 1F ) { - val image = ImageIO.read(imageStream) + val imageUnscaled = ImageIO.read(imageStream) + upscaleFactor = determineUpscaleFactor(imageUnscaled) + + var image = BufferedImage((imageUnscaled.width * upscaleFactor).toInt(), (imageUnscaled.height * upscaleFactor).toInt(), BufferedImage.TYPE_INT_ARGB) + val at = AffineTransform() + at.scale(upscaleFactor, upscaleFactor) + val scaleOp = AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR) + image = scaleOp.filter(imageUnscaled, image) + val g2d: Graphics2D = image.createGraphics() - g2d.stroke = BasicStroke(2F) + g2d.stroke = BasicStroke((3F * overlayScale)) + g2d.font = g2d.font.deriveFont((g2d.font.size * 4 * overlayScale)) val colorMap = mutableMapOf() var currentColor = 0 - for (box in diagram.boxes + diagram.textBoxes.map { it.toBox(true) } + diagram.boxes.flatMap { it.texts.map { tb -> tb.toBox(false) } }) { + // Draw Box + for (box in diagram.boxes + diagram.textBoxes.map { it.toBox(diagram, true) } + diagram.boxes.flatMap { it.texts.map { tb -> tb.toBox(diagram, false) } }) { if (!colorMap.containsKey(box.classification)) { colorMap[box.classification] = colors[currentColor]!! currentColor++ } + val col = colorMap[box.classification]!! + g2d.color = Color(col.red, col.green, col.blue, 255) + val coordinates = box.box + g2d.drawRect(toPixels(coordinates[0]), toPixels(coordinates[1]), toPixels(coordinates[2] - coordinates[0]), toPixels(coordinates[3] - coordinates[1])) + } + + // Draw Text + for (box in diagram.boxes + diagram.textBoxes.map { it.toBox(diagram, true) } + diagram.boxes.flatMap { it.texts.map { tb -> tb.toBox(diagram, false) } }) { g2d.color = colorMap[box.classification] val coordinates = box.box - g2d.drawRect(coordinates[0], coordinates[1], coordinates[2] - coordinates[0], coordinates[3] - coordinates[1]) if (box.classification == Classification.TEXT || box.classification == Classification.RAWTEXT) { - g2d.drawString( + paintTextWithOutline( + g2d, box.texts.joinToString { it.text }, - coordinates[0], - coordinates[1] + toPixels(coordinates[0]), + toPixels(coordinates[1] - 2), + colorMap[box.classification] + ) + } else { + paintTextWithOutline( + g2d, + Box.getBoundingBoxConcat(box.boundingBox.toCoordinates()), + toPixels(coordinates[0]), + toPixels(coordinates[1] - 2), + colorMap[box.classification] ) } } + g2d.dispose() ImageIO.write(image, "png", destination) } -private fun TextBox.toBox(rawBox: Boolean): Box = +private fun TextBox.toBox( + diagram: Diagram, + rawBox: Boolean +): Box = Box( - UUID.randomUUID().toString(), + diagram, this.absoluteBox().map { value -> value }.toIntArray(), 1.0, if (rawBox) "RAWTEXT" else "TEXT", @@ -149,5 +190,71 @@ fun IntArray.boundingBox(relative: Boolean = false): BoundingBox { this[3].toDouble() - this[1].toDouble() ) } - return BoundingBox(this[0].toDouble(), this[1].toDouble(), this[2].toDouble(), this[3].toDouble()) + return BoundingBox( + this[0].toDouble(), + this[1].toDouble(), + this[2].toDouble(), + this[3].toDouble() + ) +} + +private fun paintTextWithOutline( + g: Graphics?, + text: String?, + x: Int, + y: Int, + fillColor: Color? +) { + val outlineColor = Color.black + val outlineStroke = BasicStroke(3.0f) + if (g is Graphics2D) { + val g2 = g + + // remember original settings + val originalColor = g2.color + val originalStroke = g2.stroke + val originalHints = g2.renderingHints + + // create a glyph vector from your text + val glyphVector: GlyphVector = g2.font.createGlyphVector(g2.fontRenderContext, text) + // get the shape object + val textShape: Shape = glyphVector.outline + + // activate anti aliasing for text rendering (if you want it to look nice) + g2.setRenderingHint( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON + ) + g2.setRenderingHint( + RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY + ) + + // Position + g2.translate(x, y) + + g2.color = outlineColor + g2.stroke = outlineStroke + g2.draw(textShape) // draw outline + g2.color = fillColor + g2.fill(textShape) // fill the shape + + // reset to original settings after painting + g2.color = originalColor + g2.stroke = originalStroke + g2.setRenderingHints(originalHints) + + // Reset Position + g2.translate(-x, -y) + } +} + +private fun toPixels(value: Int): Int { + return (value * upscaleFactor).toInt() +} + +private fun determineUpscaleFactor(image: BufferedImage): Double { + val maxDim = max(image.width, image.height) + if (maxSize <= maxDim) return 1.0 + return maxSize.toDouble() / maxDim } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/agents/DiagramRecognitionAgent.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/agents/DiagramRecognitionAgent.kt index 5989fa328..165428dfb 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/agents/DiagramRecognitionAgent.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/agents/DiagramRecognitionAgent.kt @@ -1,25 +1,102 @@ package edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.agents +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper +import edu.kit.kastel.mcse.ardoco.core.common.util.SerializableFileBasedCache import edu.kit.kastel.mcse.ardoco.core.data.DataRepository import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.informants.ObjectDetectionInformant import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.informants.OcrInformant import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.informants.RecognitionCombinatorInformant +import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.model.SketchRecognitionResult +import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.visualize +import java.awt.Desktop +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream /** * This agent uses the [DiagramRecognitionState] to extract the diagrams and sketches from images. */ -class DiagramRecognitionAgent(dataRepository: DataRepository) : PipelineAgent( - listOf( - ObjectDetectionInformant(dataRepository), - OcrInformant(dataRepository), - RecognitionCombinatorInformant(dataRepository) - ), - ID, - dataRepository -) { +class DiagramRecognitionAgent( + dataRepository: DataRepository +) : PipelineAgent( + listOf( + ObjectDetectionInformant(dataRepository), + OcrInformant(dataRepository), + RecognitionCombinatorInformant(dataRepository) + ), + ID, + dataRepository + ) { companion object { const val ID = "DiagramRecognitionAgent" + const val ENV_DEBUG_VISUALIZE = "debugVisualize" + } + + override fun before() { + val diagramRecognitionState = DataRepositoryHelper.getDiagramRecognitionState(dataRepository) + for (diagram in diagramRecognitionState.getUnprocessedDiagrams()) { + val cached = + SerializableFileBasedCache( + SketchRecognitionResult::class + .java, + diagram.resourceName, + "diagram-recognition/" + ).getOrRead() + if (cached == null) { + logger.info("${diagram.resourceName} is not cached") + } else { + cached.boxes.forEach(diagram::addBox) + cached.textBoxes.forEach(diagram::addTextBox) + cached.edges.forEach(diagram::addConnector) + logger.info("${diagram.resourceName} loaded from cache") + diagramRecognitionState.addDiagram(diagram) + + debugVisualize(diagram) + + diagramRecognitionState.removeUnprocessedDiagram(diagram) + } + } + } + + override fun after() { + val diagramRecognitionState = DataRepositoryHelper.getDiagramRecognitionState(dataRepository) + for (diagram in diagramRecognitionState.getUnprocessedDiagrams()) { + diagramRecognitionState.addDiagram(diagram) + + logger.info("Caching {}", diagram.resourceName) + SerializableFileBasedCache( + SketchRecognitionResult::class + .java, + diagram.resourceName, + "diagram-recognition/" + ).use { + it.cache( + SketchRecognitionResult( + diagram.boxes, + diagram.textBoxes, + diagram.connectors + ) + ) + } + + debugVisualize(diagram) + } + } + + private fun debugVisualize(diagram: Diagram) { + if (System.getenv().getOrDefault(ENV_DEBUG_VISUALIZE, "false").toBoolean()) { + val destination = File.createTempFile("ArDoCo", ".png") + visualize( + FileInputStream(diagram.location), + diagram, + FileOutputStream(destination) + ) + if (Desktop.isDesktopSupported()) Desktop.getDesktop().open(destination) + } else { + logger.info("Set \"$ENV_DEBUG_VISUALIZE=true\" to enable diagram recognition visualization") + } } } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/DockerInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/DockerInformant.kt index b6db108ea..051fdbe29 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/DockerInformant.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/DockerInformant.kt @@ -1,11 +1,9 @@ package edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.informants -import com.fasterxml.jackson.databind.ObjectMapper import edu.kit.kastel.mcse.ardoco.core.data.DataRepository import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant import edu.kit.kastel.mcse.ardoco.docker.ContainerResponse import edu.kit.kastel.mcse.ardoco.docker.DockerManager -import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.createObjectMapper import org.apache.hc.client5.http.classic.methods.HttpGet import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler import org.apache.hc.client5.http.impl.classic.HttpClients @@ -20,6 +18,11 @@ abstract class DockerInformant : Informant { // E.g., 2375 private val REMOTE_DOCKER_PORT: Int? = System.getenv("REMOTE_DOCKER_PORT")?.toIntOrNull() + /** + * URI of a docker host, which hosts the endpoints used by this informant. E.g., https://some-domain.com + */ + private val REMOTE_DOCKER_URI: String? = System.getenv("REMOTE_DOCKER_URI") + private val REMOTE = REMOTE_DOCKER_IP != null && REMOTE_DOCKER_PORT != null private val dockerManagerCache: MutableMap = mutableMapOf() @@ -44,13 +47,10 @@ abstract class DockerInformant : Informant { } } - /** - * A configured object mapper for serialization / deserialization of objects. - */ - protected val oom: ObjectMapper = createObjectMapper() private val image: String private val defaultPort: Int - private val useDocker: Boolean + private var useDocker: Boolean + private val endpoint: String private var dockerManager: DockerManager? = null @@ -61,17 +61,20 @@ abstract class DockerInformant : Informant { * @param[useDocker] whether or not to use the docker image (just for debugging) * @param[id] the id of the informant * @param[dataRepository] the data repository of the informant + * @param[endpoint] the endpoint of the informant (e.g., "ocr" to access "http://IP:Port/ocr"). */ protected constructor( image: String, defaultPort: Int, useDocker: Boolean, id: String, - dataRepository: DataRepository + dataRepository: DataRepository, + endpoint: String ) : super(id, dataRepository) { this.image = image this.defaultPort = defaultPort this.useDocker = useDocker + this.endpoint = endpoint } /** @@ -83,12 +86,15 @@ abstract class DockerInformant : Informant { /** * The information about the spawned container (e.g., the information about the port mapping) */ + @Transient protected lateinit var container: ContainerResponse /** * Start the container. */ protected fun start() { + useDocker = useDocker && REMOTE_DOCKER_URI == null + if (useDocker) { this.container = docker().createContainerByImage(image, true, false) } else { @@ -105,6 +111,14 @@ abstract class DockerInformant : Informant { } } + /** + * @return the URI to the docker service. [REMOTE_DOCKER_URI] takes precedence over [REMOTE_DOCKER_IP] if set. + */ + protected fun getUri(): String { + if (REMOTE_DOCKER_URI != null) return "$REMOTE_DOCKER_URI/$endpoint/" + return "http://${hostIP()}:${container.apiPort}/$endpoint/" + } + private fun docker(): DockerManager { if (!useDocker) { error("Try to get docker while docker is disabled") @@ -117,17 +131,17 @@ abstract class DockerInformant : Informant { } /** - * Ensure the readiness of the container or service by its entrypoint (e.g., "ocr" to access "http://IP:Port/ocr"). + * Ensure the readiness of the container or service by its entrypoint * @throws[IllegalStateException] if failed after multiple retries */ - protected fun ensureReadiness(entryPoint: String) { + protected fun ensureReadiness() { val tries = 15 val waiting = 10000L HttpClients.createDefault().use { client -> for (currentTry in IntStream.range(0, tries)) { try { - val get = HttpGet("http://${hostIP()}:${container.apiPort}/$entryPoint/") + val get = HttpGet(getUri()) val data = client.execute(get, BasicHttpClientResponseHandler()) if (data.startsWith("Hello from ")) return } catch (e: IOException) { diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ImageProcessingDockerInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ImageProcessingDockerInformant.kt index 51b7a60f7..8efde2041 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ImageProcessingDockerInformant.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ImageProcessingDockerInformant.kt @@ -1,8 +1,11 @@ package edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.informants +import com.fasterxml.jackson.databind.InjectableValues +import com.fasterxml.jackson.databind.ObjectMapper import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.createObjectMapper abstract class ImageProcessingDockerInformant( image: String, @@ -10,18 +13,25 @@ abstract class ImageProcessingDockerInformant( useDocker: Boolean, id: String, dataRepository: DataRepository, - private val defaultEndpoint: String + endpoint: String ) : DockerInformant( - image, - defaultPort, - useDocker, - id, - dataRepository -) { - final override fun run() { + image, + defaultPort, + useDocker, + id, + dataRepository, + endpoint + ) { + /** + * A configured object mapper for serialization / deserialization of objects. + */ + @Transient + protected var oom: ObjectMapper = createObjectMapper() + + final override fun process() { try { start() - ensureReadiness(defaultEndpoint) + ensureReadiness() processImages() } catch (e: Exception) { logger.error(e.message, e) @@ -32,7 +42,15 @@ abstract class ImageProcessingDockerInformant( private fun processImages() { val diagramRecognitionState = DataRepositoryHelper.getDiagramRecognitionState(dataRepository) - for (diagram in diagramRecognitionState.diagrams) { + for (diagram in diagramRecognitionState.getUnprocessedDiagrams()) { + // Inject diagram into mapper + oom.setInjectableValues( + InjectableValues.Std().addValue( + Diagram::class.java, + diagram + ) + ) + logger.debug("Process {}", diagram.location) diagram.location.readBytes().let { imageStream -> processImage(diagram, imageStream) @@ -40,5 +58,8 @@ abstract class ImageProcessingDockerInformant( } } - protected abstract fun processImage(diagram: Diagram, imageData: ByteArray) + protected abstract fun processImage( + diagram: Diagram, + imageData: ByteArray + ) } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ObjectDetectionInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ObjectDetectionInformant.kt index c9e14e8d2..5fa774313 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ObjectDetectionInformant.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/ObjectDetectionInformant.kt @@ -13,14 +13,16 @@ import java.io.ByteArrayInputStream import java.io.InputStream import java.util.SortedMap -class ObjectDetectionInformant(dataRepository: DataRepository) : ImageProcessingDockerInformant( - DOCKER_SKETCH_RECOGNITION, - DEFAULT_PORT, - DOCKER_SKETCH_RECOGNITION_VIA_DOCKER, - ID, - dataRepository, - "sketches" -) { +class ObjectDetectionInformant( + dataRepository: DataRepository +) : ImageProcessingDockerInformant( + DOCKER_SKETCH_RECOGNITION, + DEFAULT_PORT, + DOCKER_SKETCH_RECOGNITION_VIA_DOCKER, + ID, + dataRepository, + "sketches" + ) { companion object { const val DOCKER_SKETCH_RECOGNITION = "ghcr.io/lissa-approach/detectron2-sr:latest" const val DEFAULT_PORT = 5005 @@ -37,11 +39,14 @@ class ObjectDetectionInformant(dataRepository: DataRepository) : ImageProcessing diagram: Diagram, imageData: ByteArray ) { - val boxes = detectEntities(ByteArrayInputStream(imageData)) + val boxes = detectEntities(diagram, ByteArrayInputStream(imageData)) boxes.forEach { diagram.addBox(it) } } - fun detectEntities(image: InputStream): List { + fun detectEntities( + diagram: Diagram, + image: InputStream + ): List { val sketchRecognition = sendSketchRecognitionRequest(image) logger.debug("Processed DiagramRecognition request") return oom.readValue(sketchRecognition) @@ -51,7 +56,7 @@ class ObjectDetectionInformant(dataRepository: DataRepository) : ImageProcessing // Create Request val builder = MultipartEntityBuilder.create() builder.addBinaryBody("file", image, ContentType.APPLICATION_OCTET_STREAM, "image") - val uploadFile = HttpPost("http://${hostIP()}:${container.apiPort}/sketches/") + val uploadFile = HttpPost(getUri()) val multipart: HttpEntity = builder.build() uploadFile.entity = multipart return executeRequest(uploadFile, true) diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/OcrInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/OcrInformant.kt index 966ef469b..1a53a39d1 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/OcrInformant.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/OcrInformant.kt @@ -19,14 +19,16 @@ import java.io.InputStream import java.net.URI import java.util.SortedMap -class OcrInformant(dataRepository: DataRepository) : ImageProcessingDockerInformant( - DOCKER_OCR, - DEFAULT_PORT, - DOCKER_OCR_VIA_DOCKER, - ID, - dataRepository, - "ocr" -) { +class OcrInformant( + dataRepository: DataRepository +) : ImageProcessingDockerInformant( + DOCKER_OCR, + DEFAULT_PORT, + DOCKER_OCR_VIA_DOCKER, + ID, + dataRepository, + "ocr" + ) { companion object { const val ID = "OCRInformant" const val DOCKER_OCR = "ghcr.io/lissa-approach/diagram-ocr:latest" @@ -54,7 +56,11 @@ class OcrInformant(dataRepository: DataRepository) : ImageProcessingDockerInform diagram: Diagram, imageData: ByteArray ) { - val textsWithHints = detectTextBoxes(ByteArrayInputStream(imageData), diagram.boxes.filter { it.classification == Classification.LABEL }) + val textsWithHints = + detectTextBoxes( + ByteArrayInputStream(imageData), + diagram.boxes.filter { it.classification == Classification.LABEL } + ) val textsWithoutHints = detectTextBoxes(ByteArrayInputStream(imageData), listOf()) val texts = mergeTexts(textsWithHints, textsWithoutHints) texts.forEach { diagram.addTextBox(it) } @@ -88,7 +94,6 @@ class OcrInformant(dataRepository: DataRepository) : ImageProcessingDockerInform val textRecognition = sendOCRRequest( image, - container.apiPort, detectedBoxesOfObjectDetection.filter { it.classification == Classification.LABEL } ) logger.debug("Processed OCRService Request") @@ -97,7 +102,6 @@ class OcrInformant(dataRepository: DataRepository) : ImageProcessingDockerInform private fun sendOCRRequest( image: InputStream, - port: Int, labels: List ): String { val boxCoordinates = enhanceLabels(labels).flatMap { it.box.toList() }.joinToString(",") @@ -105,9 +109,10 @@ class OcrInformant(dataRepository: DataRepository) : ImageProcessingDockerInform val builder = MultipartEntityBuilder.create() builder.addBinaryBody("file", image, ContentType.APPLICATION_OCTET_STREAM, "image") val multipart: HttpEntity = builder.build() - val uploadFile = HttpPost("http://${hostIP()}:$port/ocr/") + val uploadFile = HttpPost(getUri()) if (labels.isNotEmpty()) { - val uri: URI = URIBuilder(uploadFile.uri).addParameter("regions", boxCoordinates).build() + val uri: URI = + URIBuilder(uploadFile.uri).addParameter("regions", boxCoordinates).build() uploadFile.uri = uri } uploadFile.entity = multipart @@ -134,6 +139,14 @@ class OcrInformant(dataRepository: DataRepository) : ImageProcessingDockerInform box.box[3] + EXPANSION_IN_PX ) // Copy References here. No Copies! - return Box(box.uuid, newPositions.toIntArray(), box.confidence, box.classification.classificationString, box.texts, null) + return Box( + box.diagram, + newPositions.toIntArray(), + box.confidence, + box.classification + .classificationString, + box.texts, + null + ) } } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/RecognitionCombinatorInformant.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/RecognitionCombinatorInformant.kt index 1763736f9..0be356f68 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/RecognitionCombinatorInformant.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/informants/RecognitionCombinatorInformant.kt @@ -8,13 +8,20 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper import edu.kit.kastel.mcse.ardoco.core.data.DataRepository import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.boundingBox +import java.awt.Color import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.util.SortedMap import java.util.stream.IntStream import javax.imageio.ImageIO -class RecognitionCombinatorInformant(dataRepository: DataRepository) : Informant(ID, dataRepository) { +class RecognitionCombinatorInformant( + dataRepository: DataRepository +) : + Informant( + ID, + dataRepository + ) { companion object { const val ID = "RecognitionCombinatorInformant" } @@ -23,31 +30,66 @@ class RecognitionCombinatorInformant(dataRepository: DataRepository) : Informant // Not needed } - override fun run() { + override fun process() { val diagramRecognitionState = DataRepositoryHelper.getDiagramRecognitionState(dataRepository) - for (diagram in diagramRecognitionState.diagrams) { + for (diagram in diagramRecognitionState.getUnprocessedDiagrams()) { val entities = diagram.boxes.filter { it.classification != Classification.LABEL } val texts = diagram.textBoxes + removeLabelBoxes(diagram) combineBoxesAndText(entities, texts) calculateDominatingColors(diagram.location.readBytes(), entities) combineTextBoxesInBoxes(diagram) } } + private fun removeLabelBoxes(diagram: Diagram) { + diagram.boxes.filter { it.classification == Classification.LABEL }.forEach(diagram::removeBox) + } + private fun combineBoxesAndText( entities: List, - texts: List + textsUnfiltered: List ) { - for (text in texts) { - if (text.text.length < 3) continue + // Boxes without parents are tree roots + var boxes = entities.filter { it.parent.isEmpty } + var texts = textsUnfiltered.filter { it.text.length >= 2 } + + for (box in boxes) { + // TODO Could also only pass the remaining texts here instead of passing all texts to each node, but will leave it like this for now, not sure of the impact + combineBoxesAndTextForTree(box, texts) + } + } - val intersects = entities.map { it to it.box.boundingBox().iou(text.absoluteBox().boundingBox()) } + /** + * Depth first search + */ + private fun combineBoxesAndTextForTree( + root: Box, + texts: List + ): List { + val children = root.children - val results = intersects.filter { it.second.areaIntersect / text.area() > 0.9 } - if (results.isEmpty()) continue - logger.info("Found {} intersects with {}", intersects.size, text.text) - results.forEach { it.first.addTextBox(text) } + var remainingTexts = texts + for (child in children) { + if (child is Box) { + remainingTexts = combineBoxesAndTextForTree(child, remainingTexts) + } } + + val mutableRemainingTexts = remainingTexts.toMutableList() + + val intersects = + remainingTexts.map { it to root.box.boundingBox().iou(it.absoluteBox().boundingBox()) } + + val results = intersects.filter { it.second.areaIntersect / it.first.area() > 0.85 } + + results.forEach { + logger.info("Found {} intersects with {}", intersects.size, it.first.text) + root.addTextBox(it.first) + mutableRemainingTexts.remove(it.first) + } + + return mutableRemainingTexts } private fun calculateDominatingColors( @@ -67,11 +109,12 @@ class RecognitionCombinatorInformant(dataRepository: DataRepository) : Informant val count = pixels.size if (count == 0) return - val pixelCount = pixels.groupingBy { it }.eachCount().toList().sortedByDescending { it.second } + val pixelCount = + pixels.groupingBy { it }.eachCount().toList().sortedByDescending { it.second } val mostPixel = pixelCount[0] if (mostPixel.second <= count / 2) return - box.dominatingColor = mostPixel.first + box.dominatingColor = Color(mostPixel.first) setColorsOfTexts(image, box) } @@ -80,7 +123,10 @@ class RecognitionCombinatorInformant(dataRepository: DataRepository) : Informant box: Array ): List { val result = mutableListOf() - for (x in IntStream.range(box[0], box[2])) for (y in IntStream.range(box[1], box[3])) result.add( + for (x in IntStream.range(box[0], box[2])) for (y in IntStream.range( + box[1], + box[3] + )) result.add( image.getRGB(x, y) ) return result @@ -94,9 +140,10 @@ class RecognitionCombinatorInformant(dataRepository: DataRepository) : Informant val pixels = getPixels(image, text.absoluteBox().toTypedArray()) val count = pixels.size if (count == 0) continue - val pixelCount = pixels.groupingBy { it }.eachCount().toList().sortedByDescending { it.second } - val textColor = pixelCount.find { (rgba, _) -> rgba != box.dominatingColor } - if (textColor != null) text.dominatingColor = textColor.first + val pixelCount = + pixels.groupingBy { it }.eachCount().toList().sortedByDescending { it.second } + val textColor = pixelCount.find { (rgb, _) -> rgb != box.dominatingColor.rgb } + if (textColor != null) text.dominatingColor = Color(textColor.first) } } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/DiagramImpl.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/DiagramImpl.kt index 16ddfb5bc..ee9295194 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/DiagramImpl.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/DiagramImpl.kt @@ -5,14 +5,30 @@ import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Connector import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.TextBox import java.io.File +import java.util.Objects -class DiagramImpl(private val location: File) : +class DiagramImpl : Diagram { - private val boxes: MutableList = mutableListOf() - private val textBoxes: MutableList = mutableListOf() - private val connectors: MutableList = mutableListOf() + private val resourceName: String + private val location: File - private constructor() : this(File("")) { + constructor(resourceName: String, location: File) { + this.resourceName = resourceName + this.location = location + this.boxes = mutableListOf() + this.textBoxes = mutableListOf() + this.connectors = mutableListOf() + } + + private val boxes: MutableList + private val textBoxes: MutableList + private val connectors: MutableList + + private constructor() : this("", File("")) { + } + + override fun getResourceName(): String { + return resourceName } override fun getLocation(): File = location @@ -46,4 +62,16 @@ class DiagramImpl(private val location: File) : override fun getTextBoxes(): MutableList = textBoxes.toMutableList() override fun getConnectors(): MutableList = connectors.toMutableList() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other is Diagram) { + return boxes == other.boxes && textBoxes == other.textBoxes && connectors == other.connectors + } + return false + } + + override fun hashCode(): Int { + return Objects.hash(boxes, textBoxes, connectors) + } } diff --git a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/SketchRecognitionResult.kt b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/SketchRecognitionResult.kt index d766725b4..31e2fa070 100644 --- a/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/SketchRecognitionResult.kt +++ b/stages/diagram-recognition/src/main/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/model/SketchRecognitionResult.kt @@ -3,5 +3,14 @@ package edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.model import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Box import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Connector import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.TextBox +import java.io.Serializable -data class SketchRecognitionResult(val boxes: List, val textBoxes: List, val edges: List) +data class SketchRecognitionResult( + val boxes: List, + val textBoxes: List, + val edges: List +) : Serializable { + companion object { + private const val serialVersionUID: Long = 13L + } +} diff --git a/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/services/SketchRecognitionServiceTest.kt b/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/services/SketchRecognitionServiceTest.kt index 72c6f8391..3dff994fd 100644 --- a/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/services/SketchRecognitionServiceTest.kt +++ b/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/lissa/diagramrecognition/services/SketchRecognitionServiceTest.kt @@ -6,12 +6,18 @@ import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognition import edu.kit.kastel.mcse.ardoco.core.data.DataRepository import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition import edu.kit.kastel.mcse.ardoco.lissa.diagramrecognition.visualize +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagrams +import org.eclipse.collections.api.factory.SortedMaps import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource import org.slf4j.Logger import org.slf4j.LoggerFactory import java.awt.Color @@ -29,7 +35,8 @@ class SketchRecognitionServiceTest { const val PATH = "src/test/resources/bbb-arch-overview.png" const val PATH_TO_HL_ARCHITECTURE = "src/test/resources/highlevelArchitecture.png" - private val logger: Logger = LoggerFactory.getLogger(SketchRecognitionServiceTest::class.java) + private val logger: Logger = + LoggerFactory.getLogger(SketchRecognitionServiceTest::class.java) } private lateinit var dataRepository: DataRepository @@ -39,14 +46,15 @@ class SketchRecognitionServiceTest { assumeDocker() dataRepository = DataRepository() dataRepository.addData(InputDiagramData.ID, InputDiagramData("src/test/resources/")) - val stage = DiagramRecognition(dataRepository) + val stage = DiagramRecognition.get(SortedMaps.mutable.empty(), dataRepository)!! stage.run() assertNotNull(getState()) File("target/testout").mkdirs() } private fun assumeDocker() { - val remoteDocker = System.getenv("REMOTE_DOCKER_IP") != null && System.getenv("REMOTE_DOCKER_PORT") != null + val remoteDocker = + System.getenv("REMOTE_DOCKER_IP") != null && System.getenv("REMOTE_DOCKER_PORT") != null || System.getenv("REMOTE_DOCKER_URI") != null var localDocker = true try { val result = Runtime.getRuntime().exec("docker ps") @@ -59,31 +67,37 @@ class SketchRecognitionServiceTest { Assumptions.assumeTrue(remoteDocker || localDocker, "Docker is not available") } - private fun getState() = dataRepository.getData(DiagramRecognitionState.ID, DiagramRecognitionState::class.java).orElse(null) + private fun getState() = + dataRepository.getData(DiagramRecognitionState.ID, DiagramRecognitionState::class.java) + .orElse(null) @Test fun testSimpleRecognitionWithColors() { val file = File(PATH_TO_HL_ARCHITECTURE) val diagram = getState().diagrams.find { it.location == file }!! - Assertions.assertEquals(8, diagram.boxes.filter { it.classification != Classification.LABEL }.size) - Assertions.assertEquals(35, diagram.textBoxes.size) + Assertions.assertEquals( + 8, + diagram.boxes.filter { it.classification != Classification.LABEL }.size + ) - val testDriver = diagram.boxes.filter { it.texts.any { tb -> tb.text.lowercase().contains("driver") } } + val testDriver = + diagram.boxes.filter { it.texts.any { tb -> tb.text.lowercase().contains("driver") } } Assertions.assertEquals(1, testDriver.size) val testColor = testDriver[0].dominatingColor - Assertions.assertEquals(Color(217, 150, 148).rgb, testColor) + Assertions.assertEquals(Color(217, 150, 148), testColor) - val logic = diagram.boxes.filter { it.texts.any { tb -> tb.text.lowercase().contains("logic") } } + val logic = + diagram.boxes.filter { it.texts.any { tb -> tb.text.lowercase().contains("logic") } } Assertions.assertEquals(1, logic.size) val logicColor = logic[0].dominatingColor - Assertions.assertEquals(Color(179, 162, 199).rgb, logicColor) + Assertions.assertEquals(Color(179, 162, 199), logicColor) val texts = logic[0].texts Assertions.assertEquals(2, texts.size) val javaText = texts.find { it.text.lowercase().contains("java") }!! val logicText = texts.find { it.text.lowercase().contains("logic") }!! - Assertions.assertEquals(Color(255, 255, 255).rgb, javaText.dominatingColor) - Assertions.assertEquals(Color(96, 74, 123).rgb, logicText.dominatingColor) + Assertions.assertEquals(Color(255, 255, 255), javaText.dominatingColor) + Assertions.assertEquals(Color(96, 74, 123), logicText.dominatingColor) val destination = File("target/testout/result_testSimpleRecognitionWithColors.png") visualize( @@ -107,4 +121,36 @@ class SketchRecognitionServiceTest { ) if (Desktop.isDesktopSupported()) Desktop.getDesktop().open(destination) } + + /** + * Use to visualize diagram recognition for all diagram project diagrams + */ + @EnabledIfEnvironmentVariable(named = "testVisualizeAll", matches = ".*") + @DisplayName("Visualize Diagram Project Sketch Recognition") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#values") + fun visualizeAll(diagramProject: GoldStandardDiagrams) { + val diagramData = diagramProject.diagramData + val dataRepository = DataRepository() + dataRepository.addData(InputDiagramData.ID, InputDiagramData(diagramData)) + val stage = DiagramRecognition.get(SortedMaps.mutable.empty(), dataRepository) + stage.run() + val state = + dataRepository.getData(DiagramRecognitionState.ID, DiagramRecognitionState::class.java) + .orElse(null) + assertNotNull(state) + File("target/testout").mkdirs() + + for (inputDiagram in diagramData) { + val diagram = state.diagrams.find { it.resourceName.equals(inputDiagram.first) }!! + val destination = File("target/testout/result_" + diagramProject.projectName + "_" + diagram.shortResourceName) + visualize( + FileInputStream(inputDiagram.second), + diagram, + FileOutputStream(destination), + 2f + ) + if (Desktop.isDesktopSupported()) Desktop.getDesktop().open(destination) + } + } } diff --git a/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramRecognitionMockTest.kt b/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramRecognitionMockTest.kt new file mode 100644 index 000000000..2ba1cb569 --- /dev/null +++ b/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramRecognitionMockTest.kt @@ -0,0 +1,98 @@ +package edu.kit.kastel.mcse.ardoco.tests.integration + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep +import edu.kit.kastel.mcse.ardoco.erid.diagramrecognition.DiagramRecognitionMock +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagrams +import edu.kit.kastel.mcse.ardoco.tests.eval.StageTest +import org.eclipse.collections.impl.factory.SortedMaps +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.util.SortedMap + +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +internal class DiagramRecognitionMockTest : StageTest( + DiagramRecognitionMock( + null, + SortedMaps.mutable.empty(), + DataRepository() + ), + DiagramProject.entries.toTypedArray() +) { + @DisplayName("Evaluate Diagram Recognition") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + @Disabled + fun evaluateNonHistoricalDiagramRecognition(project: DiagramProject?) { + run(project) + } + + @DisplayName("Evaluate Diagram Recognition (Historical)") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(2) + @Disabled + fun evaluateHistoricalDiagramRecognition(project: DiagramProject?) { + run(project) + } + + protected override fun runComparable( + project: GoldStandardDiagrams, + additionalConfigurations: SortedMap, + cachePreRun: Boolean + ): DiagramRecognitionResult { + val result = run(project, additionalConfigurations, cachePreRun) + val diagramRecognition = result.getData(DiagramRecognitionState.ID, DiagramRecognitionState::class.java).orElseThrow() + val diagrams = diagramRecognition.getDiagrams() + return DiagramRecognitionResult(diagrams) + } + + protected override fun runPreTestRunner(project: GoldStandardDiagrams): DataRepository { + return object : AnonymousRunner(project.getProjectName()) { + override fun initializePipelineSteps(dataRepository: DataRepository): List { + dataRepository.globalConfiguration.wordSimUtils.considerAbbreviations = true + val pipelineSteps = ArrayList() + val arCoTLModelProviderAgent = + ArCoTLModelProviderAgent.get( + project.modelFile, + ArchitectureModelType.PCM, + null, + project.additionalConfigurations, + dataRepository + ) + pipelineSteps.add(arCoTLModelProviderAgent) + return pipelineSteps + } + }.runWithoutSaving() + } + + protected override fun runTestRunner( + project: GoldStandardDiagrams, + additionalConfigurations: SortedMap, + preRunDataRepository: DataRepository + ): DataRepository { + return object : AnonymousRunner(project.getProjectName(), preRunDataRepository) { + override fun initializePipelineSteps(dataRepository: DataRepository): List { + dataRepository.globalConfiguration.wordSimUtils.considerAbbreviations = true + val pipelineSteps = ArrayList() + pipelineSteps.add(DiagramRecognitionMock(project, project.additionalConfigurations, dataRepository)) + return pipelineSteps + } + }.runWithoutSaving() + } + + @JvmRecord + data class DiagramRecognitionResult(val diagrams: List) +} diff --git a/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramRecognitionTest.kt b/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramRecognitionTest.kt new file mode 100644 index 000000000..a2f5bda89 --- /dev/null +++ b/stages/diagram-recognition/src/test/kotlin/edu/kit/kastel/mcse/ardoco/tests/integration/DiagramRecognitionTest.kt @@ -0,0 +1,111 @@ +package edu.kit.kastel.mcse.ardoco.tests.integration + +import edu.kit.kastel.mcse.ardoco.core.api.InputDiagramData +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramGS +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramRecognitionState +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType +import edu.kit.kastel.mcse.ardoco.core.common.util.Comparators +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep +import edu.kit.kastel.mcse.ardoco.lissa.DiagramRecognition +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject +import edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagrams +import edu.kit.kastel.mcse.ardoco.tests.eval.StageTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Order +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.util.SortedMap + +internal class DiagramRecognitionTest : + StageTest( + DiagramRecognition(DataRepository()), + DiagramProject.entries.toTypedArray() + ) { + @DisplayName("Evaluate Diagram Recognition") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + fun evaluateNonHistoricalDiagramRecognition(project: DiagramProject) { + val result = runComparable(project) + Assertions.assertTrue( + Comparators.collectionsEqualsAnyOrder( + result!!.diagrams.map { obj: Diagram -> obj.getShortResourceName() }.toList(), + project.diagramsGoldStandard + .stream() + .map { obj: DiagramGS -> obj.getShortResourceName() } + .toList() + ) + ) + } + + @DisplayName("Evaluate Diagram Recognition (Historical)") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getHistoricalProjects") + @Order(2) + fun evaluateHistoricalDiagramRecognition(project: DiagramProject) { + val result = runComparable(project) + Assertions.assertTrue( + Comparators.collectionsEqualsAnyOrder( + result!!.diagrams.stream().map { obj: Diagram -> obj.getShortResourceName() }.toList(), + project.diagramsGoldStandard + .stream() + .map { obj: DiagramGS -> obj.getShortResourceName() } + .toList() + ) + ) + } + + protected override fun runComparable( + project: GoldStandardDiagrams, + additionalConfigurations: SortedMap, + cachePreRun: Boolean + ): DiagramRecognitionResult { + val result = run(project, additionalConfigurations, cachePreRun) + val diagramRecognition = result.getData(DiagramRecognitionState.ID, DiagramRecognitionState::class.java).orElseThrow() + val diagrams = diagramRecognition.getDiagrams() + return DiagramRecognitionResult(diagrams) + } + + protected override fun runPreTestRunner(project: GoldStandardDiagrams): DataRepository { + return object : AnonymousRunner(project.getProjectName()) { + override fun initializePipelineSteps(dataRepository: DataRepository): List { + dataRepository.globalConfiguration.wordSimUtils.considerAbbreviations = true + val pipelineSteps = ArrayList() + val arCoTLModelProviderAgent = + ArCoTLModelProviderAgent.get( + project.modelFile, + ArchitectureModelType.PCM, + null, + project.additionalConfigurations, + dataRepository + ) + pipelineSteps.add(arCoTLModelProviderAgent) + return pipelineSteps + } + }.runWithoutSaving() + } + + protected override fun runTestRunner( + project: GoldStandardDiagrams, + additionalConfigurations: SortedMap, + preRunDataRepository: DataRepository + ): DataRepository { + return object : AnonymousRunner(project.getProjectName(), preRunDataRepository) { + override fun initializePipelineSteps(dataRepository: DataRepository): List { + dataRepository.globalConfiguration.wordSimUtils.considerAbbreviations = true + val pipelineSteps = ArrayList() + dataRepository.addData(InputDiagramData.ID, InputDiagramData(project.getDiagramData())) + pipelineSteps.add(DiagramRecognition(dataRepository)) + return pipelineSteps + } + }.runWithoutSaving() + } + + @JvmRecord + data class DiagramRecognitionResult(val diagrams: List) +} diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyChecker.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyChecker.java index 68e68712d..f60613412 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyChecker.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyChecker.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency; import java.util.List; diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStateImpl.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStateImpl.java index 4b42e8187..86a166f5b 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStateImpl.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStateImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency; import java.util.List; @@ -14,10 +14,11 @@ public class InconsistencyStateImpl extends AbstractState implements InconsistencyState { - private transient MutableList recommendedInstances; - private transient MutableList inconsistencies; + private MutableList recommendedInstances; + private MutableList inconsistencies; public InconsistencyStateImpl() { + super(); inconsistencies = Lists.mutable.empty(); recommendedInstances = Lists.mutable.empty(); } diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStatesImpl.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStatesImpl.java index 7add27f92..0ae847100 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStatesImpl.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/InconsistencyStatesImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency; import java.util.EnumMap; diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/MissingElementInconsistencyCandidate.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/MissingElementInconsistencyCandidate.java index 4f0b7f3b5..98292d19f 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/MissingElementInconsistencyCandidate.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/MissingElementInconsistencyCandidate.java @@ -1,6 +1,7 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency; +import java.io.Serializable; import java.util.Objects; import org.eclipse.collections.api.factory.SortedSets; @@ -8,7 +9,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; -public class MissingElementInconsistencyCandidate { +public class MissingElementInconsistencyCandidate implements Serializable { private final RecommendedInstance recommendedInstance; private final MutableSortedSet supports = SortedSets.mutable.empty(); diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/agents/UndocumentedModelElementInconsistencyAgent.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/agents/UndocumentedModelElementInconsistencyAgent.java index 79a7fc658..fbe032afb 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/agents/UndocumentedModelElementInconsistencyAgent.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/agents/UndocumentedModelElementInconsistencyAgent.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency.agents; import java.util.List; @@ -8,8 +8,8 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; /** - * This agent analyses the model to find elements within the model that are not documented in the text. - * For this, it uses the {@link UndocumentedModelElementInconsistencyInformant}. See it for more information about configuration options. + * This agent analyses the model to find elements within the model that are not documented in the text. For this, it uses the + * {@link UndocumentedModelElementInconsistencyInformant}. See it for more information about configuration options. */ public class UndocumentedModelElementInconsistencyAgent extends PipelineAgent { diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/Filter.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/Filter.java index 30df0cbe0..4ee8d4e0c 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/Filter.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/Filter.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency.informants; import edu.kit.kastel.mcse.ardoco.core.api.inconsistency.InconsistencyState; @@ -21,7 +21,7 @@ protected Filter(String id, DataRepository dataRepository) { } @Override - public void run() { + public void process() { var dataRepository = getDataRepository(); var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); var inconsistencyStates = DataRepositoryHelper.getInconsistencyStates(dataRepository); diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/MissingModelElementInconsistencyInformant.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/MissingModelElementInconsistencyInformant.java index ff865a573..1fe3e1388 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/MissingModelElementInconsistencyInformant.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/informants/MissingModelElementInconsistencyInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency.informants; import java.util.SortedMap; @@ -34,7 +34,7 @@ public MissingModelElementInconsistencyInformant(DataRepository dataRepository) } @Override - public void run() { + public void process() { var dataRepository = getDataRepository(); var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); var connectionStates = DataRepositoryHelper.getConnectionStates(dataRepository); @@ -123,7 +123,7 @@ private void createInconsistencies(MutableList loadCommonBlacklist() { try { - return Collections.unmodifiableList(new ObjectMapper().readValue(this.getClass().getResourceAsStream("/unwanted_words_filter_common.json"), + return Collections.unmodifiableList(createObjectMapper().readValue(this.getClass().getResourceAsStream("/unwanted_words_filter_common.json"), new TypeReference>() { })); } catch (IOException e) { diff --git a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistency.java b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistency.java index 0424f7129..09de71458 100644 --- a/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistency.java +++ b/stages/inconsistency-detection/src/main/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistency.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency.types; import java.util.Locale; @@ -9,8 +9,10 @@ import org.eclipse.collections.api.list.MutableList; import edu.kit.kastel.mcse.ardoco.core.api.inconsistency.TextInconsistency; +import edu.kit.kastel.mcse.ardoco.core.inconsistency.MissingElementInconsistencyCandidate; -public record MissingModelInstanceInconsistency(String name, int sentence, double confidence) implements TextInconsistency { +public record MissingModelInstanceInconsistency(String name, int sentence, double confidence, MissingElementInconsistencyCandidate origin) implements + TextInconsistency { private static final String INCONSISTENCY_TYPE_NAME = "MissingModelInstance"; diff --git a/stages/inconsistency-detection/src/test/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistencyTest.java b/stages/inconsistency-detection/src/test/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistencyTest.java index 48087d485..12117cd90 100644 --- a/stages/inconsistency-detection/src/test/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistencyTest.java +++ b/stages/inconsistency-detection/src/test/java/edu/kit/kastel/mcse/ardoco/core/inconsistency/types/MissingModelInstanceInconsistencyTest.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.inconsistency.types; import org.junit.jupiter.api.BeforeEach; @@ -8,7 +8,6 @@ /** * This class tests the record MissingModelInconsistency. - * */ public class MissingModelInstanceInconsistencyTest extends AbstractInconsistencyTypeTest implements Claimant { @@ -16,7 +15,7 @@ public class MissingModelInstanceInconsistencyTest extends AbstractInconsistency @BeforeEach void beforeEach() { - missingModelInstanceInconsistency = new MissingModelInstanceInconsistency("inconsistency", 1, 1.0); + missingModelInstanceInconsistency = new MissingModelInstanceInconsistency("inconsistency", 1, 1.0, null); } @Override @@ -37,12 +36,12 @@ protected String getReasonString() { @Override protected Inconsistency getUnequalInconsistency() { - return new MissingModelInstanceInconsistency("otherInconsistency", 1, 1.0); + return new MissingModelInstanceInconsistency("otherInconsistency", 1, 1.0, null); } @Override protected Inconsistency getEqualInconsistency() { - return new MissingModelInstanceInconsistency("inconsistency", 1, 1.0); + return new MissingModelInstanceInconsistency("inconsistency", 1, 1.0, null); } @Override diff --git a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/ArCoTLModelProviderAgent.java b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/agents/ArCoTLModelProviderAgent.java similarity index 97% rename from stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/ArCoTLModelProviderAgent.java rename to stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/agents/ArCoTLModelProviderAgent.java index c57f0d7a7..0a5acaae7 100644 --- a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/ArCoTLModelProviderAgent.java +++ b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/agents/ArCoTLModelProviderAgent.java @@ -1,5 +1,5 @@ -/* Licensed under MIT 2023. */ -package edu.kit.kastel.mcse.ardoco.core.models; +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.models.agents; import java.io.File; import java.util.ArrayList; diff --git a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/Extractor.java b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/Extractor.java index c12bdb24c..a6c28cfb1 100644 --- a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/Extractor.java +++ b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/Extractor.java @@ -1,10 +1,12 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.models.connectors.generators; +import java.io.Serializable; + import edu.kit.kastel.mcse.ardoco.core.api.models.ModelType; import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.Model; -public abstract class Extractor { +public abstract class Extractor implements Serializable { protected String path; protected Extractor(String path) { diff --git a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/code/CodeExtractor.java b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/code/CodeExtractor.java index 650adb737..279ff8175 100644 --- a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/code/CodeExtractor.java +++ b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/connectors/generators/code/CodeExtractor.java @@ -1,16 +1,15 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.models.connectors.generators.code; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + import java.io.File; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import edu.kit.kastel.mcse.ardoco.core.api.models.CodeModelType; @@ -81,17 +80,4 @@ private String getCodeModelFileString() { return path + File.separator + CODE_MODEL_FILE_NAME; } - private static ObjectMapper createObjectMapper() { - ObjectMapper oom = new ObjectMapper(); - oom.setVisibility(oom.getSerializationConfig() - .getDefaultVisibilityChecker() // - .withFieldVisibility(JsonAutoDetect.Visibility.ANY)// - .withGetterVisibility(JsonAutoDetect.Visibility.NONE)// - .withSetterVisibility(JsonAutoDetect.Visibility.NONE)// - .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)); - oom.enable(SerializationFeature.INDENT_OUTPUT); - oom.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - return oom; - } - } diff --git a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ArCoTLModelProviderInformant.java b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ArCoTLModelProviderInformant.java index 40d5041ea..d29da552c 100644 --- a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ArCoTLModelProviderInformant.java +++ b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ArCoTLModelProviderInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.models.informants; import java.util.Optional; @@ -39,7 +39,7 @@ public ArCoTLModelProviderInformant(DataRepository dataRepository, Extractor ext } @Override - public void run() { + public void process() { if (extractor == null) { return; } diff --git a/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ModelDisambiguationInformant.java b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ModelDisambiguationInformant.java new file mode 100644 index 000000000..c379359c9 --- /dev/null +++ b/stages/model-provider/src/main/java/edu/kit/kastel/mcse/ardoco/core/models/informants/ModelDisambiguationInformant.java @@ -0,0 +1,33 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.models.informants; + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; +import edu.kit.kastel.mcse.ardoco.core.common.util.AbbreviationDisambiguationHelper; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; + +public class ModelDisambiguationInformant extends Informant { + public ModelDisambiguationInformant(DataRepository dataRepository) { + super(ModelDisambiguationInformant.class.getSimpleName(), dataRepository); + } + + @Override + public void process() { + var modelStates = DataRepositoryHelper.getModelStatesData(dataRepository); + for (var modelId : modelStates.modelIds()) { + var modelExtractionState = modelStates.getModelExtractionState(modelId); + var instances = modelExtractionState.getInstances(); + for (var instance : instances) { + var names = instance.getNameParts(); + for (var name : names) { + var abbreviations = AbbreviationDisambiguationHelper.getAbbreviationCandidates(name); + for (var abbreviation : abbreviations) { + var disambiguation = AbbreviationDisambiguationHelper.disambiguate(abbreviation); + AbbreviationDisambiguationHelper.addTransient(new Disambiguation(abbreviation, disambiguation.toArray(new String[0]))); + } + } + } + } + } +} diff --git a/stages/pom.xml b/stages/pom.xml index 6354e2333..7adec3151 100644 --- a/stages/pom.xml +++ b/stages/pom.xml @@ -18,7 +18,9 @@ code-traceability connection-generator + diagram-connection-generator diagram-consistency + diagram-inconsistency-checker diagram-recognition inconsistency-detection model-provider @@ -35,7 +37,9 @@ code-traceability connection-generator + diagram-connection-generator diagram-consistency + diagram-inconsistency-checker diagram-recognition inconsistency-detection model-provider @@ -81,7 +85,9 @@ false + diagram-connection-generator diagram-consistency + diagram-inconsistency-checker diagram-recognition diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/DefaultRecommendationStateStrategy.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/DefaultRecommendationStateStrategy.java new file mode 100644 index 000000000..2e00c4a50 --- /dev/null +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/DefaultRecommendationStateStrategy.java @@ -0,0 +1,23 @@ +/* Licensed under MIT 2024. */ +package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator; + +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationStateStrategy; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +public class DefaultRecommendationStateStrategy implements RecommendationStateStrategy { + private final GlobalConfiguration globalConfiguration; + + public DefaultRecommendationStateStrategy(GlobalConfiguration globalConfiguration) { + this.globalConfiguration = globalConfiguration; + } + + @Override + public boolean areRITypesSimilar(String typeA, String typeB) { + return globalConfiguration.getSimilarityUtils().areWordsSimilar(typeA, typeB); + } + + @Override + public boolean areRINamesSimilar(String nameA, String nameB) { + return globalConfiguration.getSimilarityUtils().areWordsSimilar(nameA, nameB); + } +} diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationGenerator.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationGenerator.java index 09781f493..a7ed96ddf 100644 --- a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationGenerator.java +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationGenerator.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator; import java.util.SortedMap; @@ -42,7 +42,7 @@ public static RecommendationGenerator get(SortedMap additionalCo @Override protected void initializeState() { - var recommendationStates = RecommendationStatesImpl.build(); + var recommendationStates = RecommendationStatesImpl.build(dataRepository.getGlobalConfiguration()); getDataRepository().addData(RecommendationStates.ID, recommendationStates); } } diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStateImpl.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStateImpl.java index f1c4e8ee3..3c322c10a 100644 --- a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStateImpl.java +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStateImpl.java @@ -1,31 +1,34 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator; import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.SortedSets; import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.set.sorted.MutableSortedSet; import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationState; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationStateStrategy; import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.data.AbstractState; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; /** - * The recommendation state encapsulates all recommended instances and relations. These recommendations should be - * contained by the model by their probability. - * + * The recommendation state encapsulates all recommended instances and relations. These recommendations should be contained by the model by their probability. */ public class RecommendationStateImpl extends AbstractState implements RecommendationState { - private transient MutableList recommendedInstances; + private final RecommendationStateStrategy recommendationStateStrategy; + private MutableSortedSet recommendedInstances; /** * Creates a new recommendation state. */ - public RecommendationStateImpl() { - recommendedInstances = Lists.mutable.empty(); + public RecommendationStateImpl(RecommendationStateStrategy recommendationStateStrategy) { + super(); + this.recommendationStateStrategy = recommendationStateStrategy; + recommendedInstances = SortedSets.mutable.empty(); } /** @@ -70,16 +73,15 @@ public RecommendedInstance addRecommendedInstance(String name, String type, Clai } /** - * Adds a recommended instance to the state. If the in the stored instance an instance with the same name and type - * is contained it is extended. If an recommendedInstance with the same name can be found it is extended. Elsewhere - * a new recommended instance is created. + * Adds a recommended instance to the state. If the in the stored instance an instance with the same name and type is contained it is extended. If an + * recommendedInstance with the same name can be found it is extended. Elsewhere a new recommended instance is created. */ private void addRecommendedInstance(RecommendedInstance ri) { if (recommendedInstances.contains(ri)) { return; } - var risWithExactName = recommendedInstances.select(r -> r.getName().equalsIgnoreCase(ri.getName())).toImmutable(); + var risWithExactName = recommendedInstances.select(r -> r.getName().equalsIgnoreCase(ri.getName())).toImmutable().toImmutableList(); var risWithExactNameAndType = risWithExactName.select(r -> r.getType().equalsIgnoreCase(ri.getType())); if (risWithExactNameAndType.isEmpty()) { @@ -96,7 +98,7 @@ private void processRecommendedInstancesWithNoExactNameAndType(RecommendedInstan var added = false; for (RecommendedInstance riWithExactName : risWithExactName) { - var areWordsSimilar = SimilarityUtils.areWordsSimilar(riWithExactName.getType(), ri.getType()); + var areWordsSimilar = recommendationStateStrategy.areRITypesSimilar(riWithExactName.getType(), ri.getType()); if (areWordsSimilar || recommendedInstancesHasEmptyType(ri, riWithExactName)) { riWithExactName.addMappings(ri.getNameMappings(), ri.getTypeMappings()); added = true; @@ -122,7 +124,7 @@ private static boolean recommendedInstancesHasEmptyType(RecommendedInstance ri, */ @Override public ImmutableList getRecommendedInstancesByTypeMapping(NounMapping mapping) { - return recommendedInstances.select(sinstance -> sinstance.getTypeMappings().contains(mapping)).toImmutable(); + return recommendedInstances.select(sinstance -> sinstance.getTypeMappings().contains(mapping)).toImmutableList(); } /** @@ -135,7 +137,7 @@ public ImmutableList getRecommendedInstancesByTypeMapping(N public ImmutableList getAnyRecommendedInstancesByMapping(NounMapping mapping) { return recommendedInstances // .select(sinstance -> sinstance.getTypeMappings().contains(mapping) || sinstance.getNameMappings().contains(mapping)) - .toImmutable(); + .toImmutableList(); } /** @@ -146,7 +148,7 @@ public ImmutableList getAnyRecommendedInstancesByMapping(No */ @Override public ImmutableList getRecommendedInstancesByName(String name) { - return recommendedInstances.select(ri -> ri.getName().toLowerCase().contentEquals(name.toLowerCase())).toImmutable(); + return recommendedInstances.select(ri -> ri.getName().toLowerCase().contentEquals(name.toLowerCase())).toImmutableList(); } /** @@ -159,7 +161,7 @@ public ImmutableList getRecommendedInstancesByName(String n public ImmutableList getRecommendedInstancesBySimilarName(String name) { MutableList ris = Lists.mutable.empty(); for (RecommendedInstance ri : recommendedInstances) { - if (SimilarityUtils.areWordsSimilar(ri.getName(), name)) { + if (recommendationStateStrategy.areRINamesSimilar(ri.getName(), name)) { ris.add(ri); } } @@ -175,7 +177,7 @@ public ImmutableList getRecommendedInstancesBySimilarName(S */ @Override public ImmutableList getRecommendedInstancesByType(String type) { - return recommendedInstances.select(ri -> ri.getType().toLowerCase().contentEquals(type.toLowerCase())).toImmutable(); + return recommendedInstances.select(ri -> ri.getType().toLowerCase().contentEquals(type.toLowerCase())).toImmutableList(); } /** @@ -186,6 +188,6 @@ public ImmutableList getRecommendedInstancesByType(String t */ @Override public ImmutableList getRecommendedInstancesBySimilarType(String type) { - return recommendedInstances.select(ri -> SimilarityUtils.areWordsSimilar(ri.getType(), type)).toImmutable(); + return recommendedInstances.select(ri -> recommendationStateStrategy.areRITypesSimilar(ri.getType(), type)).toImmutableList(); } } diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStatesImpl.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStatesImpl.java index 0b820b308..a1ebaabae 100644 --- a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStatesImpl.java +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendationStatesImpl.java @@ -1,10 +1,12 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator; import java.util.EnumMap; import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationStateStrategy; import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendationStates; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; public class RecommendationStatesImpl implements RecommendationStates { private final EnumMap recommendationStates; @@ -13,10 +15,11 @@ private RecommendationStatesImpl() { recommendationStates = new EnumMap<>(Metamodel.class); } - public static RecommendationStates build() { + public static RecommendationStates build(GlobalConfiguration globalConfiguration) { var recStates = new RecommendationStatesImpl(); for (Metamodel mm : Metamodel.values()) { - recStates.recommendationStates.put(mm, new RecommendationStateImpl()); + RecommendationStateStrategy rss = new DefaultRecommendationStateStrategy(globalConfiguration); + recStates.recommendationStates.put(mm, new RecommendationStateImpl(rss)); } return recStates; } diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendedInstanceImpl.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendedInstanceImpl.java index 719c32b74..3a9daa240 100644 --- a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendedInstanceImpl.java +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/RecommendedInstanceImpl.java @@ -1,7 +1,8 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -12,6 +13,7 @@ import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; import org.eclipse.collections.api.set.sorted.MutableSortedSet; +import edu.kit.kastel.mcse.ardoco.core.api.models.ModelElement; import edu.kit.kastel.mcse.ardoco.core.api.recommendationgenerator.RecommendedInstance; import edu.kit.kastel.mcse.ardoco.core.api.text.Word; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; @@ -23,18 +25,15 @@ import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; /** - * This class represents recommended instances. These instances should be contained by the model. The likelihood is - * measured by the probability. Every recommended instance has a unique name. + * This class represents recommended instances. These instances should be contained by the model. The likelihood is measured by the probability. Every + * recommended instance has a unique name. */ public final class RecommendedInstanceImpl extends RecommendedInstance implements Claimant, NounMappingChangeListener { private static final AggregationFunctions GLOBAL_AGGREGATOR = AggregationFunctions.AVERAGE; /** - * Meaning (Weights):
    - * 0,-1,1 => Balanced
    - * 2 => InternalConfidence: 2 / mappingConfidence: 1
    - * -2 => InternalConfidence: 1 / mappingConfidence: 2
    - * ... + * Meaning (Weights):
    0,-1,1 => Balanced
    2 => InternalConfidence: 2 / mappingConfidence: 1
    -2 => InternalConfidence: 1 / mappingConfidence: + * 2
    ... */ private int weightInternalConfidence = 0; @@ -257,11 +256,13 @@ public String toString() { ", mappings:]= " + separator + String.join(separator, nameNodeVals) + separator + String.join(separator, typeNodeVals) + "\n"; } + //FIXME Uses mutable properties @Override public int hashCode() { return Objects.hash(name, type); } + //FIXME Uses mutable properties @Override public boolean equals(Object obj) { if (this == obj) { @@ -273,6 +274,16 @@ public boolean equals(Object obj) { return Objects.equals(name, other.name) && Objects.equals(type, other.type); } + @Override + public int compareTo(ModelElement o) { + if (this == o) + return 0; + if (o instanceof RecommendedInstance ri) { + return Comparator.comparing(RecommendedInstance::getName).thenComparing(RecommendedInstance::getType).compare(this, ri); + } + return super.compareTo(o); + } + @Override public ImmutableList getClaimants() { return Lists.immutable.withAll(this.internalConfidence.getClaimants()); diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/CompoundRecommendationInformant.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/CompoundRecommendationInformant.java index f5f611e85..6df24604b 100644 --- a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/CompoundRecommendationInformant.java +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/CompoundRecommendationInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.informants; import java.util.SortedMap; @@ -17,7 +17,6 @@ import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -32,7 +31,7 @@ public CompoundRecommendationInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var modelStatesData = DataRepositoryHelper.getModelStatesData(dataRepository); var textState = DataRepositoryHelper.getTextState(dataRepository); @@ -49,8 +48,7 @@ public void run() { } /** - * Look at NounMappings and add RecommendedInstances, if a NounMapping was created because of a compound (in - * text-extraction) + * Look at NounMappings and add RecommendedInstances, if a NounMapping was created because of a compound (in text-extraction) */ private void createRecommendationInstancesFromCompoundNounMappings(TextState textState, RecommendationState recommendationState, LegacyModelExtractionState modelState) { @@ -63,8 +61,8 @@ private void createRecommendationInstancesFromCompoundNounMappings(TextState tex } /** - * Find additional compounds and create RecommendedInstances for them. Additional compounds are when a word in a - * NounMapping has another word in front or afterwards and that compounds is a TypeMapping + * Find additional compounds and create RecommendedInstances for them. Additional compounds are when a word in a NounMapping has another word in front or + * afterwards and that compounds is a TypeMapping */ private void findMoreCompoundsForRecommendationInstances(TextState textState, RecommendationState recommendationState, LegacyModelExtractionState modelState) { @@ -116,11 +114,11 @@ private ImmutableList getSimilarModelTypes(ImmutableList ty var typeIdentifiers = CommonUtilities.getTypeIdentifiers(modelState); for (var typeMapping : typeMappings) { var currSimilarTypes = Lists.immutable.fromStream(typeIdentifiers.stream() - .filter(typeId -> SimilarityUtils.areWordsSimilar(typeId, typeMapping.getReference()))); + .filter(typeId -> getMetaData().getSimilarityUtils().areWordsSimilar(typeId, typeMapping.getReference()))); similarModelTypes.addAll(currSimilarTypes.toList()); for (var word : typeMapping.getWords()) { currSimilarTypes = Lists.immutable.fromStream(typeIdentifiers.stream() - .filter(typeId -> SimilarityUtils.areWordsSimilar(typeId, word.getLemma()))); + .filter(typeId -> getMetaData().getSimilarityUtils().areWordsSimilar(typeId, word.getLemma()))); similarModelTypes.addAll(currSimilarTypes.toList()); } } diff --git a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/NameTypeInformant.java b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/NameTypeInformant.java index 84d487d9f..11405b05b 100644 --- a/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/NameTypeInformant.java +++ b/stages/recommendation-generator/src/main/java/edu/kit/kastel/mcse/ardoco/core/recommendationgenerator/informants/NameTypeInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.informants; import java.util.SortedMap; @@ -34,7 +34,7 @@ public NameTypeInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { DataRepository dataRepository = getDataRepository(); var text = DataRepositoryHelper.getAnnotatedText(dataRepository); var textState = DataRepositoryHelper.getTextState(dataRepository); @@ -71,7 +71,7 @@ private void addRecommendedInstanceIfNameBeforeType(TextState textExtractionStat return; } - var similarTypes = CommonUtilities.getSimilarTypes(word, modelState); + var similarTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!similarTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -96,7 +96,7 @@ private void addRecommendedInstanceIfNameAfterType(TextState textExtractionState return; } - var sameLemmaTypes = CommonUtilities.getSimilarTypes(word, modelState); + var sameLemmaTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!sameLemmaTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -120,7 +120,7 @@ private void addRecommendedInstanceIfNameOrTypeBeforeType(TextState textExtracti return; } - var sameLemmaTypes = CommonUtilities.getSimilarTypes(word, modelState); + var sameLemmaTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!sameLemmaTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); @@ -145,7 +145,7 @@ private void addRecommendedInstanceIfNameOrTypeAfterType(TextState textExtractio return; } - var sameLemmaTypes = CommonUtilities.getSimilarTypes(word, modelState); + var sameLemmaTypes = CommonUtilities.getSimilarTypes(getMetaData().getSimilarityUtils(), word, modelState); if (!sameLemmaTypes.isEmpty()) { textExtractionState.addNounMapping(word, MappingKind.TYPE, this, probability); diff --git a/stages/text-extraction/pom.xml b/stages/text-extraction/pom.xml index 04a061c54..72ba06192 100644 --- a/stages/text-extraction/pom.xml +++ b/stages/text-extraction/pom.xml @@ -29,6 +29,18 @@ common ${revision}
    + + io.github.ardoco.core + tests-base + ${revision} + test + + + io.github.ardoco.core + tests-resources + ${revision} + test + io.github.ardoco.core text-preprocessing diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/ContextPhrase.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/ContextPhrase.java new file mode 100644 index 000000000..70e7f08c9 --- /dev/null +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/ContextPhrase.java @@ -0,0 +1,114 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.textextraction; + +import java.util.Comparator; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.collections.api.factory.SortedMaps; +import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; +import org.eclipse.collections.api.map.sorted.MutableSortedMap; +import org.eclipse.collections.impl.factory.Lists; + +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.PhraseType; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; + +/** + * Phrase implementation for a phrase that is derived from context rather than text processing. + */ +public class ContextPhrase implements Phrase { + private final MutableList words; + private final Sentence sentence; + + /** + * Creates a new phrase consisting of the provided words in the provided sentence. + * + * @param words the words + * @param sentence the sentence + */ + public ContextPhrase(ImmutableList words, Sentence sentence) { + this.words = Lists.mutable.ofAll(words); + this.sentence = sentence; + } + + @Override + public int getSentenceNo() { + return sentence.getSentenceNumber(); + } + + @Override + public String getText() { + return words.stream().map(Word::getText).collect(Collectors.joining(" ")); + } + + @Override + public PhraseType getPhraseType() { + return PhraseType.NP; + } + + @Override + public ImmutableList getContainedWords() { + return words.toImmutableList(); + } + + @Override + public ImmutableList getSubPhrases() { + return Lists.immutable.empty(); + } + + @Override + public boolean isSuperPhraseOf(Phrase other) { + var currText = getText(); + var otherText = other.getText(); + return currText.contains(otherText) && currText.length() != otherText.length(); + } + + @Override + public boolean isSubPhraseOf(Phrase other) { + var currText = getText(); + var otherText = other.getText(); + return otherText.contains(currText) && currText.length() != otherText.length(); + } + + @Override + public ImmutableSortedMap getPhraseVector() { + MutableSortedMap phraseVector = SortedMaps.mutable.empty(); + + getContainedWords().groupBy(Word::getText).forEachKeyImmutableList((text, listOfWords) -> phraseVector.put(listOfWords.getAny(), listOfWords.size())); + + return phraseVector.toImmutable(); + } + + @Override + public int hashCode() { + return Objects.hash(this.getSentenceNo(), this.getText(), this.getPhraseType(), this.getContainedWords().get(0).getPosition()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof Phrase other)) + return false; + return this.getSentenceNo() == other.getSentenceNo() && Objects.equals(this.getText(), other.getText()) && Objects.equals(this.getPhraseType(), other + .getPhraseType()) && this.getContainedWords().get(0).getPosition() == other.getContainedWords().get(0).getPosition(); + } + + @Override + public String toString() { + return "Phrase{" + "text='" + getText() + '\'' + '}'; + } + + @Override + public int compareTo(Phrase o) { + return Comparator.comparing(Phrase::getSentenceNo) + .thenComparing(Phrase::getText) + .thenComparing(Phrase::getPhraseType) + .thenComparingInt(p -> p.getContainedWords().get(0).getPosition()) + .compare(this, o); + } +} diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/DefaultTextStateStrategy.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/DefaultTextStateStrategy.java index 123eb27b8..ce71bbd5a 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/DefaultTextStateStrategy.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/DefaultTextStateStrategy.java @@ -1,25 +1,105 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; +import java.util.LinkedHashSet; +import java.util.List; + +import org.eclipse.collections.api.factory.SortedMaps; import org.eclipse.collections.api.factory.SortedSets; +import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; +import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; +import org.eclipse.collections.api.map.sorted.MutableSortedMap; +import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.PhraseAbbreviation; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextStateStrategy; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.WordAbbreviation; +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; import edu.kit.kastel.mcse.ardoco.core.data.Confidence; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; +@Deterministic public abstract class DefaultTextStateStrategy implements TextStateStrategy { + protected final GlobalConfiguration globalConfiguration; + protected TextStateImpl textState; - private TextStateImpl textState; + protected DefaultTextStateStrategy(GlobalConfiguration globalConfiguration) { + this.globalConfiguration = globalConfiguration; + } - public void setTextState(TextStateImpl textExtractionState) { - textState = textExtractionState; + @Override + public void setState(TextState textState) { + if (this.textState != null) { + throw new IllegalStateException("The text state is already set"); + } else if (textState instanceof TextStateImpl) { + this.textState = (TextStateImpl) textState; + } else { + throw new IllegalArgumentException("The text state must be an instance of TextStateImpl"); + } } public TextStateImpl getTextState() { return textState; } + /** + * Creates a new noun mapping using the parameters without adding it to the state. + * + * @param words the words + * @param distribution the distribution of the mappings kinds + * @param referenceWords the reference words + * @param surfaceForms the surface forms + * @param reference the joined reference, nullable + * @return the created noun mapping + */ + public NounMapping createNounMappingStateless(ImmutableSortedSet words, ImmutableSortedMap distribution, + ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { + if (reference == null) { + reference = calculateNounMappingReference(referenceWords); + } + + return new NounMappingImpl(words, distribution.toImmutable(), referenceWords, surfaceForms, reference); + } + + @Override + public ImmutableList getNounMappingsWithSimilarReference(String reference) { + return this.textState.getNounMappings() + .select(nm -> globalConfiguration.getSimilarityUtils().areWordsSimilar(reference, nm.getReference())) + .toImmutable(); + } + + @Override + public NounMapping addNounMapping(ImmutableSortedSet words, ImmutableSortedMap distribution, + ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { + //Do not add noun mappings to the state, which do not have any claimants + if (distribution.valuesView().noneSatisfy(d -> !d.getClaimants().isEmpty())) { + throw new IllegalArgumentException("Atleast 1 claimant is required"); + } + + NounMapping nounMapping = createNounMappingStateless(words, distribution, referenceWords, surfaceForms, reference); + getTextState().addNounMappingAddPhraseMapping(nounMapping); + return nounMapping; + } + + @Override + public NounMapping addNounMapping(ImmutableSortedSet words, MappingKind kind, Claimant claimant, double probability, + ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { + MutableSortedMap distribution = SortedMaps.mutable.empty(); + distribution.put(MappingKind.NAME, new Confidence(DEFAULT_AGGREGATOR)); + distribution.put(MappingKind.TYPE, new Confidence(DEFAULT_AGGREGATOR)); + var nounMapping = createNounMappingStateless(words, distribution.toImmutable(), referenceWords, surfaceForms, reference); + nounMapping.addKindWithProbability(kind, claimant, probability); + getTextState().addNounMappingAddPhraseMapping(nounMapping); + return nounMapping; + } + public NounMapping mergeNounMappings(NounMapping nounMapping, MutableList nounMappingsToMerge, Claimant claimant) { for (NounMapping nounMappingToMerge : nounMappingsToMerge) { @@ -62,4 +142,39 @@ protected final Confidence putAllConfidencesTogether(Confidence confidence, Conf result.addAllConfidences(confidence1); return result; } + + @Override + public WordAbbreviation addOrExtendWordAbbreviation(String abbreviation, Word word) { + var wordAbbreviation = getTextState().getWordAbbreviations(word).stream().filter(e -> e.getAbbreviation().equals(abbreviation)).findFirst(); + if (wordAbbreviation.isPresent()) { + return extendWordAbbreviation(wordAbbreviation.orElseThrow(), word); + } else { + var newWordAbbreviation = new WordAbbreviation(abbreviation, new LinkedHashSet<>(List.of(word))); + getTextState().addWordAbbreviation(newWordAbbreviation); + return newWordAbbreviation; + } + } + + protected WordAbbreviation extendWordAbbreviation(WordAbbreviation wordAbbreviation, Word word) { + wordAbbreviation.addWord(word); + return wordAbbreviation; + } + + @Override + public PhraseAbbreviation addOrExtendPhraseAbbreviation(String abbreviation, Phrase phrase) { + var phraseAbbreviation = getTextState().getPhraseAbbreviations(phrase).stream().filter(e -> e.getAbbreviation().equals(abbreviation)).findFirst(); + if (phraseAbbreviation.isPresent()) { + return extendPhraseAbbreviation(phraseAbbreviation.orElseThrow(), phrase); + } else { + var newPhraseAbbreviation = new PhraseAbbreviation(abbreviation, new LinkedHashSet<>(List.of(phrase))); + getTextState().addPhraseAbbreviation(newPhraseAbbreviation); + return newPhraseAbbreviation; + } + } + + protected PhraseAbbreviation extendPhraseAbbreviation(PhraseAbbreviation phraseAbbreviation, Phrase phrase) { + phraseAbbreviation.addPhrase(phrase); + return phraseAbbreviation; + } + } diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/NounMappingImpl.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/NounMappingImpl.java index 80ad420c2..51a3e408f 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/NounMappingImpl.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/NounMappingImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; import static edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions.AVERAGE; @@ -12,11 +12,13 @@ import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.factory.SortedMaps; +import org.eclipse.collections.api.factory.SortedSets; import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; import org.eclipse.collections.api.map.sorted.MutableSortedMap; import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; +import org.eclipse.collections.api.set.sorted.MutableSortedSet; import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; import edu.kit.kastel.mcse.ardoco.core.api.text.Word; @@ -34,15 +36,16 @@ */ @Deterministic @NoHashCodeEquals -public final class NounMappingImpl implements NounMapping { +public class NounMappingImpl implements NounMapping { - private static final AtomicLong CREATION_TIME_COUNTER = new AtomicLong(0); + protected static final AtomicLong CREATION_TIME_COUNTER = new AtomicLong(0); private static final AggregationFunctions DEFAULT_AGGREGATOR = AVERAGE; private final Long earliestCreationTime; - private final ImmutableSortedSet words; + private final MutableSortedSet words; + private MutableSortedSet phrases; private final MutableSortedMap distribution; - private final ImmutableList referenceWords; - private final ImmutableList surfaceForms; + private final MutableList referenceWords; + private final MutableList surfaceForms; private final String reference; private boolean isDefinedAsCompound; private final Set changeListeners; @@ -91,10 +94,10 @@ public NounMappingImpl(ImmutableSortedSet words, ImmutableSortedMap words, ImmutableSortedMap distribution, ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { this.earliestCreationTime = earliestCreationTime; - this.words = words; + this.words = words.toSortedSet(); this.distribution = distribution.toSortedMap(); - this.referenceWords = referenceWords; - this.surfaceForms = surfaceForms; + this.referenceWords = referenceWords.toList(); + this.surfaceForms = surfaceForms.toList(); this.reference = reference; this.isDefinedAsCompound = false; this.changeListeners = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -134,7 +137,7 @@ public void onDelete(NounMapping replacement) { @Override public final ImmutableSortedSet getWords() { - return words; + return words.toImmutable(); } @Override @@ -148,7 +151,7 @@ private static String calculateReference(ImmutableList words) { @Override public final ImmutableList getReferenceWords() { - return referenceWords; + return referenceWords.toImmutable(); } @Override @@ -161,14 +164,16 @@ public final ImmutableList getMappingSentenceNo() { } @Override - public ImmutableList getPhrases() { - MutableList phrases = Lists.mutable.empty(); - for (Word word : this.words) { - if (phrases.contains(word.getPhrase())) - continue; - phrases.add(word.getPhrase()); + public ImmutableSortedSet getPhrases() { + if (phrases == null) { + this.phrases = SortedSets.mutable.empty(); + for (Word word : words) { + if (phrases.contains(word.getPhrase())) + continue; + phrases.add(word.getPhrase()); + } } - return phrases.toImmutable(); + return this.phrases.toImmutable(); } @Override @@ -219,7 +224,7 @@ public double getProbabilityForKind(MappingKind mappingKind) { @Override public ImmutableList getSurfaceForms() { - return this.surfaceForms; + return this.surfaceForms.toImmutable(); } @Override @@ -244,7 +249,7 @@ public Long earliestCreationTime() { } public ImmutableSortedSet words() { - return words; + return words.toImmutable(); } public MutableSortedMap distribution() { @@ -252,11 +257,11 @@ public MutableSortedMap distribution() { } public ImmutableList referenceWords() { - return referenceWords; + return referenceWords.toImmutable(); } public ImmutableList surfaceForms() { - return surfaceForms; + return surfaceForms.toImmutable(); } public String reference() { diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/OriginalTextStateStrategy.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/OriginalTextStateStrategy.java index aa1474cc9..dcb703395 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/OriginalTextStateStrategy.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/OriginalTextStateStrategy.java @@ -1,8 +1,8 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; +import java.io.Serializable; import java.util.Arrays; -import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.collections.api.factory.Lists; @@ -16,14 +16,14 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Word; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.data.Confidence; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; -public class OriginalTextStateStrategy extends DefaultTextStateStrategy { +public class OriginalTextStateStrategy extends DefaultTextStateStrategy implements Serializable { - OriginalTextStateStrategy(TextStateImpl textState) { - super.setTextState(textState); + OriginalTextStateStrategy(GlobalConfiguration globalConfiguration) { + super(globalConfiguration); } @Override @@ -33,20 +33,20 @@ public NounMapping addOrExtendNounMapping(Word word, MappingKind kind, Claimant surfaceForms); for (var existingNounMapping : super.getTextState().getNounMappings()) { - if (SimilarityUtils.areNounMappingsSimilar(disposableNounMapping, existingNounMapping)) { + if (globalConfiguration.getSimilarityUtils().areNounMappingsSimilar(disposableNounMapping, existingNounMapping)) { return mergeNounMappings(existingNounMapping, disposableNounMapping, disposableNounMapping.getReferenceWords(), disposableNounMapping .getReference(), disposableNounMapping.getKind(), claimant, disposableNounMapping.getProbability()); } } - super.getTextState().addNounMappingAddPhraseMapping(disposableNounMapping); + getTextState().addNounMappingAddPhraseMapping(disposableNounMapping); return disposableNounMapping; } @Override - public NounMapping mergeNounMappings(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, - MappingKind mappingKind, Claimant claimant, double probability) { + public NounMappingImpl mergeNounMappingsStateless(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, + String reference, MappingKind mappingKind, Claimant claimant, double probability) { MutableSortedSet mergedWords = firstNounMapping.getWords().toSortedSet(); mergedWords.add(secondNounMapping.getReferenceWords().get(0)); @@ -72,19 +72,19 @@ public NounMapping mergeNounMappings(NounMapping firstNounMapping, NounMapping s String mergedReference = mergedReferenceWords.collect(Word::getText).makeString(" "); - NounMapping mergedNounMapping = new NounMappingImpl(NounMappingImpl.earliestCreationTime(firstNounMapping, secondNounMapping), mergedWords.toSortedSet() - .toImmutable(), mergedDistribution.toImmutable(), mergedReferenceWords.toImmutable(), mergedSurfaceForms.toImmutable(), mergedReference); + return new NounMappingImpl(NounMappingImpl.earliestCreationTime(firstNounMapping, secondNounMapping), mergedWords.toSortedSet().toImmutable(), + mergedDistribution.toImmutable(), mergedReferenceWords.toImmutable(), mergedSurfaceForms.toImmutable(), mergedReference); + } + + @Override + public NounMappingImpl mergeNounMappings(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, + MappingKind mappingKind, Claimant claimant, double probability) { + var mergedNounMapping = mergeNounMappingsStateless(firstNounMapping, secondNounMapping, referenceWords, reference, mappingKind, claimant, probability); this.getTextState().removeNounMappingFromState(firstNounMapping, mergedNounMapping); this.getTextState().removeNounMappingFromState(secondNounMapping, mergedNounMapping); - this.getTextState().addNounMappingAddPhraseMapping(mergedNounMapping); return mergedNounMapping; } - - @Override - public Function creator() { - return OriginalTextStateStrategy::new; - } } diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseConcerningTextStateStrategy.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseConcerningTextStateStrategy.java index 58072d6cf..eee3bbe13 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseConcerningTextStateStrategy.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseConcerningTextStateStrategy.java @@ -1,8 +1,7 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; import java.util.Arrays; -import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.collections.api.factory.Lists; @@ -18,12 +17,13 @@ import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; import edu.kit.kastel.mcse.ardoco.core.data.Confidence; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; public class PhraseConcerningTextStateStrategy extends DefaultTextStateStrategy { - public PhraseConcerningTextStateStrategy(TextStateImpl textState) { - super.setTextState(textState); + public PhraseConcerningTextStateStrategy(GlobalConfiguration globalConfiguration) { + super(globalConfiguration); } @Override @@ -49,8 +49,8 @@ public NounMapping addOrExtendNounMapping(Word word, MappingKind kind, Claimant } @Override - public NounMapping mergeNounMappings(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, - MappingKind mappingKind, Claimant claimant, double probability) { + public NounMappingImpl mergeNounMappingsStateless(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, + String reference, MappingKind mappingKind, Claimant claimant, double probability) { MutableSortedSet mergedWords = firstNounMapping.getWords().toSortedSet(); mergedWords.addAllIterable(secondNounMapping.getWords()); @@ -87,25 +87,27 @@ public NounMapping mergeNounMappings(NounMapping firstNounMapping, NounMapping s if (firstNounMapping.getReference().equalsIgnoreCase(secondNounMapping.getReference())) { mergedReference = firstNounMapping.getReference(); } else { - mergedReference = this.getTextState().calculateNounMappingReference(mergedReferenceWords); + mergedReference = calculateNounMappingReference(mergedReferenceWords); } } - NounMapping mergedNounMapping = new NounMappingImpl(NounMappingImpl.earliestCreationTime(firstNounMapping, secondNounMapping), mergedWords + NounMappingImpl mergedNounMapping = new NounMappingImpl(NounMappingImpl.earliestCreationTime(firstNounMapping, secondNounMapping), mergedWords .toImmutableSortedSet(), mergedDistribution.toImmutable(), mergedReferenceWords.toImmutable(), mergedSurfaceForms.toImmutable(), mergedReference); mergedNounMapping.addKindWithProbability(mappingKind, claimant, probability); + return mergedNounMapping; + } + + @Override + public NounMappingImpl mergeNounMappings(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, + MappingKind mappingKind, Claimant claimant, double probability) { + var mergedNounMapping = mergeNounMappingsStateless(firstNounMapping, secondNounMapping, referenceWords, reference, mappingKind, claimant, probability); + this.getTextState().removeNounMappingFromState(firstNounMapping, mergedNounMapping); this.getTextState().removeNounMappingFromState(secondNounMapping, mergedNounMapping); - this.getTextState().addNounMappingAddPhraseMapping(mergedNounMapping); return mergedNounMapping; } - - @Override - public Function creator() { - return PhraseConcerningTextStateStrategy::new; - } } diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseMappingImpl.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseMappingImpl.java index b4d087c54..0e91744ba 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseMappingImpl.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/PhraseMappingImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; import java.util.LinkedHashSet; @@ -11,6 +11,8 @@ import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; import org.eclipse.collections.api.map.sorted.MutableSortedMap; +import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; +import org.eclipse.collections.api.set.sorted.MutableSortedSet; import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; import edu.kit.kastel.mcse.ardoco.core.api.text.PhraseType; @@ -28,12 +30,12 @@ public final class PhraseMappingImpl implements PhraseMapping { /** * Phrases encapsulated in the mapping. */ - private final MutableList phrases; + private final MutableSortedSet phrases; private final Set changeListeners = new LinkedHashSet<>(); - public PhraseMappingImpl(ImmutableList phrases) { - this.phrases = Lists.mutable.withAll(phrases); + public PhraseMappingImpl(ImmutableSortedSet phrases) { + this.phrases = SortedSets.mutable.withAll(phrases); } @Override @@ -43,8 +45,8 @@ public ImmutableList getNounMappings(TextState textState) { } @Override - public ImmutableList getPhrases() { - return Lists.immutable.withAll(phrases); + public ImmutableSortedSet getPhrases() { + return phrases.toImmutable(); } @Override diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtraction.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtraction.java index 5dc1f552c..9adc36489 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtraction.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtraction.java @@ -1,12 +1,14 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; import java.util.List; import java.util.SortedMap; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextStateStrategy; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractExecutionStage; +import edu.kit.kastel.mcse.ardoco.core.textextraction.agents.AbbreviationAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.agents.InitialTextAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.agents.PhraseAgent; @@ -19,8 +21,10 @@ public class TextExtraction extends AbstractExecutionStage { * Instantiates a new text extractor. */ public TextExtraction(DataRepository dataRepository) { - super(List.of(new InitialTextAgent(dataRepository), new PhraseAgent(dataRepository)), TextExtraction.class.getSimpleName(), dataRepository); - + super(List.of(// + new InitialTextAgent(dataRepository),// + new PhraseAgent(dataRepository),// + new AbbreviationAgent(dataRepository)), "TextExtraction", dataRepository); } /** @@ -41,7 +45,8 @@ protected void initializeState() { var dataRepository = getDataRepository(); var optionalTextState = dataRepository.getData(TextState.ID, TextStateImpl.class); if (optionalTextState.isEmpty()) { - var textState = new TextStateImpl(); + TextStateStrategy tts = new OriginalTextStateStrategy(dataRepository.getGlobalConfiguration()); + var textState = new TextStateImpl(tts); dataRepository.addData(TextState.ID, textState); } } diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateImpl.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateImpl.java index 4a98c3aca..f8a70a865 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateImpl.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateImpl.java @@ -1,20 +1,15 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction; -import static edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions.AVERAGE; - import java.util.Comparator; import java.util.SortedMap; -import java.util.function.Function; import org.eclipse.collections.api.block.predicate.Predicate; import org.eclipse.collections.api.factory.Lists; -import org.eclipse.collections.api.factory.SortedMaps; import org.eclipse.collections.api.factory.SortedSets; import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; -import org.eclipse.collections.api.map.sorted.ImmutableSortedMap; -import org.eclipse.collections.api.map.sorted.MutableSortedMap; +import org.eclipse.collections.api.ordered.SortedIterable; import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; import org.eclipse.collections.api.set.sorted.MutableSortedSet; @@ -22,14 +17,14 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Word; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.PhraseAbbreviation; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.PhraseMapping; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; -import edu.kit.kastel.mcse.ardoco.core.common.AggregationFunctions; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextStateStrategy; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.WordAbbreviation; import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; import edu.kit.kastel.mcse.ardoco.core.common.util.Comparators; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.data.AbstractState; -import edu.kit.kastel.mcse.ardoco.core.data.Confidence; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; /** @@ -37,8 +32,6 @@ */ public class TextStateImpl extends AbstractState implements TextState { - private static final AggregationFunctions DEFAULT_AGGREGATOR = AVERAGE; - private static final Comparator ORDER_NOUNMAPPING = (n1, n2) -> { if (n1.equals(n2)) return 0; @@ -58,60 +51,34 @@ public class TextStateImpl extends AbstractState implements TextState { private static final double MAPPING_KIND_MAX_DIFF = 0.1; private MutableList nounMappings; private MutableList phraseMappings; - private final transient TextStateStrategy strategy; + private MutableSortedSet wordAbbreviations; + private MutableSortedSet phraseAbbreviations; + private final TextStateStrategy strategy; - /** - * Creates a new name type relation state - */ - public TextStateImpl() { - this(OriginalTextStateStrategy::new); + // Configuration Test + private TextStateImpl() { + super(); + this.strategy = null; } - public TextStateImpl(Function constructor) { + public TextStateImpl(TextStateStrategy strategy) { + super(); + this.strategy = strategy; nounMappings = Lists.mutable.empty(); phraseMappings = Lists.mutable.empty(); - strategy = constructor.apply(this); - } - - @Override - public NounMapping addNounMapping(Word word, MappingKind kind, Claimant claimant, double probability) { - return strategy.addOrExtendNounMapping(word, kind, claimant, probability, Lists.immutable.with(word.getText())); - } - - @Override - public NounMapping addNounMapping(Word word, MappingKind kind, Claimant claimant, double probability, ImmutableList surfaceForms) { - return strategy.addOrExtendNounMapping(word, kind, claimant, probability, surfaceForms); + wordAbbreviations = SortedSets.mutable.empty(); + phraseAbbreviations = SortedSets.mutable.empty(); + this.strategy.setState(this); } @Override - public NounMapping addNounMapping(ImmutableSortedSet words, MappingKind kind, Claimant claimant, double probability, - ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { - MutableSortedMap distribution = SortedMaps.mutable.empty(); - distribution.put(MappingKind.NAME, new Confidence(DEFAULT_AGGREGATOR)); - distribution.put(MappingKind.TYPE, new Confidence(DEFAULT_AGGREGATOR)); - NounMapping nounMapping = new NounMappingImpl(words.toSortedSet().toImmutable(), distribution.toImmutable(), referenceWords, surfaceForms, reference); - nounMapping.addKindWithProbability(kind, claimant, probability); - addNounMappingAddPhraseMapping(nounMapping); - return nounMapping; - } - - @Override - public NounMapping addNounMapping(ImmutableSortedSet words, ImmutableSortedMap distribution, - ImmutableList referenceWords, ImmutableList surfaceForms, String reference) { - - if (reference == null) { - reference = calculateNounMappingReference(referenceWords); - } - - NounMapping nounMapping = new NounMappingImpl(words.toSortedSet().toImmutable(), distribution, referenceWords, surfaceForms, reference); - addNounMappingAddPhraseMapping(nounMapping); - return nounMapping; + public TextStateStrategy getTextStateStrategy() { + return this.strategy; } @Override public ImmutableList getNounMappings() { return this.nounMappings.toImmutableList(); - } @Override @@ -135,12 +102,12 @@ public PhraseMapping getPhraseMappingByNounMapping(NounMapping nounMapping) { ImmutableList phraseMappingsByNounMapping = getPhraseMappingsByNounMapping(nounMapping); assert (!phraseMappingsByNounMapping.isEmpty()) : "Every noun mapping should be connected to a phrase mapping"; return phraseMappingsByNounMapping.get(0); - } @Override public ImmutableList getNounMappingsByPhraseMapping(PhraseMapping phraseMapping) { - return getNounMappings().select(nm -> Comparators.collectionsEqualsAnyOrder(phraseMapping.getPhrases().castToList(), nm.getPhrases().castToList())); + return getNounMappings().select(nm -> Comparators.collectionsEqualsAnyOrder(phraseMapping.getPhrases().castToCollection(), nm.getPhrases() + .castToCollection())); } /** @@ -225,7 +192,25 @@ public boolean isWordContainedByMappingKind(Word word, MappingKind kind) { @Override public ImmutableList getNounMappingsWithSimilarReference(String reference) { - return getNounMappings().select(nm -> SimilarityUtils.areWordsSimilar(reference, nm.getReference())).toImmutable(); + return strategy.getNounMappingsWithSimilarReference(reference); + } + + @Override + public ImmutableSortedSet getWordAbbreviations() { + return wordAbbreviations.toImmutable(); + } + + @Override + public ImmutableSortedSet getPhraseAbbreviations() { + return phraseAbbreviations.toImmutable(); + } + + protected boolean addWordAbbreviation(WordAbbreviation wordAbbreviation) { + return this.wordAbbreviations.add(wordAbbreviation); + } + + protected boolean addPhraseAbbreviation(PhraseAbbreviation phraseAbbreviation) { + return this.phraseAbbreviations.add(phraseAbbreviation); } @Override @@ -247,7 +232,7 @@ public void mergePhraseMappingsAndNounMappings(PhraseMapping phraseMapping, Phra @Override public PhraseMapping mergePhraseMappings(PhraseMapping phraseMapping, PhraseMapping similarPhraseMapping) { - MutableList mergedPhrases = phraseMapping.getPhrases().toList(); + MutableSortedSet mergedPhrases = phraseMapping.getPhrases().toSortedSet(); mergedPhrases.addAll(similarPhraseMapping.getPhrases().toList()); PhraseMapping mergedPhraseMapping = new PhraseMappingImpl(mergedPhrases.toImmutable()); @@ -291,9 +276,13 @@ private Predicate nounMappingIsOfKind(MappingKind mappingKi void addNounMappingAddPhraseMapping(NounMapping nounMapping) { addNounMappingToState(nounMapping); - if (phraseMappings.anySatisfy(it -> Comparators.collectionsIdentityAnyOrder(it.getPhrases(), nounMapping.getPhrases()))) + if (phraseMappings.anySatisfy(it -> { + var sortedIt = (SortedIterable) it.getPhrases(); + return Comparators.collectionsIdentityAnyOrder(sortedIt, nounMapping.getPhrases()); + })) return; - phraseMappings.add(new PhraseMappingImpl(nounMapping.getPhrases())); + PhraseMapping phraseMappingImpl = new PhraseMappingImpl(nounMapping.getPhrases()); + phraseMappings.add(phraseMappingImpl); } @Override @@ -308,18 +297,6 @@ public void removeNounMapping(NounMapping nounMapping, NounMapping replacement) removeNounMappingFromState(nounMapping, replacement); } - String calculateNounMappingReference(ImmutableList referenceWords) { - StringBuilder refBuilder = new StringBuilder(); - referenceWords.toSortedListBy(Word::getPosition); - referenceWords.toSortedListBy(Word::getSentenceNo); - - for (int i = 0; i < referenceWords.size() - 1; i++) { - refBuilder.append(referenceWords.get(i).getText()).append(" "); - } - refBuilder.append(referenceWords.get(referenceWords.size() - 1).getText()); - return refBuilder.toString(); - } - private void addNounMappingToState(NounMapping nounMapping) { if (this.nounMappings.contains(nounMapping)) { throw new IllegalArgumentException("Nounmapping was already in state"); @@ -328,14 +305,30 @@ private void addNounMappingToState(NounMapping nounMapping) { this.nounMappings.sortThis(ORDER_NOUNMAPPING); } - void removePhraseMappingFromState(PhraseMapping phraseMapping, PhraseMapping replacement) { - this.phraseMappings.remove(phraseMapping); + /** + * Removes the specified phrase mapping from the state and replaces it with an (optional) replacement + * + * @param phraseMapping the mapping + * @param replacement the replacement + * @return true if removed, false otherwise + */ + boolean removePhraseMappingFromState(PhraseMapping phraseMapping, PhraseMapping replacement) { + var success = this.phraseMappings.remove(phraseMapping); phraseMapping.onDelete(replacement); + return success; } - void removeNounMappingFromState(NounMapping nounMapping, NounMapping replacement) { - this.nounMappings.remove(nounMapping); + /** + * Removes the specified noun mapping from the state and replaces it with an (optional) replacement + * + * @param nounMapping the mapping + * @param replacement the replacement + * @return true if removed, false otherwise + */ + boolean removeNounMappingFromState(NounMapping nounMapping, NounMapping replacement) { + var success = this.nounMappings.remove(nounMapping); nounMapping.onDelete(replacement); + return success; } @Override @@ -347,5 +340,4 @@ public String toString() { protected void delegateApplyConfigurationToInternalObjects(SortedMap additionalConfiguration) { // handle additional configuration } - } diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateStrategy.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateStrategy.java deleted file mode 100644 index e9f3d8d6e..000000000 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextStateStrategy.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Licensed under MIT 2022-2023. */ -package edu.kit.kastel.mcse.ardoco.core.textextraction; - -import java.util.function.Function; - -import org.eclipse.collections.api.list.ImmutableList; - -import edu.kit.kastel.mcse.ardoco.core.api.text.Word; -import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; -import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; -import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; - -/** - * The Interface for strategies for the text state. - */ -public interface TextStateStrategy { - - NounMapping addOrExtendNounMapping(Word word, MappingKind kind, Claimant claimant, double probability, ImmutableList surfaceForms); - - NounMapping mergeNounMappings(NounMapping firstNounMapping, NounMapping secondNounMapping, ImmutableList referenceWords, String reference, - MappingKind mappingKind, Claimant claimant, double probability); - - Function creator(); -} diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/AbbreviationAgent.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/AbbreviationAgent.java new file mode 100644 index 000000000..2e4f835a6 --- /dev/null +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/AbbreviationAgent.java @@ -0,0 +1,14 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.textextraction.agents; + +import java.util.List; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; +import edu.kit.kastel.mcse.ardoco.core.textextraction.informants.AbbreviationInformant; + +public class AbbreviationAgent extends PipelineAgent { + public AbbreviationAgent(DataRepository dataRepository) { + super(List.of(new AbbreviationInformant(dataRepository)), AbbreviationAgent.class.getSimpleName(), dataRepository); + } +} diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/AbbreviationInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/AbbreviationInformant.java new file mode 100644 index 000000000..0102958cb --- /dev/null +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/AbbreviationInformant.java @@ -0,0 +1,344 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.list.ImmutableList; + +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; +import edu.kit.kastel.mcse.ardoco.core.common.tuple.Triple; +import edu.kit.kastel.mcse.ardoco.core.common.util.AbbreviationDisambiguationHelper; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.core.textextraction.ContextPhrase; + +public class AbbreviationInformant extends Informant { + @Configurable + private int adjacencyLimit = 4; + @Configurable + private int characterLimit = 10; + @Configurable + private double multiplicativeOrderThreshold = 0.8; + @Configurable + private double upperCaseThreshold = 0.6; + @Configurable + private double unrepresentedFraction = 0.2; + @Configurable + private boolean allowCrossSentenceSearch = false; + @Configurable + private boolean requireSpecialCharacter = true; + @Configurable + private double rewardInitialMatch = 0.5; + @Configurable + private double rewardCaseMatch = 0.5; + @Configurable + private double rewardAnyMatch = 0.5; + private List brackets = List.of('(', ')'); + + public AbbreviationInformant(DataRepository dataRepository) { + super(AbbreviationInformant.class.getSimpleName(), dataRepository); + } + + @Override + public void process() { + ImmutableList words = DataRepositoryHelper.getAnnotatedText(getDataRepository()).words(); + ImmutableList phrases = DataRepositoryHelper.getAnnotatedText(dataRepository).phrases(); + var textState = DataRepositoryHelper.getTextState(getDataRepository()); + for (var word : words) { + if (AbbreviationDisambiguationHelper.couldBeAbbreviation(word.getText(), upperCaseThreshold)) { + findMeaning(textState, phrases, word); + } + findBrackets(textState, phrases, word); + } + } + + void findMeaning(TextState textState, ImmutableList phrases, Word word) { + findBefore(textState, phrases, word); + findAfter(textState, phrases, word); + } + + void findBefore(TextState textState, ImmutableList phrases, Word word) { + var initial = word.getText().toLowerCase(Locale.US).substring(0, 1); + var optBracket = findClosest(Word::getPreWord, word, brackets, adjacencyLimit, List.of()); + if (optBracket.isEmpty()) + return; + var bracket = optBracket.orElseThrow(); + var optPrev = findFurthestWithSharedInitial(Word::getPreWord, bracket, initial, adjacencyLimit - (word.getPosition() - bracket.getPosition()) + word + .getText() + .length(), brackets); + if (optPrev.isPresent()) { + var meaningList = between(optPrev.orElseThrow(), word); + if (!requireSpecialCharacter || meaningList.stream().anyMatch(w -> !Character.isLetter(w.getText().toCharArray()[0]))) { + extractShortestMeaning(textState, phrases, word, meaningList); + } + } + } + + void findAfter(TextState textState, ImmutableList phrases, Word word) { + var initial = word.getText().toLowerCase(Locale.US).substring(0, 1); + var optBracket = findClosest(Word::getNextWord, word, brackets, adjacencyLimit, List.of()); + if (optBracket.isEmpty()) + return; + var bracket = optBracket.orElseThrow(); + var optNext = findClosestWithSharedInitial(Word::getNextWord, bracket, initial, adjacencyLimit - (bracket.getPosition() - word.getPosition()), + brackets); + if (optNext.isPresent()) { + var expectedMaxLength = Math.round(word.getText().length() / (1 - unrepresentedFraction)); + var meaningList = next(optNext.orElseThrow(), expectedMaxLength, true, brackets); + var between = between(word, optNext.orElseThrow()); + if (!requireSpecialCharacter || between.stream().anyMatch(w -> !Character.isLetter(w.getText().toCharArray()[0]))) { + extractShortestMeaning(textState, phrases, word, meaningList); + } + } + } + + /** + * {@return a list of words between a and b, the first is inclusive, the last is exclusive} + * + * @param a a word + * @param b a word + */ + List between(Word a, Word b) { + if (a == b) + return List.of(); + var first = a.getPosition() < b.getPosition() ? a : b; + var second = a.getPosition() < b.getPosition() ? b : a; + + var list = new ArrayList(); + var next = first; + while (next != second) { + list.add(next); + next = next.getNextWord(); + } + return list; + } + + /** + * {@return a list containing the next words of maximum length amount} + * + * @param a starting word (inclusive) + * @param amount the amount + * @param skipNonLetter skip all words beginning with a non-letter character + * @param stopAt character to stop at + */ + List next(Word a, long amount, boolean skipNonLetter, List stopAt) { + var list = new ArrayList(); + var n = a; + char c; + for (int i = 0; i < amount; i++) { + list.add(n); + do { + n = n.getNextWord(); + if (n == null) + return list; + c = n.getText().toCharArray()[0]; + if (stopAt.contains(c)) { + return list; + } + } while (skipNonLetter && !Character.isLetter(c)); + } + return list; + } + + Optional findFurthestWithSharedInitial(UnaryOperator iterate, Word word, String match, int tries, List stopAt) { + if (tries <= 0) + return Optional.empty(); + var optNext = Optional.ofNullable(iterate.apply(word)); + if (optNext.isEmpty()) { + return Optional.empty(); + } + var next = optNext.orElseThrow(); + + if (!allowCrossSentenceSearch && word.getSentenceNo() != next.getSentenceNo()) + return Optional.empty(); + + var c = next.getText().toCharArray()[0]; + if (!Character.isLetter(c)) { + if (stopAt.contains(c)) + return Optional.empty(); + else + return findFurthestWithSharedInitial(iterate, next, match, tries, stopAt); + } + + if (AbbreviationDisambiguationHelper.shareInitial(next.getText().toLowerCase(Locale.US), match.toLowerCase(Locale.US))) + return Optional.of(findFurthestWithSharedInitial(iterate, next, match, tries - 1, stopAt).orElse(next)); + + return findFurthestWithSharedInitial(iterate, next, match, tries - 1, stopAt); + } + + Optional findClosestWithSharedInitial(UnaryOperator iterate, Word word, String match, int tries, List stopAt) { + if (tries <= 0) + return Optional.empty(); + var optNext = Optional.ofNullable(iterate.apply(word)); + if (optNext.isEmpty()) { + return Optional.empty(); + } + var next = optNext.orElseThrow(); + + if (!allowCrossSentenceSearch && word.getSentenceNo() != next.getSentenceNo()) + return Optional.empty(); + + var c = next.getText().toCharArray()[0]; + if (!Character.isLetter(c)) { + if (stopAt.contains(c)) + return Optional.empty(); + else + return findClosestWithSharedInitial(iterate, next, match, tries, stopAt); + } + + if (AbbreviationDisambiguationHelper.shareInitial(next.getText().toLowerCase(Locale.ENGLISH), match.toLowerCase(Locale.ENGLISH))) + return optNext; + + return findClosestWithSharedInitial(iterate, next, match, tries - 1, stopAt); + } + + /** + * Tries to find the closest word that begins with any character from a list of provided characters and stops if no tries are left or if a forbidden + * character is reached. + * + * @param iterate a function which returns the "next" word relative to the origin + * @param word the word that is the point of origin (This word is not evaluated!) + * @param search the list of initial characters + * @param tries tries left + * @param stopAt list of forbidden characters, e.g. {"(", ")"} if we are not allowed to enter brackets + * @return an optional closest word + */ + Optional findClosest(UnaryOperator iterate, Word word, List search, int tries, List stopAt) { + if (tries <= 0) + return Optional.empty(); + var optNext = Optional.ofNullable(iterate.apply(word)); + if (optNext.isEmpty()) { + return Optional.empty(); + } + var next = optNext.orElseThrow(); + + if (!allowCrossSentenceSearch && word.getSentenceNo() != next.getSentenceNo()) + return Optional.empty(); + + var c = next.getText().toCharArray()[0]; + if (search.contains(c)) + return optNext; + if (stopAt.contains(c)) + return Optional.empty(); + return findClosest(iterate, next, search, tries - 1, stopAt); + } + + void findBrackets(TextState textState, ImmutableList phrases, Word word) { + var text = word.getText(); + if (text.equals("(")) { + var optWord = findSingularWordInBracket(word); + if (optWord.isEmpty()) + return; + var candidate = optWord.orElseThrow(); + if (candidate.getText().length() > characterLimit) + return; + var optPrev = findFurthestWithSharedInitial(Word::getPreWord, word, candidate.getText(), adjacencyLimit, brackets); + if (optPrev.isEmpty()) + return; + var preceding = between(optPrev.orElseThrow(), word); + extractShortestMeaning(textState, phrases, candidate, preceding); + } + } + + /** + * Calculates a score for each potential meaning to an abbreviation. Each sublist of the word list is treated as a potential meaning. + * E.g. the word list {"Eurovision", "Song", "Contest"} is treated as "Eurovision", "Eurovision Song" and "Eurovision Song Contest". + * A higher score is considered better. + * + * @param abbreviationCandidate the abbreviation candidate, e.g. "ESC" + * @param wordList a list of words, e.g. {"Eurovision", "Song", "Contest"} + * @param caseSensitive whether the calculation should be case-sensitive + * @return a list containing a triple with the score, list of words and the potential meaning + */ + List, String>> getScores(Word abbreviationCandidate, List wordList, boolean caseSensitive) { + ArrayList, String>> meaningScores = new ArrayList<>(); + for (int i = 1; i <= wordList.size(); i++) { + var subList = new ArrayList<>(new ArrayList<>(wordList).subList(0, i)); + var lMeaning = subList.stream().map(Word::getText).collect(Collectors.joining(" ")); + var lAbbreviation = abbreviationCandidate.getText(); + if (!caseSensitive) { + lMeaning = lMeaning.toLowerCase(Locale.US); + lAbbreviation = lAbbreviation.toLowerCase(Locale.US); + } + var shareInitial = AbbreviationDisambiguationHelper.shareInitial(lMeaning, lAbbreviation); + + if (shareInitial) { + processCandidate(lMeaning, lAbbreviation, caseSensitive, meaningScores, subList); + } + } + return meaningScores; + } + + void processCandidate(String lMeaning, String lAbbreviation, boolean caseSensitive, ArrayList, String>> meaningScores, + ArrayList subList) { + var inOrder = AbbreviationDisambiguationHelper.containsInOrder(lMeaning, lAbbreviation); + var dynamicThreshold = Math.max(1, Math.ceil(lAbbreviation.length() * multiplicativeOrderThreshold)); + + if (inOrder >= dynamicThreshold) { + if (!caseSensitive) { + var asInitial = AbbreviationDisambiguationHelper.maximumAbbreviationScore(lMeaning, lAbbreviation, 1, 0, 0, 0); + if (asInitial < dynamicThreshold) + return; + } + + var newScore = AbbreviationDisambiguationHelper.maximumAbbreviationScore(lMeaning, lAbbreviation, rewardInitialMatch, rewardAnyMatch, + rewardCaseMatch, 0); + if (meaningScores.stream().noneMatch(t -> t.first() >= newScore)) { + meaningScores.add(new Triple<>(newScore, subList, lMeaning)); + } + } + } + + void extractShortestMeaning(TextState textState, ImmutableList phrases, Word abbreviationCandidate, List meaningList) { + var meaningScores = getScores(abbreviationCandidate, meaningList, true); + if (meaningScores.isEmpty()) + meaningScores = getScores(abbreviationCandidate, meaningList, false); + + var optTriple = meaningScores.stream().max(Comparator.comparingDouble(Triple::first)); + if (optTriple.isPresent()) { + var triple = optTriple.orElseThrow(); + var subList = triple.second(); + + if (subList.size() == 1) { + var meaning = subList.get(0); + //Only add meanings which expand the abbreviation + if (meaning.getText().length() > abbreviationCandidate.getText().length()) + textState.addWordAbbreviation(abbreviationCandidate.getText(), meaning); + } + + Phrase shortestPhrase; + var optShortestPhrase = phrases.stream() + .filter(p -> p.getText().equalsIgnoreCase(triple.third())) + .min(Comparator.comparingInt(a -> a.getText().length())); + if (optShortestPhrase.isEmpty()) { + var sentence = subList.get(0).getSentence(); + shortestPhrase = new ContextPhrase(Lists.immutable.ofAll(subList), sentence); + sentence.addPhrase(shortestPhrase); + logger.debug("Added phrase {}", shortestPhrase); + } else { + shortestPhrase = optShortestPhrase.orElseThrow(); + } + //Only add meanings which expand the abbreviation + if (shortestPhrase.getText().length() > abbreviationCandidate.getText().length()) + textState.addPhraseAbbreviation(abbreviationCandidate.getText(), shortestPhrase); + } + } + + Optional findSingularWordInBracket(Word leadingOpeningBracket) { + Word content = leadingOpeningBracket.getNextWord(); + if (content != null && content.getNextWord() != null && content.getNextWord().getText().equals(")")) + return Optional.of(content); + return Optional.empty(); + } +} diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/CompoundAgentInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/CompoundAgentInformant.java index bdf16cebd..da096408d 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/CompoundAgentInformant.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/CompoundAgentInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; import java.util.SortedMap; @@ -30,7 +30,7 @@ public CompoundAgentInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { var text = DataRepositoryHelper.getAnnotatedText(getDataRepository()); var textState = getDataRepository().getData(TextState.ID, TextStateImpl.class).orElseThrow(); for (var word : text.words()) { diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/InDepArcsInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/InDepArcsInformant.java index a2d078edc..1d2545ef9 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/InDepArcsInformant.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/InDepArcsInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; import java.util.SortedMap; @@ -34,7 +34,7 @@ public InDepArcsInformant(DataRepository data) { } @Override - public void run() { + public void process() { var textState = DataRepositoryHelper.getTextState(getDataRepository()); for (var word : DataRepositoryHelper.getAnnotatedText(getDataRepository()).words()) { exec(textState, word); diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/MappingCombinerInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/MappingCombinerInformant.java index b3f39fc74..afbd41da2 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/MappingCombinerInformant.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/MappingCombinerInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; import java.util.SortedMap; @@ -15,7 +15,6 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.Comparators; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; import edu.kit.kastel.mcse.ardoco.core.common.util.PhraseMappingAggregatorStrategy; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.configuration.Configurable; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; @@ -30,7 +29,7 @@ public MappingCombinerInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { TextState textState = DataRepositoryHelper.getTextState(getDataRepository()); combineSimilarPhraseMappings(textState); } @@ -40,8 +39,8 @@ private void combineSimilarPhraseMappings(TextState textState) { ImmutableList phraseMappings = textState.getPhraseMappings(); for (PhraseMapping phraseMapping : phraseMappings) { - ImmutableList similarPhraseMappings = phraseMappings.select(p -> SimilarityUtils.getPhraseMappingSimilarity(textState, phraseMapping, - p, PhraseMappingAggregatorStrategy.MAX_SIMILARITY) > minCosineSimilarity); + ImmutableList similarPhraseMappings = phraseMappings.select(p -> getMetaData().getSimilarityUtils() + .getPhraseMappingSimilarity(textState, phraseMapping, p, PhraseMappingAggregatorStrategy.MAX_SIMILARITY) > minCosineSimilarity); // Remove the phrase mapping from the list of similar phrase mappings // Comment: This would break the logic but seems to be logical .. @@ -103,7 +102,7 @@ private void processSimilarPhraseMappingWhenEqualPhraseText(TextState textState, .contains(nounMapping)) { continue; } - if (SimilarityUtils.areNounMappingsSimilar(nounMapping, nounMappingOfSimilarPhraseMapping)) { + if (getMetaData().getSimilarityUtils().areNounMappingsSimilar(nounMapping, nounMappingOfSimilarPhraseMapping)) { textState.mergeNounMappings(nounMapping, nounMappingOfSimilarPhraseMapping, this); } } @@ -115,7 +114,7 @@ private NounMapping getMostSimilarNounMappingOverThreshold(NounMapping nounMappi MutableList similarNounMappings = Lists.mutable.empty(); for (NounMapping nounMappingOfSimilarPhraseMapping : nounMappingsOfSimilarPhraseMapping) { - if (SimilarityUtils.areNounMappingsSimilar(nounMapping, nounMappingOfSimilarPhraseMapping)) { + if (getMetaData().getSimilarityUtils().areNounMappingsSimilar(nounMapping, nounMappingOfSimilarPhraseMapping)) { similarNounMappings.add(nounMappingOfSimilarPhraseMapping); } } diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/NounInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/NounInformant.java index 8630bd8ac..3754eb9c7 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/NounInformant.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/NounInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; import java.util.SortedMap; @@ -34,7 +34,7 @@ public NounInformant(DataRepository data) { } @Override - public void run() { + public void process() { ImmutableList words = DataRepositoryHelper.getAnnotatedText(getDataRepository()).words(); var textState = DataRepositoryHelper.getTextState(getDataRepository()); for (var word : words) { diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/OutDepArcsInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/OutDepArcsInformant.java index cf872121d..59fd7a1f1 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/OutDepArcsInformant.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/OutDepArcsInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; import java.util.SortedMap; @@ -35,7 +35,7 @@ public OutDepArcsInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { var textState = DataRepositoryHelper.getTextState(getDataRepository()); for (var word : DataRepositoryHelper.getAnnotatedText(getDataRepository()).words()) { exec(textState, word); diff --git a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/SeparatedNamesInformant.java b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/SeparatedNamesInformant.java index 01e5a5e46..c24e07d2d 100644 --- a/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/SeparatedNamesInformant.java +++ b/stages/text-extraction/src/main/java/edu/kit/kastel/mcse/ardoco/core/textextraction/informants/SeparatedNamesInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.informants; import java.util.SortedMap; @@ -33,7 +33,7 @@ public SeparatedNamesInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { var textState = DataRepositoryHelper.getTextState(getDataRepository()); for (var word : DataRepositoryHelper.getAnnotatedText(getDataRepository()).words()) { exec(textState, word); diff --git a/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtractionTest.java b/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtractionTest.java new file mode 100644 index 000000000..89ec19e69 --- /dev/null +++ b/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/TextExtractionTest.java @@ -0,0 +1,154 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.textextraction; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; + +import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.set.sorted.ImmutableSortedSet; +import org.eclipse.collections.impl.factory.Lists; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import edu.kit.kastel.mcse.ardoco.core.api.Disambiguation; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.PhraseAbbreviation; +import edu.kit.kastel.mcse.ardoco.core.api.textextraction.WordAbbreviation; +import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; +import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; +import edu.kit.kastel.mcse.ardoco.tests.eval.StageTest; + +class TextExtractionTest extends StageTest { + public TextExtractionTest() { + super(new TextExtraction(null), TextProject.values()); + } + + @Override + protected TextExtractionResult runComparable(TextProject project, SortedMap additionalConfigurations, boolean cachePreRun) { + var dataRepository = run(project, additionalConfigurations, cachePreRun); + + var wordAbbreviations = DataRepositoryHelper.getTextState(dataRepository).getWordAbbreviations(); + var phraseAbbreviations = DataRepositoryHelper.getTextState(dataRepository).getPhraseAbbreviations(); + + var result = new TextExtractionResult(wordAbbreviations, phraseAbbreviations); + + return result; + } + + @Override + protected DataRepository runPreTestRunner(TextProject project) { + return new AnonymousRunner(project.getProjectName()) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) throws IOException { + var pipelineSteps = new ArrayList(); + + var text = CommonUtilities.readInputText(project.getTextFile()); + if (text.isBlank()) { + throw new IllegalArgumentException("Cannot deal with empty input text. Maybe there was " + "an error reading the file."); + } + DataRepositoryHelper.putInputText(dataRepository, text); + pipelineSteps.add(TextPreprocessingAgent.get(project.getAdditionalConfigurations(), dataRepository)); + + return pipelineSteps; + } + }.runWithoutSaving(); + } + + @Override + protected DataRepository runTestRunner(TextProject project, SortedMap additionalConfigurations, DataRepository preRunDataRepository) { + return new AnonymousRunner(project.getProjectName(), preRunDataRepository) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) { + var pipelineSteps = new ArrayList(); + pipelineSteps.add(TextExtraction.get(project.getAdditionalConfigurations(), dataRepository)); + return pipelineSteps; + } + }.runWithoutSaving(); + } + + public record TextExtractionResult(ImmutableSortedSet wordAbbreviations, ImmutableSortedSet phraseAbbreviations) { + } + + @DisplayName("Evaluate Text Extraction") + @ParameterizedTest(name = "{0}") + @EnumSource(value = TextProject.class, mode = EnumSource.Mode.MATCH_NONE, names = "^" + ".*HISTORICAL$") + @Order(1) + void evaluateNonHistoricalDiagramRecognition(TextProject project) { + runComparable(project); + } + + @DisplayName("Evaluate Text Extraction (Historical)") + @ParameterizedTest(name = "{0}") + @EnumSource(value = TextProject.class, mode = EnumSource.Mode.MATCH_ALL, names = "^.*HISTORICAL$") + @Order(2) + void evaluateHistoricalDiagramRecognition(TextProject project) { + runComparable(project); + } + + public enum TextProject implements GoldStandardProject { + MEDIASTORE(// + Project.MEDIASTORE, // + List.of() // + ), // + TEASTORE( // + Project.TEASTORE, // + List.of(new Disambiguation("LFU", new String[] { "Least Frequently Used" }), new Disambiguation("CRUD", new String[] { + "Create Read Update Delete" })) // + ), // + TEAMMATES( // + Project.TEAMMATES, // + List.of(new Disambiguation("GAE", new String[] { "Google App Engine" }), new Disambiguation("POJOs", new String[] { "Plain Old Java Objects" }), + new Disambiguation("CRUD", new String[] { "Create, Read, Update, Delete" }), new Disambiguation("L&P", new String[] { + "Load & Performance" })) // + ), // + BIGBLUEBUTTON( // + Project.BIGBLUEBUTTON, // + List.of(new Disambiguation("LMS", new String[] { "learning management system" }), new Disambiguation("fsels", new String[] { + "FreeSWITCH Event Socket Layer" }), new Disambiguation("SVG", new String[] { "scalable vector graphics" })) // + ), // + TEASTORE_HISTORICAL( // + Project.TEASTORE_HISTORICAL, // + List.of(new Disambiguation("REST", new String[] { "representational state transfer" }), new Disambiguation("JSP", new String[] { + "Java Server Page" }), new Disambiguation("JSPs", new String[] { "Java Server Pages" }), new Disambiguation("OPEN.xtrace", + new String[] { "Open Execution Trace " + "Exchange" })) // + ), // + TEAMMATES_HISTORICAL( // + Project.TEAMMATES_HISTORICAL, // + List.of(new Disambiguation("GAE", new String[] { "Google App Engine" }), new Disambiguation("JSP", new String[] { "Java Server Pages" }), + new Disambiguation("POJOs", new String[] { "Plain Old Java Objects" }), new Disambiguation("CRUD", new String[] { + "Create Read Update Delete" })) // + ), // + BIGBLUEBUTTON_HISTORICAL( // + Project.BIGBLUEBUTTON_HISTORICAL, // + List.of(new Disambiguation("LMS", new String[] { "learning management system" })) // + ); + + private final Project project; + private final ImmutableList disambiguations; + + TextProject(Project project, List disambiguations) { + this.project = project; + this.disambiguations = Lists.immutable.ofAll(disambiguations); + } + + @Override + public String getProjectName() { + return project.getProjectName(); + } + + @Override + public SortedSet getResourceNames() { + return project.getResourceNames(); + } + } +} diff --git a/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/MappingCombinerTest.java b/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/MappingCombinerTest.java index e90d821a0..4fb82f0ac 100644 --- a/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/MappingCombinerTest.java +++ b/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/MappingCombinerTest.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.agents; import org.eclipse.collections.api.factory.Lists; @@ -16,9 +16,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.textextraction.MappingKind; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.NounMapping; import edu.kit.kastel.mcse.ardoco.core.api.textextraction.PhraseMapping; -import edu.kit.kastel.mcse.ardoco.core.api.textextraction.TextState; import edu.kit.kastel.mcse.ardoco.core.common.util.PhraseMappingAggregatorStrategy; -import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityUtils; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; import edu.kit.kastel.mcse.ardoco.core.text.providers.informants.corenlp.PhraseImpl; @@ -33,8 +31,8 @@ class MappingCombinerTest implements Claimant { private MappingCombiner agent; - private TextState preTextState; - private TextState textState; + private TextStateImpl preTextState; + private TextStateImpl textState; private Word fox0; private Word dog1; @@ -62,7 +60,8 @@ class MappingCombinerTest implements Claimant { void setup() { this.data = new DataRepository(); this.agent = new MappingCombiner(data); - preTextState = new TextStateImpl(PhraseConcerningTextStateStrategy::new); + PhraseConcerningTextStateStrategy strategy = new PhraseConcerningTextStateStrategy(data.getGlobalConfiguration()); + preTextState = new TextStateImpl(strategy); Word a0 = Mockito.mock(Word.class); Word fast0 = Mockito.mock(Word.class); @@ -168,7 +167,7 @@ void copy() { preTextState.addNounMapping(fox0, MappingKind.NAME, this, 0.5); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertEquals(preTextState.getNounMappings(), textState.getNounMappings()); @@ -193,7 +192,7 @@ void addEqualNounMappingWithEqualPhrase() { Assertions.assertEquals(1, preTextState.getPhraseMappings().size()); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertEquals(preTextState.getNounMappings(), textState.getNounMappings()); @@ -207,13 +206,15 @@ void addTextualEqualNounMappingWithSimilarPhrase() { preTextState.addNounMapping(dog3, MappingKind.NAME, this, 0.5); Assertions.assertTrue(phraseMappingsAreSimilar(preTextState, dog1, dog3)); - Assertions.assertTrue(SimilarityUtils.areNounMappingsSimilar(preTextState.getNounMappingByWord(dog1), preTextState.getNounMappingByWord(dog3))); + Assertions.assertTrue(data.getGlobalConfiguration() + .getSimilarityUtils() + .areNounMappingsSimilar(preTextState.getNounMappingByWord(dog1), preTextState.getNounMappingByWord(dog3))); Assertions.assertEquals(2, preTextState.getNounMappings().size()); Assertions.assertEquals(2, preTextState.getPhraseMappings().size()); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertTrue(nounMappingsWereMerged(preTextState, dog1, dog3, textState)); @@ -232,7 +233,7 @@ void addTextualEqualNounMappingWithDifferentPhrase() { Assertions.assertEquals(2, preTextState.getPhraseMappings().size()); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertEquals(2, textState.getNounMappings().size()); @@ -249,7 +250,7 @@ void addSimilarNounMappingWithEqualPhrase() { preTextState.addNounMapping(alternativeDog, MappingKind.NAME, this, 0.5); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertNotEquals(null, textState.getNounMappingByWord(dog1)); @@ -277,7 +278,7 @@ void addSimilarNounMappingWithSimilarPhraseContainingSimilarNounMapping() { Assertions.assertTrue(phraseMappingsAreSimilar(preTextState, dog1, dog3)); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertTrue(nounMappingsWereMerged(preTextState, dog1, dog3, textState)); @@ -293,7 +294,7 @@ void addSimilarNounMappingWithSimilarPhraseContainingNoSimilarNounMapping() { Assertions.assertTrue(phraseMappingsAreSimilar(preTextState, turtle4, dog6)); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertAll(// @@ -307,10 +308,12 @@ void addSimilarNounMappingWithDifferentPhrase() { preTextState.addNounMapping(dog1, MappingKind.NAME, this, 0.5); preTextState.addNounMapping(doggy5, MappingKind.TYPE, this, 0.5); + var dog = dogPhrase1.compareTo(dogPhrase3); + Assertions.assertFalse(phraseMappingsAreSimilar(preTextState, dog1, doggy5)); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertAll(// @@ -330,7 +333,7 @@ void addDifferentNounMappingWithEqualPhrase() { Assertions.assertTrue(phraseMappingsAreSimilar(preTextState, dog1, hut3)); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); PhraseMapping dog1PhraseMapping = textState.getPhraseMappings().select(pm -> pm.getPhrases().contains(dogPhrase1)).get(0); @@ -364,7 +367,7 @@ void addDifferentNounMappingWithSimilarPhrase() { Assertions.assertTrue(phraseMappingsAreSimilar(preTextState, dog1, hut3)); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertAll(// @@ -381,7 +384,7 @@ void addDifferentNounMappingWithDifferentPhrase() { Assertions.assertFalse(phraseMappingsAreSimilar(preTextState, dog1, turtle4)); textState = createCopy(preTextState); - this.data.addData(TextState.ID, textState); + this.data.addData(TextStateImpl.ID, textState); agent.run(); Assertions.assertAll(// @@ -389,15 +392,19 @@ void addDifferentNounMappingWithDifferentPhrase() { .getPhraseMappings(), textState.getPhraseMappings())); } - private boolean phraseMappingsAreSimilar(TextState textState, Word word1, Word word2) { + private boolean phraseMappingsAreSimilar(TextStateImpl textState, Word word1, Word word2) { var nm0 = textState.getNounMappingByWord(word1); var nm1 = textState.getNounMappingByWord(word2); - return SimilarityUtils.getPhraseMappingSimilarity(textState, textState.getPhraseMappingByNounMapping(nm0), textState.getPhraseMappingByNounMapping(nm1), - PhraseMappingAggregatorStrategy.MAX_SIMILARITY) > MappingCombinerTest.MIN_COSINE_SIMILARITY; + var pm0 = textState.getPhraseMappingByNounMapping(nm0); + var pm1 = textState.getPhraseMappingByNounMapping(nm1); + + return data.getGlobalConfiguration() + .getSimilarityUtils() + .getPhraseMappingSimilarity(textState, pm0, pm1, PhraseMappingAggregatorStrategy.MAX_SIMILARITY) > MappingCombinerTest.MIN_COSINE_SIMILARITY; } - private boolean nounMappingsWereMerged(TextState preTextState, Word word1, Word word2, TextState afterTextState) { + private boolean nounMappingsWereMerged(TextStateImpl preTextState, Word word1, Word word2, TextStateImpl afterTextState) { Assertions.assertNotEquals(preTextState.getNounMappings().size(), afterTextState.getNounMappings().size()); Assertions.assertNotNull(textState.getNounMappingByWord(word1)); @@ -412,7 +419,7 @@ private boolean nounMappingsWereMerged(TextState preTextState, Word word1, Word return true; } - private boolean phraseMappingsWereMerged(TextState preTextState, Word word1, Word word2, TextState afterTextState) { + private boolean phraseMappingsWereMerged(TextStateImpl preTextState, Word word1, Word word2, TextStateImpl afterTextState) { var nounMapping1 = afterTextState.getNounMappingByWord(word1); var nounMapping2 = afterTextState.getNounMappingByWord(word2); @@ -435,6 +442,7 @@ private void mockPhrase(PhraseImpl phrase, String text, int sentenceNumber, Immu Mockito.when(phrase.getContainedWords()).thenReturn(containedWords); Mockito.when(phrase.getPhraseVector()).thenCallRealMethod(); Mockito.when(phrase.toString()).thenCallRealMethod(); + Mockito.when(phrase.compareTo(Mockito.any())).thenCallRealMethod(); } private void mockWord(Word word, String text, String lemma, int sentenceNumber, Phrase phrase, int position) { @@ -446,8 +454,9 @@ private void mockWord(Word word, String text, String lemma, int sentenceNumber, Mockito.when(word.compareTo(Mockito.any())).thenCallRealMethod(); } - private TextState createCopy(TextState textState) { - TextStateImpl newTextState = new TextStateImpl(); + private TextStateImpl createCopy(TextStateImpl textState) { + PhraseConcerningTextStateStrategy strategy = new PhraseConcerningTextStateStrategy(data.getGlobalConfiguration()); + TextStateImpl newTextState = new TextStateImpl(strategy); MutableList nounMappings = getField(textState, "nounMappings"); MutableList phraseMappings = getField(textState, "phraseMappings"); diff --git a/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/PDFExtractorTestCase.java b/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/PDFExtractorTestCase.java index a894c865c..389166092 100644 --- a/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/PDFExtractorTestCase.java +++ b/stages/text-extraction/src/test/java/edu/kit/kastel/mcse/ardoco/core/textextraction/agents/PDFExtractorTestCase.java @@ -1,6 +1,8 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.textextraction.agents; +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + import java.io.File; import java.io.IOException; import java.util.Set; @@ -14,8 +16,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import com.fasterxml.jackson.databind.ObjectMapper; - import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; /** @@ -101,6 +101,6 @@ private void extract(String in, String out, BiConsumer words; + private final MutableList words; private final SentenceImpl parent; @@ -30,7 +31,7 @@ public class PhraseImpl implements Phrase { public PhraseImpl(Tree tree, ImmutableList words, SentenceImpl parent) { this.tree = tree; - this.words = words; + this.words = words.toList(); this.parent = parent; } @@ -55,7 +56,7 @@ public PhraseType getPhraseType() { @Override public ImmutableList getContainedWords() { - return words; + return words.toImmutable(); } @Override @@ -112,7 +113,7 @@ public int hashCode() { public boolean equals(Object obj) { if (this == obj) return true; - if (!(obj instanceof PhraseImpl other)) + if (!(obj instanceof Phrase other)) return false; return this.getSentenceNo() == other.getSentenceNo() && Objects.equals(this.getText(), other.getText()) && Objects.equals(this.getPhraseType(), other .getPhraseType()) && this.getContainedWords().get(0).getPosition() == other.getContainedWords().get(0).getPosition(); @@ -122,4 +123,15 @@ public boolean equals(Object obj) { public String toString() { return "Phrase{" + "text='" + getText() + '\'' + '}'; } + + @Override + public int compareTo(Phrase o) { + if (this == o) + return 0; + return Comparator.comparing(Phrase::getSentenceNo) + .thenComparing(Phrase::getText) + .thenComparing(Phrase::getPhraseType) + .thenComparingInt(p -> p.getContainedWords().get(0).getPosition()) + .compare(this, o); + } } diff --git a/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/SentenceImpl.java b/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/SentenceImpl.java index ed4eeaf59..5158e98a8 100644 --- a/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/SentenceImpl.java +++ b/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/SentenceImpl.java @@ -1,6 +1,10 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.text.providers.informants.corenlp; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; import java.util.List; import java.util.Objects; @@ -21,17 +25,21 @@ class SentenceImpl implements Sentence { private static final Logger logger = LoggerFactory.getLogger(SentenceImpl.class); - private ImmutableList words = Lists.immutable.empty(); - private ImmutableList phrases = Lists.immutable.empty(); + private MutableList words = Lists.mutable.empty(); + private MutableList phrases = Lists.mutable.empty(); - private final TextImpl parent; - private final CoreSentence coreSentence; - private final int sentenceNumber; + private TextImpl parent; + private transient CoreSentence coreSentence; + private SemanticGraph semanticGraph; + private int sentenceNumber; + + private final String text; public SentenceImpl(CoreSentence coreSentence, int sentenceNumber, TextImpl parent) { this.coreSentence = coreSentence; this.sentenceNumber = sentenceNumber; this.parent = parent; + this.text = coreSentence.text(); } @Override @@ -42,14 +50,14 @@ public int getSentenceNumber() { @Override public ImmutableList getWords() { if (words.isEmpty()) { - this.words = parent.words().select(w -> w.getSentenceNo() == sentenceNumber).toImmutable(); + this.words = Lists.mutable.ofAll(parent.words().select(w -> w.getSentenceNo() == sentenceNumber)); } - return words; + return words.toImmutable(); } @Override public String getText() { - return coreSentence.text(); + return text; } @Override @@ -64,10 +72,15 @@ public ImmutableList getPhrases() { newPhrases.add(currPhrase); } } - phrases = newPhrases.toImmutable(); + phrases = newPhrases; } - return phrases; + return phrases.toImmutable(); + } + + @Override + public void addPhrase(Phrase phrase) { + phrases.add(phrase); } protected List getWordsForPhrase(Tree phrase) { @@ -111,6 +124,33 @@ public int hashCode() { } public SemanticGraph dependencyParse() { - return this.coreSentence.dependencyParse(); + if (semanticGraph == null) + semanticGraph = this.coreSentence.dependencyParse(); + return semanticGraph; + } + + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + getPhrases(); //Initialize before write + dependencyParse(); //Initialize before write + out.defaultWriteObject(); + /* It is a lot cheaper to serialize the phrases (up to 70x less storage space and much + faster), if the coreSentence is ever made accessible, this should be uncommented + out.writeObject(coreSentence.coreMap()); + ProtobufAnnotationSerializer serializer = new ProtobufAnnotationSerializer(); + serializer.writeCoreDocument(coreSentence.document(), out); + */ + } + + @Serial + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + /* It is a lot cheaper to serialize the phrases (up to 70x less storage space and much + faster), if the coreSentence is ever made accessible, this should be uncommented + var coreMap = (CoreMap) in.readObject(); + ProtobufAnnotationSerializer serializer = new ProtobufAnnotationSerializer(); + var coreDocument = serializer.readCoreDocument(in).first; + coreSentence = new CoreSentence(coreDocument, coreMap); + */ } } diff --git a/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/TextImpl.java b/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/TextImpl.java index 6528703b3..68fe2705a 100644 --- a/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/TextImpl.java +++ b/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/TextImpl.java @@ -1,6 +1,10 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.text.providers.informants.corenlp; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; import java.util.SortedMap; import java.util.TreeMap; @@ -15,7 +19,7 @@ public class TextImpl implements Text { - final CoreDocument coreDocument; + private transient CoreDocument coreDocument; private ImmutableList sentences = Lists.immutable.empty(); private ImmutableList words = Lists.immutable.empty(); private final SortedMap wordsIndex = new TreeMap<>(); @@ -75,4 +79,25 @@ private void iterateDocumentForWordsAndSentences() { } } + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + words(); //Initialize words + getSentences(); //Initialize sentences + out.defaultWriteObject(); + /* It is a lot cheaper to serialize the phrases (up to 70x less storage space and much + faster), if the coreDocument is ever made accessible, this should be uncommented + ProtobufAnnotationSerializer serializer = new ProtobufAnnotationSerializer(); + serializer.writeCoreDocument(coreDocument, out); + */ + } + + @Serial + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + /* It is a lot cheaper to serialize the phrases (up to 70x less storage space and much + faster), if the coreDocument is ever made accessible, this should be uncommented + ProtobufAnnotationSerializer serializer = new ProtobufAnnotationSerializer(); + coreDocument = serializer.readCoreDocument(in).first; + */ + } } diff --git a/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/WordImpl.java b/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/WordImpl.java index c53b2d6c6..f278bd2bb 100644 --- a/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/WordImpl.java +++ b/stages/text-preprocessing/src/main/java/edu/kit/kastel/mcse/ardoco/core/text/providers/informants/corenlp/WordImpl.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.text.providers.informants.corenlp; import java.util.List; @@ -187,4 +187,9 @@ public int compareTo(Word o) { } return Integer.compare(this.getPosition(), o.getPosition()); } + + @Override + public String toString() { + return token.toString(); + } } diff --git a/stages/text-preprocessing/src/test/java/edu/kit/kastel/mcse/ardoco/core/text/providers/corenlp/CoreNLPProviderTest.java b/stages/text-preprocessing/src/test/java/edu/kit/kastel/mcse/ardoco/core/text/providers/corenlp/CoreNLPProviderTest.java index a2b835972..95d7505b8 100644 --- a/stages/text-preprocessing/src/test/java/edu/kit/kastel/mcse/ardoco/core/text/providers/corenlp/CoreNLPProviderTest.java +++ b/stages/text-preprocessing/src/test/java/edu/kit/kastel/mcse/ardoco/core/text/providers/corenlp/CoreNLPProviderTest.java @@ -1,10 +1,11 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.text.providers.corenlp; import static edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities.readInputText; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -23,8 +24,8 @@ public synchronized static CoreNLPProvider getCoreNLPProvider() { if (coreNLPProvider == null) { try { createCoreNlpProvider(); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); + } catch (IOException e) { + throw new IllegalArgumentException(e); } } return coreNLPProvider; diff --git a/tests/pom.xml b/tests/pom.xml index edbbc68b6..7eececd21 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -81,6 +81,7 @@ tests-base tests-inconsistency tests-misc + tests-resources tests-tlr diff --git a/tests/tests-base/pom.xml b/tests/tests-base/pom.xml index db8667e14..e20870ec0 100644 --- a/tests/tests-base/pom.xml +++ b/tests/tests-base/pom.xml @@ -9,6 +9,7 @@ tests-base + jar Base classes for tests. It also includes the benchmark data from https://github.com/ArDoCo/Benchmark diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/TestUtil.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/TestUtil.java index 9a6691d53..ba2a76ce2 100644 --- a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/TestUtil.java +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/TestUtil.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests; import java.util.Locale; @@ -145,9 +145,13 @@ public static String createResultLogString(String name, EvaluationResults res return String.format(Locale.ENGLISH, "%n%s:%n%s", name, results); } + public static void logExtendedResultsAsRow(Logger logger, String headerVal, String headerKey, EvaluationResults results) { + var txt = String.format("%n%s", results.toRow(headerVal, headerKey)); + logger.info(txt); + } + /** - * Log the provided {@link EvaluationResults} using the provided logger and name. The log contains Precision and Recall printed with explicit numbers for - * the calculation. See the following example output:
    Precision: 4/4 = 1.000
    Recall: 4/6 = 0.667 + * Log the provided {@link EvaluationResults} using the provided logger and name. Additionally, logs TP, FP, TN and FN used to calculate the metrics. * * @param logger Logger to use * @param name Name to show in the output @@ -179,8 +183,10 @@ public static void logResultsWithExpected(Logger logger, String name, Evaluation public static void logExtendedResultsWithExpected(Logger logger, Object testClass, String name, EvaluationResults results, ExpectedResults expectedResults) { - var infoString = String.format(Locale.ENGLISH, "%n%s (%s):%n%s", name, testClass.getClass().getSimpleName(), results - .getExtendedResultStringWithExpected(expectedResults)); + var infoString = String.format(Locale.ENGLISH, """ + + %s (%s): + %s""", name, testClass.getClass().getSimpleName(), results.getExtendedResultStringWithExpected(expectedResults)); logger.info(infoString); } diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProject.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProject.java index c04942c86..31255a65c 100644 --- a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProject.java +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProject.java @@ -1,8 +1,6 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval; -import static edu.kit.kastel.mcse.ardoco.core.tests.eval.ProjectHelper.ANALYZE_CODE_DIRECTLY; - import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -10,6 +8,8 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.function.Predicate; import org.eclipse.collections.api.factory.Lists; @@ -21,7 +21,13 @@ import edu.kit.kastel.mcse.ardoco.core.common.util.TraceLinkUtilities; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; -public enum CodeProject { +/** + * This enum captures the different case studies that are used for evaluation in the integration tests. + */ +public enum CodeProject implements GoldStandardProject { + /** + * @see Project#MEDIASTORE + */ MEDIASTORE(// Project.MEDIASTORE, // "https://github.com/ArDoCo/MediaStore3.git", // @@ -33,6 +39,9 @@ public enum CodeProject { new ExpectedResults(.995, .515, .675, .990, .715, .999) // ), + /** + * @see Project#TEASTORE + */ TEASTORE(Project.TEASTORE, // "https://github.com/ArDoCo/TeaStore.git", // "bdc49020a55cfa97eaabbb25744fefbc2697defa", // @@ -43,6 +52,9 @@ public enum CodeProject { new ExpectedResults(.999, .708, .829, .976, .831, .999) // ), + /** + * @see Project#TEAMMATES + */ TEAMMATES(Project.TEAMMATES, // "https://github.com/ArDoCo/teammates.git",// "b24519a2af9e17b2bc9c025e87e4cf60009c425d",// @@ -53,6 +65,9 @@ public enum CodeProject { new ExpectedResults(.705, .909, .795, .975, .785, .975) // ), + /** + * @see Project#BIGBLUEBUTTON + */ BIGBLUEBUTTON(Project.BIGBLUEBUTTON,// "https://github.com/ArDoCo/bigbluebutton.git",// "8fa2507d6c3865a9850004fd6fefd09738e68406",// @@ -63,6 +78,9 @@ public enum CodeProject { new ExpectedResults(.765, .905, .835, .985, .825, .985) // ), + /** + * @see Project#JABREF + */ JABREF(Project.JABREF, // "https://github.com/ArDoCo/jabref.git",// "6269698cae437610ec79c38e6dd611eef7e88afe",// @@ -83,6 +101,7 @@ public enum CodeProject { private final Project project; private final ExpectedResults expectedResultsForSamCode; private final ExpectedResults expectedResultsForSadSamCode; + private final SortedSet resourceNames; CodeProject(Project project, String codeRepository, String commitHash, String codeModelLocationInResources, String samCodeGoldStandardLocation, String sadCodeGoldStandardLocation, ExpectedResults expectedResultsForSamCode, ExpectedResults expectedResultsForSadSamCode) { @@ -94,24 +113,47 @@ public enum CodeProject { this.sadCodeGoldStandardLocation = sadCodeGoldStandardLocation; this.expectedResultsForSamCode = expectedResultsForSamCode; this.expectedResultsForSadSamCode = expectedResultsForSadSamCode; + SortedSet set = new TreeSet<>(project.getResourceNames()); + set.add(codeModelLocationInResources); + set.add(samCodeGoldStandardLocation); + set.add(sadCodeGoldStandardLocation); + resourceNames = set; + } + + @Override + public String getProjectName() { + return this.name(); } - public Project getProject() { - return project; + @Override + public SortedSet getResourceNames() { + return new TreeSet<>(resourceNames); } + /** + * {@return the link to the code repository of this project} + */ public String getCodeRepository() { return codeRepository; } + /** + * {@return the commit hash the project is based on} + */ public String getCommitHash() { return commitHash; } + /** + * {@return path of the code directory} + */ public String getCodeLocation() { return getTemporaryCodeLocation().getAbsolutePath(); } + /** + * {@return the directory of the code model} + */ public String getCodeModelDirectory() { try { loadCodeModelFromResourcesIfNeeded(); @@ -122,8 +164,13 @@ public String getCodeModelDirectory() { } } + /** + * Loads the code from resources or from the code directoy cache + * + * @throws IOException Can occur during file operations + */ public void loadCodeModelFromResourcesIfNeeded() throws IOException { - if (ANALYZE_CODE_DIRECTLY.get()) + if (ProjectHelper.ANALYZE_CODE_DIRECTLY.get()) return; File temporaryCodeLocation = getTemporaryCodeLocation(); @@ -135,14 +182,25 @@ public void loadCodeModelFromResourcesIfNeeded() throws IOException { } } + /** + * {@return the expected results using the software architecture model code} + */ public ExpectedResults getExpectedResultsForSamCode() { return expectedResultsForSamCode; } + /** + * {@return the expected results using the software architecture model code} + */ public ExpectedResults getExpectedResultsForSadSamCode() { return expectedResultsForSadSamCode; } + /** + * {@return all trace link strings from the gold standard} + * + * @see TraceLinkUtilities#createTraceLinkString(String, String) + */ public ImmutableList getSamCodeGoldStandard() { File samCodeGoldStandardFile = ProjectHelper.loadFileFromResources(samCodeGoldStandardLocation); List lines = getLinesFromGoldStandardFile(samCodeGoldStandardFile); @@ -160,6 +218,9 @@ public ImmutableList getSamCodeGoldStandard() { return goldStandard.toImmutable(); } + /** + * {@return all lines from the gold standard in csv format} + */ public ImmutableList getSadCodeGoldStandard() { File sadCodeGoldStandardFile = ProjectHelper.loadFileFromResources(sadCodeGoldStandardLocation); List lines = getLinesFromGoldStandardFile(sadCodeGoldStandardFile); diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/GoldStandardProject.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/GoldStandardProject.java new file mode 100644 index 000000000..bec95bd22 --- /dev/null +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/GoldStandardProject.java @@ -0,0 +1,245 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.tests.eval; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.Optional; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.prefs.Preferences; + +import org.apache.commons.codec.digest.DigestUtils; +import org.eclipse.collections.api.list.ImmutableList; +import org.eclipse.collections.api.list.MutableList; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; +import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.ArchitectureModel; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; + +/** + * Interface for all Project extensions + */ +public interface GoldStandardProject extends Serializable { + /** + * {@return the project the instance is based on} + */ + String getProjectName(); + + /** + * {@return the name of all resources associated with instances relative to the class} + */ + SortedSet getResourceNames(); + + /** + * {@return the version of the source files of this project} + */ + default long getSourceFilesVersion() { + getResourceNames().forEach(this::validateResourceChecksum); + return Preferences.userNodeForPackage(getClass()).getLong("version", -1L); + } + + /** + * Calculates an MD5 checksum for the resource at the given path. Will bump the source files version if the checksum doesn't match. + * + * @param resourceName the resource relative to the class + * @return true, if the checksum matches, false otherwise + * @see #getSourceFilesVersion() + */ + private boolean validateResourceChecksum(String resourceName) { + var cls = getClass(); + var logger = LoggerFactory.getLogger(cls); + try (var resource = cls.getResourceAsStream(resourceName)) { + if (resource == null) + throw new MissingResourceException("No such resource at path " + resourceName, File.class.getSimpleName(), resourceName); + String md5 = DigestUtils.md5Hex(resource); + if (!Objects.equals(Preferences.userNodeForPackage(cls).get(resourceName, null), md5)) { + Preferences.userNodeForPackage(cls).put(resourceName, md5); + Preferences.userNodeForPackage(cls).putLong("version", System.currentTimeMillis()); + logger.info("Checksum for source file {} doesn't match", resourceName); + return false; + } + logger.info("Checksum for source file {} matches", resourceName); + return true; + } catch (IOException e) { + logger.error("Couldn't calculate checksum for resource at " + resourceName, e); + return false; + } + } + + /** + * {@return the project alias} + */ + default String getAlias() { + return getProjectOrThrow().getAlias(); + } + + /** + * Returns the File that represents the model for this project. + * + * @return the File that represents the model for this project + */ + default File getModelFile() { + return getProjectOrThrow().getModelFile(); + } + + /** + * {@return the resource name that represents the model for this project} + */ + default String getModelResourceName() { + return getProjectOrThrow().getModelResourceName(); + } + + /** + * Returns the File that represents the model for this project with the given model type. + * + * @param modelType the model type + * @return the File that represents the model for this project + */ + default File getModelFile(ArchitectureModelType modelType) { + return getProjectOrThrow().getModelFile(modelType); + } + + /** + * {@return the resource name that represents the model for this project with the given model type} + * + * @param modelType the model type + */ + default String getModelResourceName(ArchitectureModelType modelType) { + return getProjectOrThrow().getModelResourceName(modelType); + } + + /** + * Returns the File that represents the text for this project. + * + * @return the File that represents the text for this project + */ + default File getTextFile() { + return getProjectOrThrow().getTextFile(); + } + + /** + * {@return the resource name that represents the text for this project} + */ + default String getTextResourceName() { + return getProjectOrThrow().getTextResourceName(); + } + + /** + * Return the map of additional configuration options + * + * @return the map of additional configuration options + */ + default SortedMap getAdditionalConfigurations() { + return getProjectOrThrow().getAdditionalConfigurations(); + } + + /** + * Returns a {@link File} that points to the text file containing additional configurations + * + * @return the file for additional configurations + */ + default File getAdditionalConfigurationsFile() { + return getProjectOrThrow().getAdditionalConfigurationsFile(); + } + + /** + * {@return the resource name that represents the additional configurations for this project} + */ + default String getAdditionalConfigurationsResourceName() { + return getProjectOrThrow().getAdditionalConfigurationsResourceName(); + } + + /** + * Returns the {@link GoldStandard} for this project. + * + * @return the File that represents the gold standard for this project + */ + default File getTlrGoldStandardFile() { + return getProjectOrThrow().getTlrGoldStandardFile(); + } + + /** + * {@return the resource name that represents the TLR {@link GoldStandard} for this project} + */ + default String getTlrGoldStandardResourceName() { + return getProjectOrThrow().getTlrGoldStandardResourceName(); + } + + /** + * Returns a string-list of entries as goldstandard for TLR for this project. + * + * @return a list with the entries of the goldstandard for TLR + */ + default ImmutableList getTlrGoldStandard() { + return getProjectOrThrow().getTlrGoldStandard(); + } + + /** + * Returns the {@link GoldStandard} for this project for the given model connector. + * + * @param architectureModel the model + * @return the {@link GoldStandard} for this project + */ + default GoldStandard getTlrGoldStandard(ArchitectureModel architectureModel) { + return getProjectOrThrow().getTlrGoldStandard(architectureModel); + } + + default MutableList getMissingTextForModelElementGoldStandard() { + return getProjectOrThrow().getMissingTextForModelElementGoldStandard(); + } + + /** + * {@return the {@link GoldStandard} for this project} + */ + default File getMissingTextForModelElementGoldStandardFile() { + return getProjectOrThrow().getMissingTextForModelElementGoldStandardFile(); + } + + /** + * {@return the resource name that represents the MME {@link GoldStandard} for this project} + */ + default String getMissingTextForModelElementGoldStandardResourceName() { + return getProjectOrThrow().getMissingTextForModelElementGoldStandardResourceName(); + } + + /** + * {@return the expected results for Traceability Link Recovery} + */ + default ExpectedResults getExpectedTraceLinkResults() { + return getProjectOrThrow().getExpectedTraceLinkResults(); + } + + /** + * {@return the expected results for Inconsistency Detection} + */ + default ExpectedResults getExpectedInconsistencyResults() { + return getProjectOrThrow().getExpectedInconsistencyResults(); + } + + /** + * Private so the project doesn't get passed around directly, defeating the purpose of making it extensible + * + * @return the project this instance belongs to + */ + private Project getProjectOrThrow() { + return getFromName().orElseThrow(); + } + + /** + * Returns an {@link Optional} containing the project that has a name that equals the given name, ignoring case. + * + * @return the Optional containing the project with the given name or is empty if no such is found. + */ + private Optional getFromName() { + for (Project project : Project.values()) { + if (project.name().equalsIgnoreCase(getProjectName())) { + return Optional.of(project); + } + } + return Optional.empty(); + } +} diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/Project.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/Project.java index 3a5d2ac68..b2929bdb3 100644 --- a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/Project.java +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/Project.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval; import java.io.File; @@ -6,8 +6,9 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; -import java.util.Optional; import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.list.ImmutableList; @@ -23,8 +24,9 @@ /** * This enum captures the different case studies that are used for evaluation in the integration tests. */ -public enum Project { +public enum Project implements GoldStandardProject { MEDIASTORE(// + "MS", // "/benchmark/mediastore/model_2016/pcm/ms.repository", // "/benchmark/mediastore/text_2016/mediastore.txt", // "/benchmark/mediastore/goldstandards/goldstandard_sad_2016-sam_2016.csv", // @@ -34,6 +36,7 @@ public enum Project { new ExpectedResults(.212, .792, .328, .702, .227, .690) // ), // TEASTORE( // + "TS", // "/benchmark/teastore/model_2020/pcm/teastore.repository", // "/benchmark/teastore/text_2020/teastore.txt", // "/benchmark/teastore/goldstandards/goldstandard_sad_2020-sam_2020.csv", // @@ -43,6 +46,7 @@ public enum Project { new ExpectedResults(.962, .703, .784, .957, .808, .994) // ), // TEASTORE_HISTORICAL( // + "TS-H", // "/benchmark/teastore/model_2020/pcm/teastore.repository", // "/benchmark/teastore/text_2018/teastore_2018_AB.txt", // "/benchmark/teastore/goldstandards/goldstandard_sad_2018-sam_2020_AB.csv", // @@ -52,6 +56,7 @@ public enum Project { new ExpectedResults(.163, .982, .278, .376, .146, .289) // ), // TEAMMATES( // + "TM", // "/benchmark/teammates/model_2021/pcm/teammates.repository", // "/benchmark/teammates/text_2021/teammates.txt", // "/benchmark/teammates/goldstandards/goldstandard_sad_2021-sam_2021.csv", // @@ -61,6 +66,7 @@ public enum Project { new ExpectedResults(.175, .745, .279, .851, .287, .851) // ), // TEAMMATES_HISTORICAL( // + "TM-H", // "/benchmark/teammates/model_2021/pcm/teammates.repository", // "/benchmark/teammates/text_2015/teammates_2015.txt", // "/benchmark/teammates/goldstandards/goldstandard_sad_2015-sam_2021.csv", // @@ -70,7 +76,7 @@ public enum Project { new ExpectedResults(.168, .629, .263, .863, .260, .870) // ), // BIGBLUEBUTTON( // - "/benchmark/bigbluebutton/model_2021/pcm/bbb.repository", // + "BBB", "/benchmark/bigbluebutton/model_2021/pcm/bbb.repository", // "/benchmark/bigbluebutton/text_2021/bigbluebutton.txt", // "/benchmark/bigbluebutton/goldstandards/goldstandard_sad_2021-sam_2021.csv", // "/configurations/bbb/filterlists_all.txt", // options: filterlists_none.txt, filterlists_onlyCommon.txt, filterlists_all.txt @@ -79,7 +85,7 @@ public enum Project { new ExpectedResults(.887, .461, .429, .956, .534, .984) // ), // BIGBLUEBUTTON_HISTORICAL( // - "/benchmark/bigbluebutton/model_2021/pcm/bbb.repository", // + "BBB-H", "/benchmark/bigbluebutton/model_2021/pcm/bbb.repository", // "/benchmark/bigbluebutton/text_2015/bigbluebutton_2015.txt", // "/benchmark/bigbluebutton/goldstandards/goldstandard_sad_2015-sam_2021.csv", // "/configurations/bbb/filterlists_all.txt", // options: filterlists_none.txt, filterlists_onlyCommon.txt, filterlists_all.txt @@ -88,7 +94,7 @@ public enum Project { new ExpectedResults(.085, .175, .111, .813, .018, .869) // ), // JABREF( // - "/benchmark/jabref/model_2021/pcm/jabref.repository", // + "JR", "/benchmark/jabref/model_2021/pcm/jabref.repository", // "/benchmark/jabref/text_2021/jabref.txt", // "/benchmark/jabref/goldstandards/goldstandard_sad_2021-sam_2021.csv", // "/configurations/jabref/filterlists_all.txt", // options: filterlists_none.txt, filterlists_onlyCommon.txt, filterlists_all.txt @@ -97,7 +103,7 @@ public enum Project { new ExpectedResults(1.0, .443, .443, .845, .616, 1.0) // ), // JABREF_HISTORICAL( // - "/benchmark/jabref/model_2021/pcm/jabref.repository", // + "JR-H", "/benchmark/jabref/model_2021/pcm/jabref.repository", // "/benchmark/jabref/text_2016/jabref_2016.txt", // "/benchmark/jabref/goldstandards/goldstandard_sad_2016-sam_2021.csv", // "/configurations/jabref/filterlists_all.txt", // options: filterlists_none.txt, filterlists_onlyCommon.txt, filterlists_all.txt @@ -108,6 +114,7 @@ public enum Project { private static final Logger logger = LoggerFactory.getLogger(Project.class); + private final String alias; private final String model; private final String textFile; private final String configurationsFile; @@ -115,9 +122,11 @@ public enum Project { private final String goldStandardMissingTextForModelElement; private final ExpectedResults expectedTraceLinkResults; private final ExpectedResults expectedInconsistencyResults; + private final SortedSet resourceNames; - Project(String model, String textFile, String goldStandardTraceabilityLinkRecovery, String configurationsFile, + Project(String alias, String model, String textFile, String goldStandardTraceabilityLinkRecovery, String configurationsFile, String goldStandardMissingTextForModelElement, ExpectedResults expectedTraceLinkResults, ExpectedResults expectedInconsistencyResults) { + this.alias = alias; this.model = model; this.textFile = textFile; this.configurationsFile = configurationsFile; @@ -125,38 +134,26 @@ public enum Project { this.goldStandardMissingTextForModelElement = goldStandardMissingTextForModelElement; this.expectedTraceLinkResults = expectedTraceLinkResults; this.expectedInconsistencyResults = expectedInconsistencyResults; + resourceNames = new TreeSet<>(List.of(model, textFile, goldStandardTraceabilityLinkRecovery, configurationsFile, + goldStandardMissingTextForModelElement)); } - /** - * Returns an {@link Optional} containing the project that has a name that equals the given name, ignoring case. - * - * @param name the name of the project - * @return the Optional containing the project with the given name or is empty if no such is found. - */ - public static Optional getFromName(String name) { - for (Project project : Project.values()) { - if (project.name().equalsIgnoreCase(name)) { - return Optional.of(project); - } - } - return Optional.empty(); + @Override + public String getAlias() { + return alias; } - /** - * Returns the File that represents the model for this project. - * - * @return the File that represents the model for this project - */ + @Override public File getModelFile() { return ProjectHelper.loadFileFromResources(model); } - /** - * Returns the File that represents the model for this project with the given model type. - * - * @param modelType the model type - * @return the File that represents the model for this project - */ + @Override + public String getModelResourceName() { + return model; + } + + @Override public File getModelFile(ArchitectureModelType modelType) { return switch (modelType) { case PCM -> getModelFile(); @@ -164,15 +161,24 @@ public File getModelFile(ArchitectureModelType modelType) { }; } - /** - * Returns the File that represents the text for this project. - * - * @return the File that represents the text for this project - */ + @Override + public String getModelResourceName(ArchitectureModelType modelType) { + return switch (modelType) { + case PCM -> model; + case UML -> model.replace("/pcm/", "/uml/").replace(".repository", ".uml"); + }; + } + + @Override public File getTextFile() { return ProjectHelper.loadFileFromResources(textFile); } + @Override + public String getTextResourceName() { + return textFile; + } + /** * Return the map of additional configuration options * @@ -182,29 +188,27 @@ public SortedMap getAdditionalConfigurations() { return ConfigurationHelper.loadAdditionalConfigs(getAdditionalConfigurationsFile()); } - /** - * Returns a {@link File} that points to the text file containing additional configurations - * - * @return the file for additional configurations - */ + @Override public File getAdditionalConfigurationsFile() { return ProjectHelper.loadFileFromResources(this.configurationsFile); } - /** - * Returns the {@link GoldStandard} for this project. - * - * @return the File that represents the gold standard for this project - */ + @Override + public String getAdditionalConfigurationsResourceName() { + return configurationsFile; + } + + @Override public File getTlrGoldStandardFile() { return ProjectHelper.loadFileFromResources(goldStandardTraceabilityLinkRecovery); } - /** - * Returns a string-list of entries as goldstandard for TLR for this project. - * - * @return a list with the entries of the goldstandard for TLR - */ + @Override + public String getTlrGoldStandardResourceName() { + return goldStandardTraceabilityLinkRecovery; + } + + @Override public ImmutableList getTlrGoldStandard() { var path = Paths.get(this.getTlrGoldStandardFile().toURI()); List goldLinks = Lists.mutable.empty(); @@ -218,16 +222,12 @@ public ImmutableList getTlrGoldStandard() { return Lists.immutable.ofAll(goldLinks); } - /** - * Returns the {@link GoldStandard} for this project for the given model connector. - * - * @param architectureModel the model - * @return the {@link GoldStandard} for this project - */ + @Override public GoldStandard getTlrGoldStandard(ArchitectureModel architectureModel) { return new GoldStandard(getTlrGoldStandardFile(), architectureModel); } + @Override public MutableList getMissingTextForModelElementGoldStandard() { var path = Paths.get(this.getMissingTextForModelElementGoldStandardFile().toURI()); List goldLinks = Lists.mutable.empty(); @@ -241,25 +241,33 @@ public MutableList getMissingTextForModelElementGoldStandard() { return Lists.mutable.ofAll(goldLinks); } - private File getMissingTextForModelElementGoldStandardFile() { + @Override + public File getMissingTextForModelElementGoldStandardFile() { return ProjectHelper.loadFileFromResources(goldStandardMissingTextForModelElement); } - /** - * Returns the expected results for Traceability Link Recovery. - * - * @return the expectedTraceLinkResults - */ + @Override + public String getMissingTextForModelElementGoldStandardResourceName() { + return goldStandardMissingTextForModelElement; + } + + @Override public ExpectedResults getExpectedTraceLinkResults() { return expectedTraceLinkResults; } - /** - * Returns the expected results for Inconsistency Detection. - * - * @return the expectedInconsistencyResults - */ + @Override public ExpectedResults getExpectedInconsistencyResults() { return expectedInconsistencyResults; } + + @Override + public String getProjectName() { + return this.name(); + } + + @Override + public SortedSet getResourceNames() { + return new TreeSet<>(resourceNames); + } } diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/ProjectHelper.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/ProjectHelper.java index a63139a00..d2e23c59b 100644 --- a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/ProjectHelper.java +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/ProjectHelper.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval; import java.io.File; @@ -11,7 +11,7 @@ import org.slf4j.LoggerFactory; /** - * Helper class for {@link Project} and {@link CodeProject}. + * Helper class for {@link GoldStandardProject} implementations. */ public class ProjectHelper { /** diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/EvaluationResults.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/EvaluationResults.java index e6ab83214..9332bb8f0 100644 --- a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/EvaluationResults.java +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/EvaluationResults.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval.results; import java.util.Locale; @@ -13,31 +13,70 @@ public record EvaluationResults(double precision, double recall, double f1, I ImmutableList falseNegatives, ImmutableList falsePositives, double accuracy, double phiCoefficient, double specificity, double phiCoefficientMax, double phiOverPhiMax) { + public String toRow() { + return String.format(Locale.ENGLISH, """ + %4s & %4s & %4s & %4s & %4s & %4s & %4s + %4.2f & %4.2f & %4.2f & %4.2f & %4.2f & %4.2f & %4.2f""", "P", "R", "F1", "Acc", "Spec", "Phi", "PhiN", precision, recall, f1, accuracy, + specificity, phiCoefficient, phiOverPhiMax); + } + + public String toRow(String headerKey, String headerVal) { + return String.format(Locale.ENGLISH, """ + %10s & %4s & %4s & %4s & %4s & %4s & %4s & %4s + %10s & %4.2f & %4.2f & %4.2f & %4.2f & %4.2f & %4.2f & %4.2f""", headerKey, "P", "R", "F1", "Acc", "Spec", "Phi", "PhiN", headerVal, precision, + recall, f1, accuracy, specificity, phiCoefficient, phiOverPhiMax); + } + @Override public String toString() { - String output = String.format(Locale.ENGLISH, "\tPrecision:%8.2f%n\tRecall:%11.2f%n\tF1:%15.2f", precision, recall, f1); - output += String.format(Locale.ENGLISH, "%n\tAccuracy:%9.2f%n\tSpecificity:%6.2f", accuracy, specificity); - output += String.format(Locale.ENGLISH, "%n\tPhi Coef.:%8.2f%n\tPhi/PhiMax:%7.2f (Phi Max: %.2f)", phiCoefficient, phiOverPhiMax, phiCoefficientMax); - return output; + return String.format(Locale.ENGLISH, """ + \tPrecision:%8.2f + \tRecall:%11.2f + \tF1:%15.2f + \tAccuracy:%9.2f + \tSpecificity:%6.2f + \tPhi Coef.:%8.2f + \tPhi/PhiMax:%7.2f (Phi Max: %.2f) + %s""", precision, recall, f1, accuracy, specificity, phiCoefficient, phiOverPhiMax, phiCoefficientMax, toRow()); } public String getResultStringWithExpected(ExpectedResults expectedResults) { - return String.format(Locale.ENGLISH, - "\tPrecision:%8.2f (min. expected: %.2f)%n\tRecall:%11.2f (min. expected: %.2f)%n\tF1:%15.2f (min. expected: %.2f)", precision, expectedResults - .precision(), recall, expectedResults.recall(), f1, expectedResults.f1()); + return String.format(Locale.ENGLISH, """ + \tPrecision:%8.2f (min. expected: %.2f) + \tRecall:%11.2f (min. expected: %.2f) + \tF1:%15.2f (min. expected: %.2f) + %s""", precision, expectedResults.precision(), recall, expectedResults.recall(), f1, expectedResults.f1(), toRow()); } public String getExtendedResultStringWithExpected(ExpectedResults expectedResults) { StringBuilder outputBuilder = new StringBuilder(); - outputBuilder.append(String.format(Locale.ENGLISH, - "\tPrecision:%8.2f (min. expected: %.2f)%n\tRecall:%11.2f (min. expected: %.2f)%n\tF1:%15.2f (min. expected: %.2f)", precision, expectedResults - .precision(), recall, expectedResults.recall(), f1, expectedResults.f1())); - outputBuilder.append(String.format(Locale.ENGLISH, "%n\tAccuracy:%9.2f (min. expected: %.2f)%n\tSpecificity:%6.2f (min. expected: %.2f)", accuracy, - expectedResults.accuracy(), specificity, expectedResults.specificity())); - outputBuilder.append(String.format(Locale.ENGLISH, "%n\tPhi Coef.:%8.2f (min. expected: %.2f)", phiCoefficient, expectedResults.phiCoefficient())); + outputBuilder.append(String.format(Locale.ENGLISH, """ + \tPrecision:%8.2f (min. expected: %.2f) + \tRecall:%11.2f (min. expected: %.2f) + \tF1:%15.2f (min. expected: %.2f)""", precision, expectedResults.precision(), recall, expectedResults.recall(), f1, expectedResults.f1())); + outputBuilder.append(String.format(Locale.ENGLISH, """ + + \tAccuracy:%9.2f (min. expected: %.2f) + \tSpecificity:%6.2f (min. expected: %.2f)""", accuracy, expectedResults.accuracy(), specificity, expectedResults.specificity())); + outputBuilder.append(String.format(Locale.ENGLISH, """ + + \tPhi Coef.:%8.2f (min. expected: %.2f) + \tPhi/PhiMax:%7.2f (Phi Max: %.2f) + %s""", phiCoefficient, expectedResults.phiCoefficient(), phiOverPhiMax, phiCoefficientMax, toRow())); return outputBuilder.toString(); } + public String getExplicitResultString() { + return String.format(Locale.ENGLISH, """ + \tTP:%15d + \tFP:%15d + \tTN:%15d + \tFN:%15d + \tP:%16d + \tN:%16d""", truePositives.size(), falsePositives.size(), trueNegatives, falseNegatives.size(), truePositives.size() + falseNegatives.size(), + trueNegatives + falsePositives.size()); + } + /** * returns the weight (truePos + falseNeg) * diff --git a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/ExpectedResults.java b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/ExpectedResults.java index 3d81c4470..37d2c6dfe 100644 --- a/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/ExpectedResults.java +++ b/tests/tests-base/src/main/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/results/ExpectedResults.java @@ -1,9 +1,13 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval.results; +import java.io.Serializable; + +import com.google.errorprone.annotations.Immutable; + /** * This record represents expected results for an evaluation - * + * * @param precision the expected precision * @param recall the expected recall * @param f1 the expected F1 score @@ -11,7 +15,8 @@ * @param phiCoefficient the expected Phi Coefficient * @param specificity the expected specificity */ -public record ExpectedResults(double precision, double recall, double f1, double accuracy, double phiCoefficient, double specificity) { +@Immutable +public record ExpectedResults(double precision, double recall, double f1, double accuracy, double phiCoefficient, double specificity) implements Serializable { public ExpectedResults(double precision, double recall, double f1) { this(precision, recall, f1, .0, .0, .0); diff --git a/tests/tests-base/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProjectTest.java b/tests/tests-base/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProjectTest.java index 2bec65088..fff77ce42 100644 --- a/tests/tests-base/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProjectTest.java +++ b/tests/tests-base/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/CodeProjectTest.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval; import org.junit.jupiter.api.Assertions; @@ -9,7 +9,7 @@ class CodeProjectTest { @EnumSource(CodeProject.class) @ParameterizedTest(name = "{0}") void testFiles(CodeProject project) { - Assertions.assertNotNull(project.getProject()); + Assertions.assertNotNull(project); Assertions.assertNotNull(project.getCodeRepository()); Assertions.assertNotNull(project.getCommitHash()); Assertions.assertNotNull(project.getCodeLocation()); diff --git a/tests/tests-inconsistency/pom.xml b/tests/tests-inconsistency/pom.xml index 6105cd7a5..270969343 100644 --- a/tests/tests-inconsistency/pom.xml +++ b/tests/tests-inconsistency/pom.xml @@ -78,6 +78,17 @@ org.apache.maven.plugins maven-failsafe-plugin + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + diff --git a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaseline.java b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaseline.java index aaa6cc6f6..b99113b43 100644 --- a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaseline.java +++ b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaseline.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval.baseline; import java.util.List; @@ -10,7 +10,6 @@ * Baseline approach for inconsistency detection */ public class InconsistencyBaseline extends AbstractExecutionStage { - public InconsistencyBaseline(DataRepository dataRepository) { super(List.of(new InconsistencyBaselineAgent(dataRepository)), "InconsistencyBaseline", dataRepository); } diff --git a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaselineInformant.java b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaselineInformant.java index ea611b098..695a90e1d 100644 --- a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaselineInformant.java +++ b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/baseline/InconsistencyBaselineInformant.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval.baseline; import java.util.SortedMap; @@ -26,7 +26,7 @@ protected InconsistencyBaselineInformant(DataRepository dataRepository) { } @Override - public void run() { + public void process() { var inconsistencyStates = InconsistencyStatesImpl.build(); DataRepository dataRepository = getDataRepository(); dataRepository.addData(InconsistencyStates.ID, inconsistencyStates); @@ -45,7 +45,7 @@ public void run() { InconsistencyState inconsistencyState = inconsistencyStates.getInconsistencyState(metamodel); for (var sentence : sentencesWithoutTraceLinks) { - inconsistencyState.addInconsistency(new MissingModelInstanceInconsistency("", sentence + 1, 0.69)); + inconsistencyState.addInconsistency(new MissingModelInstanceInconsistency("", sentence + 1, 0.69, null)); } } } diff --git a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/InconsistencyDetectionEvaluationIT.java b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/InconsistencyDetectionEvaluationIT.java index 789c8595c..965e94129 100644 --- a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/InconsistencyDetectionEvaluationIT.java +++ b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/InconsistencyDetectionEvaluationIT.java @@ -6,7 +6,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -14,6 +15,7 @@ import java.util.stream.Collectors; import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.factory.Maps; import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.api.tuple.Pair; @@ -40,6 +42,7 @@ import edu.kit.kastel.mcse.ardoco.core.inconsistency.types.MissingModelInstanceInconsistency; import edu.kit.kastel.mcse.ardoco.core.models.connectors.generators.architecture.pcm.PcmExtractor; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; @@ -55,7 +58,7 @@ * are the spots of inconsistency then. We run this multiple times so each element was held back once. */ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class InconsistencyDetectionEvaluationIT { +public class InconsistencyDetectionEvaluationIT { private static final Logger logger = LoggerFactory.getLogger(InconsistencyDetectionEvaluationIT.class); private static final String OUTPUT = "target/testout"; @@ -72,10 +75,14 @@ class InconsistencyDetectionEvaluationIT { */ private static final MutableList> OVERALL_UME_RESULTS = Lists.mutable.empty(); + private static final Map, ExpectedResults>> MME_RESULTS = new LinkedHashMap<>(); + private static final Map> MME_RESULTS_BASELINE = new LinkedHashMap<>(); + private static final Map> UME_RESULTS = new LinkedHashMap<>(); + private static final String LINE_SEPARATOR = System.lineSeparator(); private static boolean ranBaseline = false; - private static final Map> inconsistentSentencesPerProject = new EnumMap<>(Project.class); - private static final Map arDoCoResults = new EnumMap<>(Project.class); + private static final Map> inconsistentSentencesPerProject = new HashMap<>(); + private static final Map arDoCoResults = new HashMap<>(); /** * Tests the inconsistency detection for missing model elements on all {@link Project projects}. @@ -84,14 +91,14 @@ class InconsistencyDetectionEvaluationIT { * here * Example: add ", names = { "BIGBLUEBUTTON" }" to EnumSource However, make sure to revert this before you commit and push! * - * @param project Project that gets inserted automatically with the enum {@link Project}. + * @param goldStandardProject Project that gets inserted automatically with the enum {@link Project}. */ @DisplayName("Evaluating MME-Inconsistency Detection") @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") @EnumSource(value = Project.class, mode = EnumSource.Mode.MATCH_NONE, names = "^.*HISTORICAL$") @Order(1) - void missingModelElementInconsistencyIT(Project project) { - runMissingModelElementInconsistencyEval(project); + protected void missingModelElementInconsistencyIT(GoldStandardProject goldStandardProject) { + runMissingModelElementInconsistencyEval(goldStandardProject, goldStandardProject.getExpectedInconsistencyResults()); } @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") @@ -99,40 +106,40 @@ void missingModelElementInconsistencyIT(Project project) { @ParameterizedTest(name = "Evaluating MME-Inconsistency for {0}") @EnumSource(value = Project.class, mode = EnumSource.Mode.MATCH_ALL, names = "^.*HISTORICAL$") @Order(2) - void missingModelElementInconsistencyHistoricIT(Project project) { - runMissingModelElementInconsistencyEval(project); + protected void missingModelElementInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + runMissingModelElementInconsistencyEval(goldStandardProject, goldStandardProject.getExpectedInconsistencyResults()); } - private void runMissingModelElementInconsistencyEval(Project project) { - logger.info("Start evaluation of MME-inconsistency for {}", project.name()); - Map runs = produceRuns(project); + protected void runMissingModelElementInconsistencyEval(GoldStandardProject goldStandardProject, ExpectedResults expectedInconsistencyResults) { + logger.info("Start evaluation of MME-inconsistency for {}", goldStandardProject.getProjectName()); + Map runs = produceRuns(goldStandardProject); - var results = calculateEvaluationResults(project, runs); + var results = calculateEvaluationResults(goldStandardProject, runs); OVERALL_MME_RESULTS.addAll(results); EvaluationResults weightedResults = ResultCalculatorUtil.calculateWeightedAverageResults(results.toImmutable()); - var expectedInconsistencyResults = project.getExpectedInconsistencyResults(); - logResultsMissingModelInconsistency(project, weightedResults, expectedInconsistencyResults); + MME_RESULTS.put(goldStandardProject, Tuples.pair(weightedResults, expectedInconsistencyResults)); + logResultsMissingModelInconsistency(goldStandardProject, weightedResults, expectedInconsistencyResults); checkResults(weightedResults, expectedInconsistencyResults); - writeOutResults(project, results, runs); + writeOutResults(goldStandardProject, results, runs); } /** * Tests the baseline approach that reports a missing model element inconsistency for each sentence that is not traced to a model element. This test is * enabled by providing the environment variable "testBaseline" with any value. * - * @param project Project that gets inserted automatically with the enum {@link Project}. + * @param goldStandardProject Project that gets inserted automatically with the enum {@link Project}. */ @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") @DisplayName("Evaluating MME-Inconsistency Detection Baseline") @ParameterizedTest(name = "Evaluating Baseline for {0}") @EnumSource(value = Project.class, mode = EnumSource.Mode.MATCH_NONE, names = "^.*HISTORICAL$") @Order(5) - void missingModelElementInconsistencyBaselineIT(Project project) { - runMissingModelElementInconsistencyBaselineEval(project); + protected void missingModelElementInconsistencyBaselineIT(GoldStandardProject goldStandardProject) { + runMissingModelElementInconsistencyBaselineEval(goldStandardProject); } @EnabledIfEnvironmentVariable(named = "testBaseline", matches = ".*") @@ -140,25 +147,27 @@ void missingModelElementInconsistencyBaselineIT(Project project) { @ParameterizedTest(name = "Evaluating Baseline for {0}") @EnumSource(value = Project.class, mode = EnumSource.Mode.MATCH_ALL, names = "^.*HISTORICAL$") @Order(6) - void missingModelElementInconsistencyBaselineHistoricIT(Project project) { - runMissingModelElementInconsistencyBaselineEval(project); + protected void missingModelElementInconsistencyBaselineHistoricIT(GoldStandardProject goldStandardProject) { + runMissingModelElementInconsistencyBaselineEval(goldStandardProject); } - private void runMissingModelElementInconsistencyBaselineEval(Project project) { - logger.info("Start evaluation of MME-inconsistency baseline for {}", project.name()); + protected void runMissingModelElementInconsistencyBaselineEval(GoldStandardProject goldStandardProject) { + logger.info("Start evaluation of MME-inconsistency baseline for {}", goldStandardProject.getProjectName()); ranBaseline = true; HoldBackRunResultsProducer holdBackRunResultsProducer = new HoldBackRunResultsProducer(); - Map runs = holdBackRunResultsProducer.produceHoldBackRunResults(project, true); + Map runs = holdBackRunResultsProducer.produceHoldBackRunResults(goldStandardProject, true); - Assertions.assertTrue(runs != null && runs.size() > 0); + Assertions.assertTrue(runs != null && !runs.isEmpty()); - var results = calculateEvaluationResults(project, runs); + var results = calculateEvaluationResults(goldStandardProject, runs); OVERALL_MME_RESULTS_BASELINE.addAll(results); + var weightedResults = ResultCalculatorUtil.calculateWeightedAverageResults(results.toImmutable()); + MME_RESULTS_BASELINE.put(goldStandardProject, weightedResults); + if (logger.isInfoEnabled()) { - var weightedResults = ResultCalculatorUtil.calculateWeightedAverageResults(results.toImmutable()); - String name = project.name() + " missing model inconsistency"; + String name = goldStandardProject.getProjectName() + " missing model inconsistency"; TestUtil.logResults(logger, name, weightedResults); } } @@ -166,61 +175,66 @@ private void runMissingModelElementInconsistencyBaselineEval(Project project) { /** * Tests the inconsistency detection for undocumented model elements on all {@link Project projects}. * - * @param project Project that gets inserted automatically with the enum {@link Project}. + * @param goldStandardProject Project that gets inserted automatically with the enum {@link Project}. */ @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies") @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") @EnumSource(value = Project.class, mode = EnumSource.Mode.MATCH_NONE, names = "^.*HISTORICAL$") @Order(10) - void missingTextInconsistencyIT(Project project) { - runMissingTextInconsistencyEval(project); + protected void missingTextInconsistencyIT(GoldStandardProject goldStandardProject) { + runMissingTextInconsistencyEval(goldStandardProject); } @EnabledIfEnvironmentVariable(named = "testHistoric", matches = ".*") - @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies (Historical)") + @DisplayName("Evaluate Inconsistency Analyses For MissingTextForModelElementInconsistencies " + "(Historical)") @ParameterizedTest(name = "Evaluating UME-inconsistency for {0}") @EnumSource(value = Project.class, mode = EnumSource.Mode.MATCH_ALL, names = "^.*HISTORICAL$") @Order(11) - void missingTextInconsistencyHistoricIT(Project project) { - runMissingTextInconsistencyEval(project); + protected void missingTextInconsistencyHistoricIT(GoldStandardProject goldStandardProject) { + runMissingTextInconsistencyEval(goldStandardProject); } - private void runMissingTextInconsistencyEval(Project project) { - var projectResults = arDoCoResults.get(project); + private void runMissingTextInconsistencyEval(GoldStandardProject goldStandardProject) { + var projectResults = arDoCoResults.get(goldStandardProject); if (projectResults == null) { - produceRuns(project); - projectResults = arDoCoResults.get(project); + produceRuns(goldStandardProject); + projectResults = arDoCoResults.get(goldStandardProject); } Assertions.assertNotNull(projectResults, "No results found."); - MutableList expectedInconsistentModelElements = project.getMissingTextForModelElementGoldStandard(); + MutableList expectedInconsistentModelElements = goldStandardProject.getMissingTextForModelElementGoldStandard(); var inconsistentModelElements = projectResults.getAllModelInconsistencies().collect(ModelInconsistency::getModelInstanceUid).toList(); var results = TestUtil.compareInconsistencies(projectResults, inconsistentModelElements.toImmutable(), expectedInconsistentModelElements.toImmutable()); OVERALL_UME_RESULTS.add(results); + UME_RESULTS.put(goldStandardProject, results); - String name = project.name() + " missing text inconsistency"; + String name = goldStandardProject.getProjectName() + " missing text inconsistency"; TestUtil.logExplicitResults(logger, name, results); - writeOutResults(project, results); + writeOutResults(goldStandardProject, results); } - private static Map produceRuns(Project project) { - HoldBackRunResultsProducer holdBackRunResultsProducer = new HoldBackRunResultsProducer(); + private Map produceRuns(GoldStandardProject goldStandardProject) { + HoldBackRunResultsProducer holdBackRunResultsProducer = getHoldBackRunResultsProducer(); - Map runs = holdBackRunResultsProducer.produceHoldBackRunResults(project, false); + Map runs = holdBackRunResultsProducer.produceHoldBackRunResults(goldStandardProject, false); ArDoCoResult baseArDoCoResult = runs.get(null); - saveOutput(project, baseArDoCoResult); - arDoCoResults.put(project, baseArDoCoResult); + saveOutput(goldStandardProject, baseArDoCoResult); + arDoCoResults.put(goldStandardProject, baseArDoCoResult); return runs; } + protected HoldBackRunResultsProducer getHoldBackRunResultsProducer() { + return new HoldBackRunResultsProducer(); + } + @EnabledIfEnvironmentVariable(named = "overallResults", matches = ".*") @Test @Order(999) void overAllResultsIT() { var weightedResults = ResultCalculatorUtil.calculateWeightedAverageResults(OVERALL_MME_RESULTS.toImmutable()); - var macroResults = ResultCalculatorUtil.calculateWeightedAverageResults(OVERALL_MME_RESULTS.toImmutable()); + var macroResults = ResultCalculatorUtil.calculateAverageResults(OVERALL_MME_RESULTS.toImmutable()); Assertions.assertNotNull(weightedResults); Assertions.assertNotNull(macroResults); @@ -232,26 +246,50 @@ void overAllResultsIT() { Assertions.assertNotNull(macroUMEResults); if (logger.isInfoEnabled()) { - var name = "MME Overall Weighted"; - TestUtil.logResults(logger, name, weightedResults); + var mmeBaselineMacro = ResultCalculatorUtil.calculateAverageResults(OVERALL_MME_RESULTS_BASELINE.toImmutable()); + var mmeBaselineWeighted = ResultCalculatorUtil.calculateWeightedAverageResults(OVERALL_MME_RESULTS_BASELINE.toImmutable()); + ; + + var mmeOverallWeightedName = "MME Overall Weighted"; + TestUtil.logResults(logger, mmeOverallWeightedName, weightedResults); - name = "MME Overall Macro"; - TestUtil.logResults(logger, name, macroResults); + var mmeOverallMacroName = "MME Overall Macro"; + TestUtil.logResults(logger, mmeOverallMacroName, macroResults); + var mmeBaselineOverallWeightedName = "MME BASELINE Overall Weighted"; + var mmeBaselineOverallMacroName = "MME BASELINE Overall Macro"; if (ranBaseline) { - name = "MME BASELINE Overall Weighted"; - var results = ResultCalculatorUtil.calculateWeightedAverageResults(OVERALL_MME_RESULTS_BASELINE.toImmutable()); - TestUtil.logResults(logger, name, results); + TestUtil.logResults(logger, mmeBaselineOverallWeightedName, mmeBaselineWeighted); + TestUtil.logResults(logger, mmeBaselineOverallMacroName, mmeBaselineMacro); + } + + var umeOverallWeightedName = "Undoc. Model Element Overall Weighted"; + TestUtil.logResults(logger, umeOverallWeightedName, weightedUMEResults); + var umeOverallMacroName = "Undoc. Model Element Overall Macro"; + TestUtil.logResults(logger, umeOverallMacroName, macroUMEResults); - name = "MME BASELINE Overall Macro"; - results = ResultCalculatorUtil.calculateAverageResults(OVERALL_MME_RESULTS_BASELINE.toImmutable()); - TestUtil.logResults(logger, name, results); + logger.info("MME"); + for (var entry : MME_RESULTS.entrySet()) { + TestUtil.logExtendedResultsAsRow(logger, "Proj", entry.getKey().getAlias(), entry.getValue().getOne()); } + TestUtil.logExtendedResultsAsRow(logger, "-", "Macro", macroResults); + TestUtil.logExtendedResultsAsRow(logger, "-", "Weighted", weightedResults); - name = "Undoc. Model Element Overall Weighted"; - TestUtil.logResults(logger, name, weightedUMEResults); - name = "Undoc. Model Element Overall Macro"; - TestUtil.logResults(logger, name, macroUMEResults); + if (ranBaseline) { + logger.info("MME Baseline"); + for (var entry : MME_RESULTS_BASELINE.entrySet()) { + TestUtil.logExtendedResultsAsRow(logger, "Proj", entry.getKey().getAlias(), entry.getValue()); + } + TestUtil.logExtendedResultsAsRow(logger, "-", "Macro", mmeBaselineMacro); + TestUtil.logExtendedResultsAsRow(logger, "-", "Weighted", mmeBaselineWeighted); + } + + logger.info("UME"); + for (var entry : UME_RESULTS.entrySet()) { + TestUtil.logExtendedResultsAsRow(logger, "Proj", entry.getKey().getAlias(), entry.getValue()); + } + TestUtil.logExtendedResultsAsRow(logger, "-", "Macro", macroUMEResults); + TestUtil.logExtendedResultsAsRow(logger, "-", "Weighted", weightedUMEResults); } try { @@ -262,26 +300,25 @@ void overAllResultsIT() { } } - private MutableList> calculateEvaluationResults(Project project, Map runs) { + private MutableList> calculateEvaluationResults(GoldStandardProject goldStandardProject, Map runs) { - MutableList> results = Lists.mutable.empty(); + Map> results = Maps.mutable.empty(); for (var run : runs.entrySet()) { ModelElement modelInstance = run.getKey(); ArDoCoResult arDoCoResult = run.getValue(); - var runEvalResults = evaluateRun(project, modelInstance, arDoCoResult); + var runEvalResults = evaluateRun(goldStandardProject, modelInstance, arDoCoResult); if (runEvalResults != null) { - results.add(runEvalResults); + results.put(modelInstance, runEvalResults); } else { // for the base case, instead of calculating results, save the found inconsistencies. - inconsistentSentencesPerProject.put(project, arDoCoResult.getInconsistentSentences()); + inconsistentSentencesPerProject.put(goldStandardProject, arDoCoResult.getInconsistentSentences()); } } - - return results; + return Lists.mutable.ofAll(results.values()); } - private EvaluationResults evaluateRun(Project project, ModelElement removedElement, ArDoCoResult arDoCoResult) { + private EvaluationResults evaluateRun(GoldStandardProject goldStandardProject, ModelElement removedElement, ArDoCoResult arDoCoResult) { var modelId = arDoCoResult.getModelIds().get(0); ImmutableList inconsistencies = arDoCoResult.getInconsistenciesOfTypeForModel(modelId, @@ -291,7 +328,7 @@ private EvaluationResults evaluateRun(Project project, ModelElement remo return null; } - var goldStandard = project.getTlrGoldStandard(getPcmModel(project)); + var goldStandard = goldStandardProject.getTlrGoldStandard(getPcmModel(goldStandardProject)); var expectedLines = goldStandard.getSentencesWithElement(removedElement).distinct().collect(Object::toString); var actualSentences = inconsistencies.collect(MissingModelInstanceInconsistency::sentence).distinct().collect(Object::toString); @@ -303,18 +340,15 @@ private static EvaluationResults calculateEvaluationResults(ArDoCoResult return TestUtil.compareInconsistencies(arDoCoResult, actualSentences, expectedLines); } - private static ArchitectureModel getPcmModel(Project project) { - try { - return new PcmExtractor(project.getModelFile().getAbsolutePath()).extractModel(); - } catch (Exception e) { - throw new RuntimeException(e); - } + private static ArchitectureModel getPcmModel(GoldStandardProject goldStandardProject) { + return new PcmExtractor(goldStandardProject.getModelFile().getAbsolutePath()).extractModel(); } - private void logResultsMissingModelInconsistency(Project project, EvaluationResults weightedAverageResult, ExpectedResults expectedResults) { + private void logResultsMissingModelInconsistency(GoldStandardProject goldStandardProject, EvaluationResults weightedAverageResult, + ExpectedResults expectedResults) { if (logger.isInfoEnabled()) { - String name = project.name() + " missing model inconsistency"; - TestUtil.logResultsWithExpected(logger, name, weightedAverageResult, expectedResults); + String name = goldStandardProject.getProjectName() + " missing model inconsistency"; + TestUtil.logExtendedResultsWithExpected(logger, this, name, weightedAverageResult, expectedResults); } } @@ -329,11 +363,11 @@ private void checkResults(EvaluationResults results, ExpectedResults exp .accuracy() >= expectedResults.accuracy(), "Accuracy " + results .accuracy() + " is below the expected minimum value " + expectedResults.accuracy()), // () -> Assertions.assertTrue(results.phiCoefficient() >= expectedResults.phiCoefficient(), "Phi coefficient " + results - .phiCoefficient() + " is below the expected minimum value " + expectedResults.phiCoefficient())); + .phiCoefficient() + " is below the expected " + "minimum value " + expectedResults.phiCoefficient())); } - private void writeOutResults(Project project, List> results, Map runs) { - var outputs = createOutput(project, results, runs); + private void writeOutResults(GoldStandardProject goldStandardProject, List> results, Map runs) { + var outputs = createOutput(goldStandardProject, results, runs); var outputBuilder = outputs.getOne(); var detailedOutputBuilder = outputs.getTwo(); @@ -346,7 +380,7 @@ private void writeOutResults(Project project, List> re logger.warn("Could not create directories.", e); } - String projectFileName = "inconsistencies_" + project.name().toLowerCase() + ".txt"; + String projectFileName = "inconsistencies_" + goldStandardProject.getProjectName() + ".txt"; var filename = idEvalPath.resolve(projectFileName).toFile().getAbsolutePath(); FilePrinter.writeToFile(filename, outputBuilder.toString()); @@ -355,7 +389,7 @@ private void writeOutResults(Project project, List> re FilePrinter.writeToFile(detailedFilename, detailedOutputBuilder.toString()); } - private void writeOutResults(Project project, EvaluationResults results) { + private void writeOutResults(GoldStandardProject goldStandardProject, EvaluationResults results) { Path outputPath = Path.of(OUTPUT); Path idEvalPath = outputPath.resolve(DIRECTORY_NAME); try { @@ -365,7 +399,7 @@ private void writeOutResults(Project project, EvaluationResults results) logger.warn("Could not create directories.", e); } - var outputBuilder = createStringBuilderWithHeader(project); + var outputBuilder = createStringBuilderWithHeader(goldStandardProject); outputBuilder.append(TestUtil.createResultLogString("Inconsistent Model Elements", results)); outputBuilder.append(LINE_SEPARATOR); outputBuilder.append("Number of True Positives: ").append(results.truePositives().size()); @@ -374,16 +408,16 @@ private void writeOutResults(Project project, EvaluationResults results) outputBuilder.append(LINE_SEPARATOR); outputBuilder.append("Number of False Negatives: ").append(results.falseNegatives().size()); - String projectFileName = "inconsistentModelElements_" + project.name().toLowerCase() + ".txt"; + String projectFileName = "inconsistentModelElements_" + goldStandardProject.getProjectName() + ".txt"; var filename = idEvalPath.resolve(projectFileName).toFile().getAbsolutePath(); FilePrinter.writeToFile(filename, outputBuilder.toString()); } - private static void saveOutput(Project project, ArDoCoResult arDoCoResult) { - Objects.requireNonNull(project); + private static void saveOutput(GoldStandardProject goldStandardProject, ArDoCoResult arDoCoResult) { + Objects.requireNonNull(goldStandardProject); Objects.requireNonNull(arDoCoResult); - String projectName = project.name().toLowerCase(); + String projectName = goldStandardProject.getProjectName(); var outputDir = Path.of(OUTPUT); var filename = projectName + ".txt"; @@ -393,9 +427,9 @@ private static void saveOutput(Project project, ArDoCoResult arDoCoResult) { FilePrinter.writeInconsistencyOutput(outputFileID, arDoCoResult); } - private static Pair createOutput(Project project, List> results, + private static Pair createOutput(GoldStandardProject goldStandardProject, List> results, Map runs) { - StringBuilder outputBuilder = createStringBuilderWithHeader(project); + StringBuilder outputBuilder = createStringBuilderWithHeader(goldStandardProject); var resultCalculatorStringBuilderPair = inspectResults(results, runs, outputBuilder); var resultCalculator = resultCalculatorStringBuilderPair.getOne(); outputBuilder.append(getOverallResultsString(resultCalculator)); @@ -417,8 +451,8 @@ private static void writeOutput(EvaluationResults weightedResults, Evalu outputBuilder.append(LINE_SEPARATOR); for (var entry : inconsistentSentencesPerProject.entrySet()) { - var project = entry.getKey(); - outputBuilder.append("## ").append(project.name()); + var goldStandardProject = entry.getKey(); + outputBuilder.append("## ").append(goldStandardProject.getProjectName()); outputBuilder.append(LINE_SEPARATOR); var inconsistentSentences = entry.getValue(); for (var inconsistentSentence : inconsistentSentences) { @@ -430,13 +464,14 @@ private static void writeOutput(EvaluationResults weightedResults, Evalu Files.writeString(outputFile, outputBuilder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } + //FIXME Something is wrong with this. private static void writeOverallOutputMissingTextInconsistency(EvaluationResults weightedResults, EvaluationResults macroResults) throws IOException { var evalDir = Path.of(OUTPUT).resolve(DIRECTORY_NAME); Files.createDirectories(evalDir); var outputFile = evalDir.resolve("_MissingTextInconsistency_Overall_Results.md"); - var outputBuilder = new StringBuilder("# Inconsistency Detection - Missing Text For Model Element").append(LINE_SEPARATOR); + var outputBuilder = new StringBuilder("# Inconsistency Detection - Missing Text For Model " + "Element").append(LINE_SEPARATOR); var resultString = TestUtil.createResultLogString("Overall Weighted", weightedResults); outputBuilder.append(resultString).append(LINE_SEPARATOR); @@ -449,15 +484,15 @@ private static String getOverallResultsString(MutableList>, StringBuilder> inspe outputBuilder.append(LINE_SEPARATOR); detailedOutputBuilder.append(LINE_SEPARATOR); var result = results.get(counter++); - var resultString = String.format(Locale.ENGLISH, "Precision: %.3f, Recall: %.3f, F1: %.3f, Accuracy: %.3f, Phi Coef.: %.3f", result.precision(), - result.recall(), result.f1(), result.accuracy(), result.phiCoefficient()); + var resultString = String.format(Locale.ENGLISH, "Precision: %.3f, Recall: %.3f, F1: %" + ".3f, Accuracy: %.3f, Phi Coef.: %.3f", result + .precision(), result.recall(), result.f1(), result.accuracy(), result.phiCoefficient()); outputBuilder.append(resultString); detailedOutputBuilder.append(resultString); inspectRun(outputBuilder, detailedOutputBuilder, resultsWithWeight, arDoCoResult, result); @@ -504,8 +539,8 @@ private static void inspectRun(StringBuilder outputBuilder, StringBuilder detail var falseNegatives = result.falseNegatives().toList(); appendResults(falseNegatives, detailedOutputBuilder, "False Negatives", arDoCoResult, outputBuilder); - var results = EvaluationResults.createEvaluationResults(new ResultMatrix(truePositives.toImmutable(), 0, falsePositives.toImmutable(), - falseNegatives.toImmutable())); + var results = EvaluationResults.createEvaluationResults(new ResultMatrix<>(truePositives.toImmutable(), 0, falsePositives.toImmutable(), falseNegatives + .toImmutable())); allResults.add(results); } @@ -557,5 +592,4 @@ private static ImmutableList getInitialIncons var id = arDoCoResult.getModelIds().get(0); return arDoCoResult.getInconsistenciesOfTypeForModel(id, MissingModelInstanceInconsistency.class); } - } diff --git a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackArCoTLModelProvider.java b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackArCoTLModelProvider.java index e101a4586..5a63c351c 100644 --- a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackArCoTLModelProvider.java +++ b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackArCoTLModelProvider.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.inconsistencyhelper; import java.io.File; @@ -16,7 +16,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.Model; import edu.kit.kastel.mcse.ardoco.core.api.models.arcotl.architecture.ArchitectureComponent; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; -import edu.kit.kastel.mcse.ardoco.core.models.ArCoTLModelProviderAgent; +import edu.kit.kastel.mcse.ardoco.core.models.agents.ArCoTLModelProviderAgent; import edu.kit.kastel.mcse.ardoco.core.models.connectors.generators.Extractor; import edu.kit.kastel.mcse.ardoco.core.models.connectors.generators.architecture.pcm.PcmExtractor; diff --git a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackRunResultsProducer.java b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackRunResultsProducer.java index 5a4f2210a..28663e5e5 100644 --- a/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackRunResultsProducer.java +++ b/tests/tests-inconsistency/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/inconsistencyhelper/HoldBackRunResultsProducer.java @@ -1,129 +1,129 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.inconsistencyhelper; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.SortedMap; -import org.junit.jupiter.api.Assertions; - -import edu.kit.kastel.mcse.ardoco.core.api.PreprocessingData; import edu.kit.kastel.mcse.ardoco.core.api.models.ModelElement; import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; import edu.kit.kastel.mcse.ardoco.core.common.util.DataRepositoryHelper; import edu.kit.kastel.mcse.ardoco.core.connectiongenerator.ConnectionGenerator; import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; -import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; -import edu.kit.kastel.mcse.ardoco.core.execution.ConfigurationHelper; +import edu.kit.kastel.mcse.ardoco.core.data.DeepCopy; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner; import edu.kit.kastel.mcse.ardoco.core.inconsistency.InconsistencyChecker; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractPipelineStep; import edu.kit.kastel.mcse.ardoco.core.recommendationgenerator.RecommendationGenerator; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.baseline.InconsistencyBaseline; import edu.kit.kastel.mcse.ardoco.core.text.providers.TextPreprocessingAgent; import edu.kit.kastel.mcse.ardoco.core.textextraction.TextExtraction; -public class HoldBackRunResultsProducer { - private File inputText; - private File inputModel; +/** + * Produces the inconsistency detection runs. The first run uses all model elements for the baseline. For each subsequent run a single model element is removed + * to simulate a missing model element. + */ +public class HoldBackRunResultsProducer implements Serializable { + protected File inputText; + protected File inputModel; + protected SortedMap additionalConfigs; public HoldBackRunResultsProducer() { super(); } /** - * Runs ArDoCo or the ArDoCo-backed baseline approach multiple times to produce results. The first run calls ArDoCo - * normally, in further runs one element is held back each time (so that each element was held back once). This way, - * we can simulate missing elements. + * Runs ArDoCo or the ArDoCo-backed baseline approach multiple times to produce results. The first run calls ArDoCo normally, in further runs one element is + * held back each time (so that each element was held back once). This way, we can simulate missing elements. * - * @param project the project that should be run + * @param goldStandardProject the project that should be run * @param useBaselineApproach set to true if the baseline approach should be used instead of ArDoCo - * @return a map containing the mapping from ModelElement that was held back to the DataStructure that was produced - * when running ArDoCo without the ModelElement + * @return a map containing the mapping from ModelElement that was held back to the DataStructure that was produced when running ArDoCo without the + * ModelElement */ - public Map produceHoldBackRunResults(Project project, boolean useBaselineApproach) { + public Map produceHoldBackRunResults(GoldStandardProject goldStandardProject, boolean useBaselineApproach) { Map runs = new LinkedHashMap<>(); - - inputModel = project.getModelFile(); - inputText = project.getTextFile(); + inputModel = goldStandardProject.getModelFile(); + inputText = goldStandardProject.getTextFile(); + additionalConfigs = goldStandardProject.getAdditionalConfigurations(); HoldBackArCoTLModelProvider holdBackArCoTLModelProvider = new HoldBackArCoTLModelProvider(inputModel); - ArDoCo arDoCoBaseRun; - try { - arDoCoBaseRun = definePipelineBase(project, inputText, holdBackArCoTLModelProvider, useBaselineApproach); - } catch (IOException e) { - Assertions.fail(e); - return runs; - } - arDoCoBaseRun.run(); - var baseRunData = new ArDoCoResult(arDoCoBaseRun.getDataRepository()); + var preRunDataRepository = runShared(goldStandardProject); + + var baseRunData = new ArDoCoResult(runUnshared(goldStandardProject, holdBackArCoTLModelProvider, preRunDataRepository.deepCopy(), useBaselineApproach)); runs.put(null, baseRunData); for (int i = 0; i < holdBackArCoTLModelProvider.numberOfActualInstances(); i++) { holdBackArCoTLModelProvider.setCurrentHoldBackIndex(i); var currentHoldBack = holdBackArCoTLModelProvider.getCurrentHoldBack(); - var currentRun = defineArDoCoWithPreComputedData(baseRunData, holdBackArCoTLModelProvider, useBaselineApproach); - currentRun.run(); - runs.put(currentHoldBack, new ArDoCoResult(currentRun.getDataRepository())); - } - return runs; - } - - private static ArDoCo definePipelineBase(Project project, File inputText, HoldBackArCoTLModelProvider holdElementsBackModelConnector, - boolean useInconsistencyBaseline) throws FileNotFoundException { - ArDoCo arDoCo = new ArDoCo(project.name().toLowerCase()); - var dataRepository = arDoCo.getDataRepository(); - String text = CommonUtilities.readInputText(inputText); - DataRepositoryHelper.putInputText(dataRepository, text); - var additionalConfigs = project.getAdditionalConfigurations(); - - arDoCo.addPipelineStep(TextPreprocessingAgent.get(additionalConfigs, dataRepository)); - - addMiddleSteps(holdElementsBackModelConnector, arDoCo, dataRepository, additionalConfigs); - - if (useInconsistencyBaseline) { - arDoCo.addPipelineStep(new InconsistencyBaseline(dataRepository)); - } else { - arDoCo.addPipelineStep(InconsistencyChecker.get(additionalConfigs, dataRepository)); + var currentRunData = runUnshared(goldStandardProject, holdBackArCoTLModelProvider, preRunDataRepository.deepCopy(), useBaselineApproach); + var result = new ArDoCoResult(currentRunData); + runs.put(currentHoldBack, result); } - return arDoCo; + return runs; } - private static ArDoCo defineArDoCoWithPreComputedData(ArDoCoResult precomputedResults, HoldBackArCoTLModelProvider holdElementsBackModelConnector, - boolean useInconsistencyBaseline) { - var projectName = precomputedResults.getProjectName(); - ArDoCo arDoCo = new ArDoCo(projectName); - var dataRepository = arDoCo.getDataRepository(); - - var additionalConfigs = ConfigurationHelper.loadAdditionalConfigs(null); - var optionalProject = Project.getFromName(projectName); - if (optionalProject.isPresent()) { - additionalConfigs = optionalProject.get().getAdditionalConfigurations(); - } - - var preprocessingData = new PreprocessingData(precomputedResults.getText()); - dataRepository.addData(PreprocessingData.ID, preprocessingData); - - addMiddleSteps(holdElementsBackModelConnector, arDoCo, dataRepository, additionalConfigs); - - if (useInconsistencyBaseline) { - arDoCo.addPipelineStep(new InconsistencyBaseline(dataRepository)); - } else { - arDoCo.addPipelineStep(InconsistencyChecker.get(additionalConfigs, dataRepository)); - } - return arDoCo; + /** + * Runs the part that is shared by all runs. + * + * @param goldStandardProject the current project + * @return the data repository that is produced + */ + protected DataRepository runShared(GoldStandardProject goldStandardProject) { + return new AnonymousRunner(goldStandardProject.getProjectName()) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) { + var pipelineSteps = new ArrayList(); + + var text = CommonUtilities.readInputText(inputText); + if (text.isBlank()) { + throw new IllegalArgumentException("Cannot deal with empty input text. Maybe there was an error reading the file."); + } + DataRepositoryHelper.putInputText(dataRepository, text); + pipelineSteps.add(TextPreprocessingAgent.get(additionalConfigs, dataRepository)); + pipelineSteps.add(TextExtraction.get(additionalConfigs, dataRepository)); + + return pipelineSteps; + } + }.runWithoutSaving(); } - private static void addMiddleSteps(HoldBackArCoTLModelProvider holdElementsBackModelConnector, ArDoCo arDoCo, DataRepository dataRepository, - SortedMap additionalConfigs) { - arDoCo.addPipelineStep(holdElementsBackModelConnector.get(additionalConfigs, dataRepository)); - arDoCo.addPipelineStep(TextExtraction.get(additionalConfigs, dataRepository)); - arDoCo.addPipelineStep(RecommendationGenerator.get(additionalConfigs, dataRepository)); - arDoCo.addPipelineStep(ConnectionGenerator.get(additionalConfigs, dataRepository)); + /** + * Runs the part that is specific to each run. + * + * @param goldStandardProject the current project + * @param holdElementsBackModelConnector the model connector with the held-back model element + * @param preRunDataRepository a deep copy of the data repository of the shared part + * @param useInconsistencyBaseline whether the inconsistency baseline is used or ArDoCo's inconsistency checker + * @return the data repository that is produced + */ + protected DataRepository runUnshared(GoldStandardProject goldStandardProject, HoldBackArCoTLModelProvider holdElementsBackModelConnector, + @DeepCopy DataRepository preRunDataRepository, boolean useInconsistencyBaseline) { + return new AnonymousRunner(goldStandardProject.getProjectName(), preRunDataRepository) { + @Override + public List initializePipelineSteps(DataRepository dataRepository) { + var pipelineSteps = new ArrayList(); + + pipelineSteps.add(holdElementsBackModelConnector.get(additionalConfigs, dataRepository)); + pipelineSteps.add(RecommendationGenerator.get(additionalConfigs, dataRepository)); + pipelineSteps.add(ConnectionGenerator.get(additionalConfigs, dataRepository)); + + if (useInconsistencyBaseline) { + pipelineSteps.add(new InconsistencyBaseline(dataRepository)); + } else { + pipelineSteps.add(InconsistencyChecker.get(additionalConfigs, dataRepository)); + } + + return pipelineSteps; + } + }.runWithoutSaving(); } } diff --git a/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/ArchitectureTest.java b/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/ArchitectureTest.java index 5a498e036..97371d3a8 100644 --- a/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/ArchitectureTest.java +++ b/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/ArchitectureTest.java @@ -1,12 +1,31 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.architecture; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; import static com.tngtech.archunit.library.Architectures.layeredArchitecture; +import java.io.Serializable; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.core.domain.JavaParameterizedType; +import com.tngtech.archunit.core.domain.JavaType; +import com.tngtech.archunit.core.domain.JavaTypeVariable; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.SimpleConditionEvent; @AnalyzeClasses(packages = "edu.kit.kastel.mcse.ardoco.core") public class ArchitectureTest { @@ -81,4 +100,129 @@ public class ArchitectureTest { .whereLayer("ModelExtractor") .mayOnlyBeAccessedByLayers("RecommendationGenerator", "ConnectionGenerator", "CodeTraceability", "InconsistencyDetection", "Pipeline", "Common", "Execution"); + + @ArchTest + public static final ArchRule transientRule = fields().that().haveModifier(JavaModifier.TRANSIENT).should(new ArchCondition<>("beAccessedIndirectly") { + @Override + public void check(JavaField javaField, ConditionEvents conditionEvents) { + javaField.getAccessesToSelf().forEach(fieldAccess -> { + var origin = fieldAccess.getOrigin(); + if (fieldAccess.getAccessType().equals(JavaFieldAccess.AccessType.GET)) { + if (origin.isMethod()) { + if (origin.getName().equalsIgnoreCase("get" + fieldAccess.getName())) { + satisfied(conditionEvents, javaField, null); + } else { + violated(conditionEvents, javaField, "Method accesses " + origin.getFullName() + " accesses transient field " + javaField + .getFullName() + ", but is not a getter or does not match the name pattern for getters 'get{$FieldName}'"); + } + } else { + violated(conditionEvents, javaField, "Transient field " + javaField + .getFullName() + " has to be accessed by a getter with name 'get{$fieldName}'"); + } + } else { + if (origin.isConstructor()) { + violated(conditionEvents, javaField, "Transient field " + javaField.getFullName() + " has to be set outside of the constructor"); + } else { + satisfied(conditionEvents, javaField, null); + } + } + }); + } + }).orShould(new ArchCondition<>("belongToClassWithCustomSerialization") { + @Override + public void check(JavaField javaField, ConditionEvents conditionEvents) { + if (javaField.getOwner().getMethods().stream().noneMatch(method -> method.getName().equalsIgnoreCase("writeObject"))) { + violated(conditionEvents, javaField, "Transient field " + javaField.getFullName() + " doesn't belong to a class with a custom serialization"); + } + } + }); + + @ArchTest + private static final ArchRule serializableRule = classes().that() + .areNotInterfaces() + .and() + .doNotHaveModifier(JavaModifier.ABSTRACT) + .and() + .areAssignableTo(Serializable.class) + .and() + .areNotEnums() + .should(new ArchCondition<>("beSerializable") { + @Override + public void check(JavaClass javaClass, ConditionEvents conditionEvents) { + if (javaClass.getMethods().stream().noneMatch(method -> method.getName().equalsIgnoreCase("writeObject"))) { + Predicate transientOrStatic = (JavaField javaField) -> new LinkedHashSet<>(javaField.getModifiers()).removeAll(List + .of(JavaModifier.STATIC, JavaModifier.TRANSIENT)); + var fields = javaClass.getFields(); + for (var field : fields) { + if (transientOrStatic.test(field)) + continue; + var erasure = field.getType().toErasure(); + if (isContainer.test(erasure)) { + getAllInvolvedRawTypesExceptSelf(field).stream().filter(erasureIsSerializableShallow.negate()).forEach(parameter -> { + violated(conditionEvents, javaClass, "Non-transient field " + field.getFullName() + " of serializable class " + javaClass + .getFullName() + " needs to be serializable or the class must have custom serialization, but has non-serializable parameter " + parameter + .getName()); + }); + } else { + if (erasureIsSerializableShallow.negate().test(erasure)) { + //Class has non-transient field that is not serializable + violated(conditionEvents, javaClass, "Non-transient field " + field.getFullName() + " of serializable class " + javaClass + .getFullName() + " needs to be serializable or the class must have custom serialization"); + } + } + } + } + } + }); + + private static final Predicate isContainer = (JavaClass javaClass) -> { + return javaClass.isArray() || javaClass.isAssignableTo(Collection.class) || javaClass.isAssignableTo(Map.class) || javaClass.isAssignableTo( + Iterable.class); + }; + + private static final Predicate erasureIsSerializableShallow = (JavaClass javaClass) -> { + return javaClass.isPrimitive() || javaClass.isAssignableTo(Serializable.class) || isContainer.test(javaClass); + }; + + private static LinkedHashSet isParameterizedGeneric(JavaField javaField) { + var javaType = javaField.getType(); + if (javaType instanceof JavaParameterizedType javaParameterizedType) { + javaParameterizedType.getActualTypeArguments(); + } + var set = new LinkedHashSet<>(javaType.getAllInvolvedRawTypes()); + set.remove(javaType); + return set; + } + + /** + * Returns all types of a field, except the (outer) type of the field itself. Generic type variables are not considered. + * + * @param javaField the field + * @return all types of a field + */ + private static LinkedHashSet getAllInvolvedRawTypesExceptSelf(JavaField javaField) { + var javaType = javaField.getType(); + LinkedHashSet set; + if (javaType instanceof JavaParameterizedType javaParameterizedType) { + set = javaParameterizedType.getActualTypeArguments() + .stream() + .filter(typeArgument -> !(typeArgument instanceof JavaTypeVariable)) + .map(JavaType::toErasure) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } else { + set = new LinkedHashSet<>(javaType.getAllInvolvedRawTypes()); + } + set.remove(javaType); + return set; + } + + private static void satisfied(ConditionEvents events, Object location, String message) { + var event = new SimpleConditionEvent(location, true, message); + events.add(event); + } + + private static void violated(ConditionEvents events, Object location, String message) { + var event = new SimpleConditionEvent(location, false, message); + events.add(event); + } } diff --git a/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/DeterministicArDoCoTest.java b/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/DeterministicArDoCoTest.java index 201c21e72..625580f4c 100644 --- a/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/DeterministicArDoCoTest.java +++ b/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/architecture/DeterministicArDoCoTest.java @@ -1,10 +1,7 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.architecture; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; import java.lang.annotation.Annotation; import java.util.HashMap; diff --git a/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/helper/LoadPCMTest.java b/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/helper/LoadPCMTest.java index da9703279..4df56dc47 100644 --- a/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/helper/LoadPCMTest.java +++ b/tests/tests-misc/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/eval/helper/LoadPCMTest.java @@ -1,8 +1,6 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.eval.helper; -import java.io.IOException; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; @@ -10,13 +8,14 @@ import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; import edu.kit.kastel.mcse.ardoco.core.models.connectors.generators.architecture.pcm.parser.PcmModel; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; class LoadPCMTest { @DisplayName("Load Components of Project") @ParameterizedTest(name = "Loading components of {0}") @EnumSource(value = Project.class) - void loadAllProjectComponentsTest(Project project) throws IOException { + void loadAllProjectComponentsTest(GoldStandardProject project) { var pcmModelFile = project.getModelFile(ArchitectureModelType.PCM); Assertions.assertNotNull(pcmModelFile); PcmModel model = new PcmModel(pcmModelFile); diff --git a/tests/tests-resources/.gitignore b/tests/tests-resources/.gitignore new file mode 100644 index 000000000..5ff6309b7 --- /dev/null +++ b/tests/tests-resources/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/tests/tests-resources/pom.xml b/tests/tests-resources/pom.xml new file mode 100644 index 000000000..1ab539020 --- /dev/null +++ b/tests/tests-resources/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + io.github.ardoco.core + tests + ${revision} + ../pom.xml + + tests-resources + + jar + + + + io.github.ardoco.core + common + ${revision} + + + io.github.ardoco.core + tests-base + ${revision} + compile + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + compile + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + compile + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + compile + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + + + test-jar + + + + + + + diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBoxDeserializer.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBoxDeserializer.java new file mode 100644 index 000000000..8c107d9f0 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBoxDeserializer.java @@ -0,0 +1,44 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +/** + * Deserializes a JSON bounding box node into a {@link BoundingBox}. For example the JSON node + *
    + * {@code
    + *  {
    + *      "x": 12,
    + *      "y": 30,
    + *      "w": 50,
    + *      "h": 60
    + * }
    + * }
    is converted to a bounding box with {@code minX = 12, minY + * = 30, maxX = 62, maxY = 90}. + */ +public class BoundingBoxDeserializer extends StdDeserializer { + public BoundingBoxDeserializer() { + this(BoundingBox.class); + } + + protected BoundingBoxDeserializer(Class vc) { + super(vc); + } + + @Override + public BoundingBox deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + var x = node.get("x").asInt(); + var y = node.get("y").asInt(); + var w = node.get("w").asInt(); + var h = node.get("h").asInt(); + + return new BoundingBox(x, y, x + w, y + h); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoxGS.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoxGS.java new file mode 100644 index 000000000..b61c07567 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoxGS.java @@ -0,0 +1,96 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.awt.*; +import java.io.File; +import java.io.Serializable; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramGoldStandardTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; + +/** + * Connector for the {@link Box} JSON representation. + */ +@Deterministic +public class BoxGS extends Box implements Serializable { + private final DiagramGS diagramGS; + private final BoxGS[] subBoxes; + private final TraceLinkGS[] tracelinks; + + /** + * Create a new box from the goldstandard. + * + * @param diagram the {@link Diagram} of the box. + * @param boundingBox the {@link BoundingBox} of the box. + * @param textBoxes all {@link TextBox} instances that are directly contained in this box (does not include sub boxes!) + * @param subBoxes all subboxes that are contained withing the bounding box of this box + * @param tracelinks all tracelinks associated with this box (does not include sub boxes!) + */ + @JsonCreator + public BoxGS(@JacksonInject DiagramGS diagram, @JsonProperty("boundingBox") BoundingBox boundingBox, @JsonProperty("textBoxes") TextBox[] textBoxes, + @JsonProperty("subBoxes") BoxGS[] subBoxes, @JsonProperty("tracelinks") TraceLinkGS[] tracelinks) { + super(diagram, calculateUUID(boundingBox.toCoordinates()), boundingBox.toCoordinates(), 1, Classification.UNKNOWN.getClassificationString(), Arrays + .asList(textBoxes), Color.BLACK); + this.diagramGS = diagram; + this.subBoxes = subBoxes; + this.tracelinks = tracelinks; + } + + /** + * {@return the set of diagram-sentence trace links associated with this box} The sentences are used to resolve the sentence numbers to actual sentences + * from the document. + * + * @param sentences the sentences from the text + */ + public Set getTraceLinks(List sentences) { + var list = Arrays.stream(tracelinks) + .filter(t -> getDiagram().getDiagramProject().getTextResourceName().contains(t.name())) + .flatMap(t -> t.toTraceLinks(this, sentences).stream()) + .toList(); + var set = new LinkedHashSet<>(list); + assert set.size() == list.size(); + return set; + } + + /** + * {@return the set of sub boxes contained by this box} Only direct children are returned. In comparison to {@link #getChildren()}, this entirely relies on + * the JSON structure of the gold standard file to determine the hierarchy of boxes. + */ + public BoxGS[] getSubBoxes() { + return subBoxes; + } + + /** + * {@return the diagram this box belongs to} + */ + @Override + public DiagramGS getDiagram() { + return this.diagramGS; + } + + @Override + public String toString() { + var allText = getTexts().stream().map(TextBox::getText).reduce((l, r) -> l + " | " + r).orElse(""); + var preText = getDiagram() + File.separator; + return String.format("BoxGS [%s %s]", super.toString(false), preText + allText.substring(0, Math.min(allText.length(), 20))); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramGS.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramGS.java new file mode 100644 index 000000000..8111f451d --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramGS.java @@ -0,0 +1,196 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.lang3.NotImplementedException; +import org.eclipse.collections.api.factory.Lists; +import org.jetbrains.annotations.Nullable; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramGoldStandardTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; +import edu.kit.kastel.mcse.ardoco.core.common.util.wordsim.WordSimUtils; +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +/** + * Implementation of the {@link Diagram} interface used for JSON deserialization. Instances are created from a + * {@link edu.kit.kastel.mcse.ardoco.tests.eval.GoldStandardDiagrams GoldStandardDiagrams}. + */ +@Deterministic +public class DiagramGS implements Diagram { + private String resourceName; + private List properBoxes = new ArrayList<>(); + private List properTextBoxes = new ArrayList<>(); + private DiagramProject project; + + /** + * JSON constructor for deserialization + * + * @param project the project this diagram belongs to + * @param resourceName the resource name of this diagram + * @param boxesNode the unprocessed JSON node representing the top-level diagram boxes + * @throws JsonProcessingException can occur during deserialization if the structure does not match + */ + @JsonCreator + public DiagramGS(@JacksonInject DiagramProject project, @JsonProperty("path") String resourceName, @JsonProperty("boxes") JsonNode boxesNode) + throws JsonProcessingException { + SimpleModule module = new SimpleModule(); + module.addDeserializer(BoundingBox.class, new BoundingBoxDeserializer()); + module.addDeserializer(TextBox.class, new TextBoxDeserializer()); + ObjectMapper objectMapper = createObjectMapper(); + objectMapper.registerModule(module); + objectMapper.setInjectableValues(new InjectableValues.Std().addValue(DiagramGS.class, this)); + var boxes = objectMapper.treeToValue(boxesNode, BoxGS[].class); + this.project = project; + + if (!project.getDiagramResourceNames().contains(resourceName)) { + var closest = project.getDiagramResourceNames() + .stream() + .max(Comparator.comparingDouble(a -> new WordSimUtils().getSimilarity(a, resourceName))) + .orElse("NONE"); + throw new IllegalArgumentException(String.format("The resource name \"%s\" doesn't match any known resource of \"%s\". Did you mean \"%s\"?", + resourceName, project.getProjectName(), closest)); + } + + this.resourceName = resourceName; + addBoxes(boxes); + } + + DiagramGS(DiagramProject project, String path, BoxGS[] boxes) { + this.project = project; + this.resourceName = path; + addBoxes(boxes); + } + + private void addBoxes(BoxGS[] boxes) { + for (BoxGS boxGS : boxes) { + addBox(boxGS); + addBoxes(boxGS.getSubBoxes()); + } + } + + @Override + public String getResourceName() { + return resourceName; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof DiagramGS other) { + return resourceName.equals(other.resourceName); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(resourceName); + } + + @Override + public File getLocation() { + return project.getDiagramsGoldStandardFile(); + } + + @Override + public void addBox(Box box) { + properBoxes.add(box); + } + + @Override + public boolean removeBox(Box box) { + return properBoxes.remove(box); + } + + @Override + public List getBoxes() { + return List.copyOf(properBoxes); + } + + @Override + public void addTextBox(TextBox textBox) { + properTextBoxes.add(textBox); + } + + @Override + public boolean removeTextBox(TextBox textBox) { + return properTextBoxes.remove(textBox); + } + + @Override + public List getTextBoxes() { + return List.copyOf(properTextBoxes); + } + + @Override + public void addConnector(Connector connector) { + throw new NotImplementedException(); + } + + @Override + public boolean removeConnector(Connector connector) { + throw new NotImplementedException(); + } + + @Override + public List getConnectors() { + throw new NotImplementedException(); + } + + /** + * {@return the list of diagram-sentence trace links associated with this diagram} The sentences are used to resolve the sentence numbers to actual + * sentences from the document. + * + * @param sentences the sentences from the text + */ + public List getTraceLinks(List sentences) { + var traceLinks = Lists.mutable.empty(); + for (Box box : properBoxes) { + if (box instanceof BoxGS boxGS) { + var boxTLs = boxGS.getTraceLinks(sentences).stream().toList(); + traceLinks.addAll(boxTLs); + } + } + assert traceLinks.size() == traceLinks.distinct().size(); //Otherwise there are duplicates in the goldstandard + return traceLinks; + } + + /** + * Retrieves a distinct list of diagram text trace links associated with the diagram + * + * @param textGoldstandard Partial path to the goldstandard text file + * @return List of tracelinks + */ + public List getTraceLinks(List sentences, @Nullable String textGoldstandard) { + if (textGoldstandard == null) + return getTraceLinks(sentences); + return getTraceLinks(sentences).stream().filter(t -> textGoldstandard.contains(t.getGoldStandard())).distinct().toList(); + } + + public DiagramProject getDiagramProject() { + return project; + } + + @Override + public String toString() { + return getShortResourceName(); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramsGS.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramsGS.java new file mode 100644 index 000000000..c387e13af --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramsGS.java @@ -0,0 +1,13 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Encapsulates multiple {@link DiagramGS} instances from the gold standard. + */ +public class DiagramsGS { + @JsonProperty("$schema") + public String schema; + public DiagramGS[] diagrams; +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBoxDeserializer.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBoxDeserializer.java new file mode 100644 index 000000000..9c9092d5c --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TextBoxDeserializer.java @@ -0,0 +1,46 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +/** + * Deserializes a JSON text box node into a {@link TextBox}. For example the JSON node + *
    + * {@code
    + *  [
    + *      {
    + *          "text": "User Management",
    + *          "boundingBox": {
    + *              "x": 12,
    + *              "y": 30,
    + *              "w": 50,
    + *              "h": 60
    + * }
    + * }
    + * ]
    + * }
    is converted to a text box with {@code text = "User Management"} and its bounding box with {@code minX = 12, minY + * = 30, maxX = 62, maxY = 90}. + */ +public class TextBoxDeserializer extends StdDeserializer { + public TextBoxDeserializer() { + this(TextBox.class); + } + + protected TextBoxDeserializer(Class vc) { + super(vc); + } + + @Override + public TextBox deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + var text = node.get("text").asText(); + var boundingBox = deserializationContext.readValue(node.get("boundingBox").traverse(jsonParser.getCodec()), BoundingBox.class); + return new TextBox(boundingBox.minX(), boundingBox.minY(), boundingBox.width(), boundingBox.height(), 1, text); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TraceLinkGS.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TraceLinkGS.java new file mode 100644 index 000000000..4e5c50a7a --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TraceLinkGS.java @@ -0,0 +1,56 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramGoldStandardTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.TraceType; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.architecture.Deterministic; + +/** + * Encapsulates the diagram-sentence trace links to its parent diagram element. Used for deserialization purposes. + * + * @param name the name of the text file the sentence numbers refer to + * @param sentenceIds contains the sentence number of each trace link + * @param typedTracelinks a set of trace links with additional information + */ +@Deterministic +public record TraceLinkGS(@JsonProperty("name") String name, @JsonProperty("sentences") int[] sentenceIds, + @JsonProperty("typedTracelinks") TypedTraceLinkGS[] typedTracelinks) implements Serializable { + /** + * {@return the set of diagram-sentence trace links associated with this box} The sentences are used to resolve the sentence numbers to actual sentences + * from the document. + * + * @param boxGS the box this trace link points to + * @param sentences the sentences from the text + */ + public Set toTraceLinks(BoxGS boxGS, List sentences) { + //From sentences, set default trace type ENTITY + var list = toTraceLinks(boxGS, sentences, sentenceIds, TraceType.ENTITY); + //From typed trace links + var typedList = List.of(); + if (typedTracelinks != null) + typedList = Arrays.stream(typedTracelinks).flatMap(typed -> toTraceLinks(boxGS, sentences, typed.sentences(), typed.traceType()).stream()).toList(); + list.addAll(typedList); + + var set = new LinkedHashSet<>(list); + assert set.size() == list.size(); //Otherwise there are duplicates in the goldstandard + return set; + } + + private List toTraceLinks(BoxGS boxGS, List sentences, int[] sentenceIds, TraceType traceType) { + var project = boxGS.getDiagram().getDiagramProject(); + return Arrays.stream(sentenceIds) + .mapToObj(i -> new DiagramGoldStandardTraceLink(boxGS, sentences.get(i - 1), project.name(), name, traceType)) + .collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TypedTraceLinkGS.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TypedTraceLinkGS.java new file mode 100644 index 000000000..cd24e4c0b --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/TypedTraceLinkGS.java @@ -0,0 +1,17 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.TraceType; + +/** + * Encapsulates a diagram-sentence trace link of a specific type. + * + * @param sentences + * @param traceType + */ +public record TypedTraceLinkGS(@JsonProperty("sentences") int[] sentences, @JsonProperty("traceType") TraceType traceType) implements Serializable { +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramGoldStandardTraceLink.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramGoldStandardTraceLink.java new file mode 100644 index 000000000..04156d69d --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramGoldStandardTraceLink.java @@ -0,0 +1,91 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; + +import java.util.Comparator; +import java.util.Objects; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; + +public class DiagramGoldStandardTraceLink extends DiagramTextTraceLink { + private final String goldStandard; + private final TraceType traceType; + + /** + * Creates a tracelink between a diagram element and a sentence number + * + * @param diagramElement diagram element + * @param sentence sentence + * @param projectName project name + * @param goldStandard path to the textual gold standard file + */ + public DiagramGoldStandardTraceLink(DiagramElement diagramElement, Sentence sentence, String projectName, String goldStandard) { + this(diagramElement, sentence, projectName, goldStandard, TraceType.ENTITY); + } + + /** + * Creates a tracelink between a diagram element and a sentence number + * + * @param diagramElement diagram element + * @param sentence sentence + * @param projectName project name + * @param goldStandard path to the textual gold standard file + * @param traceType type of the trace + */ + public DiagramGoldStandardTraceLink(DiagramElement diagramElement, Sentence sentence, String projectName, String goldStandard, TraceType traceType) { + super(diagramElement, sentence, projectName); + this.goldStandard = goldStandard; + this.traceType = traceType; + } + + /** + * {@return the path to the goldstandard text file} + */ + public String getGoldStandard() { + return goldStandard; + } + + /** + * {@return the type of this trace} + */ + public TraceType getTraceType() { + return traceType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof DiagramGoldStandardTraceLink other) { + return Objects.equals(getGoldStandard(), other.getGoldStandard()) && Objects.equals(getDiagramElement(), other.getDiagramElement()) && Objects + .equals(getSentenceNo(), other.getSentenceNo()) && Objects.equals(getTraceType(), other.getTraceType()); + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return Objects.hash(getSentenceNo(), getDiagramElement(), getGoldStandard(), getTraceType()); + } + + @Override + public int compareTo(DiagramTextTraceLink o) { + if (equals(o)) + return 0; + if (o instanceof DiagramWordTraceLink) + return -1; + if (o instanceof DiagramGoldStandardTraceLink other) { + return Comparator.comparing(DiagramGoldStandardTraceLink::getGoldStandard) + .thenComparing(DiagramGoldStandardTraceLink::getDiagramElement) + .thenComparingInt(DiagramGoldStandardTraceLink::getSentenceNo) + .thenComparing(DiagramGoldStandardTraceLink::getTraceType) + .compare(this, other); + } + return super.compareTo(o); + } + + @Override + public String toString() { + return String.format("%s-[%s]", super.toString(), getTraceType().name()); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramTextTraceLink.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramTextTraceLink.java new file mode 100644 index 000000000..034d4d2a1 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramTextTraceLink.java @@ -0,0 +1,92 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; + +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.Objects; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.common.util.SimilarityComparable; +import edu.kit.kastel.mcse.ardoco.core.data.GlobalConfiguration; + +public class DiagramTextTraceLink implements SimilarityComparable, Comparable, Serializable { + protected final DiagramElement diagramElement; + protected final Sentence sentence; + protected final String projectName; + + /** + * Creates a tracelink between a diagram element and a sentence + * + * @param diagramElement diagram element + * @param sentence sentence + * @param projectName project name + */ + public DiagramTextTraceLink(DiagramElement diagramElement, Sentence sentence, String projectName) { + this.diagramElement = diagramElement; + this.sentence = sentence; + this.projectName = projectName; + } + + public DiagramElement getDiagramElement() { + return diagramElement; + } + + /** + * Gets the sentence number, indexing starts at 1. + * + * @return sentence number + */ + public int getSentenceNo() { + return sentence.getSentenceNumberForOutput(); + } + + @Override + public String toString() { + return toString(true); + } + + public String toString(boolean withSentence) { + if (withSentence) { + return MessageFormat.format("[{0}]-[{1}]-[{2}]", getDiagramElement(), getSentence().getSentenceNumberForOutput(), getSentence().getText()); + } + return MessageFormat.format("[{0}]-[{1}]", getDiagramElement(), getSentence().getSentenceNumberForOutput()); + } + + public Sentence getSentence() { + return this.sentence; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + else if (obj instanceof DiagramTextTraceLink other) { + return Objects.equals(getSentenceNo(), other.getSentenceNo()) && Objects.equals(getDiagramElement(), other.getDiagramElement()); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(getSentenceNo(), getDiagramElement()); + } + + @Override + public int compareTo(DiagramTextTraceLink o) { + if (equals(o)) + return 0; + var comp = getDiagramElement().compareTo(o.getDiagramElement()); + if (comp == 0) + return getSentenceNo() - o.getSentenceNo(); + return comp; + } + + @Override + public boolean similar(GlobalConfiguration globalConfiguration, DiagramTextTraceLink obj) { + if (equals(obj)) + return true; + return getDiagramElement().getBoundingBox().similar(globalConfiguration, obj.getDiagramElement().getBoundingBox()) && Objects.equals(getSentenceNo(), + obj.getSentenceNo()); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramWordTraceLink.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramWordTraceLink.java new file mode 100644 index 000000000..3e13d5d23 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramWordTraceLink.java @@ -0,0 +1,116 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramElement; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Claimant; + +/** + * Represents a tracelink between a {@link DiagramElement} and a {@link Word}. + */ +public class DiagramWordTraceLink extends DiagramTextTraceLink { + public static final Comparator CONFIDENCE_COMPARATOR = Comparator.comparingDouble(DiagramWordTraceLink::getConfidence); + + private final Word word; + private final double confidence; + private final Serializable origin; + private final TreeSet relatedWordLinks = new TreeSet<>(); + private final TreeSet relatedGSLinks = new TreeSet<>(); + + /** + * Creates a tracelink between a diagram element and a sentence number of a word + * + * @param diagramElement diagram element + * @param word word + * @param projectName project name + * @param confidence confidence + * @param origin claimant this link was derived from + */ + public DiagramWordTraceLink(DiagramElement diagramElement, Word word, String projectName, double confidence, @Nullable Claimant origin) { + super(diagramElement, word.getSentence(), projectName); + + this.word = word; + this.confidence = confidence; + this.origin = origin; + } + + public Word getWord() { + return this.word; + } + + public double getConfidence() { + return this.confidence; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + else if (obj instanceof DiagramWordTraceLink other) { + return Objects.equals(getDiagramElement(), other.getDiagramElement()) && getWord().getPosition() == other.getWord().getPosition() && Objects.equals( + getConfidence(), other.getConfidence()); + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return Objects.hash(getDiagramElement(), getWord(), getConfidence()); + } + + @Override + public int compareTo(DiagramTextTraceLink o) { + if (equals(o)) + return 0; + var supComp = super.compareTo(o); + if (o instanceof DiagramWordTraceLink other && supComp == 0) { + var comp = Integer.compare(getWord().getPosition(), other.getWord().getPosition()); + if (comp == 0) { + return Double.compare(getConfidence(), other.getConfidence()); + } + return comp; + } + return supComp; + } + + public void addRelated(Collection related) { + var list = new ArrayList<>(related); + for (var link : list) { + if (link == this) + continue; + if (link instanceof DiagramWordTraceLink wLink) { + relatedWordLinks.add(wLink); + } else if (link instanceof DiagramGoldStandardTraceLink gsLink) { + relatedGSLinks.add(gsLink); + } + } + } + + public TreeSet getRelatedGSLinks() { + return relatedGSLinks; + } + + public TreeSet getRelatedWordLinks() { + return relatedWordLinks; + } + + @Override + public String toString() { + var relatedTypes = ""; + if (!relatedGSLinks.isEmpty()) { + relatedTypes = relatedGSLinks.stream().map(g -> "[" + g.getTraceType().name() + "]").collect(Collectors.joining("-")) + "-"; + } + return String.format("%s-[%s]-[%s]-[%.3f]-%s", super.toString(false), getWord().getText(), getWord().getPhrase().getText(), getConfidence(), + relatedTypes); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceType.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceType.java new file mode 100644 index 000000000..5c455fed3 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/TraceType.java @@ -0,0 +1,31 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; + +import java.io.Serializable; + +/** + * The {@link TraceType} refers to the type associated with a trace link. It can be used to annotate trace links in the gold standard with additional + * information. Actual negatives can be marked using {@link #COMMON_NOUN}, {@link #SHARED_STEM} and {@link #OTHER_ENTITY}. The gold standard is not complete for + * actual negatives, however the provided information can be used to determine potential causes of false positives. + */ +public enum TraceType implements Serializable { + ENTITY(true),//Both endpoints point to the same entity + COMMON_NOUN(false),//Created due to a common noun usage (e.g. Entity "test" and word "test" in the text) + SHARED_STEM(false),//Created due to a shared word stem (e.g. Entity "testing" and word "testing" in the text) + ENTITY_COREFERENCE(true),//Both endpoints point to the same entity, but the textual endpoint is a coreference + OTHER_ENTITY(false),//Created due to another (similarly-named) entity + UNCERTAIN(false);//Marker for discussion + + private final boolean actualPositive; + + TraceType(boolean actualPositive) { + this.actualPositive = actualPositive; + } + + /** + * {@return whether a trace link of this type is an actual positive} + */ + public boolean isActualPositive() { + return this.actualPositive; + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/DiagramProject.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/DiagramProject.java new file mode 100644 index 000000000..291034d6a --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/DiagramProject.java @@ -0,0 +1,309 @@ +/* Licensed under MIT 2021-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import static edu.kit.kastel.mcse.ardoco.core.common.JsonHandling.createObjectMapper; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; + +import com.fasterxml.jackson.databind.InjectableValues; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramGS; +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramsGS; +import edu.kit.kastel.mcse.ardoco.core.api.models.ArchitectureModelType; +import edu.kit.kastel.mcse.ardoco.core.api.models.Metamodel; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramGoldStandardTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.TraceType; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.ProjectHelper; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; + +/** + * This enum captures the different case studies that are used for evaluation in the integration tests. + */ +public enum DiagramProject implements GoldStandardDiagramsWithTLR { + MEDIASTORE(// + Project.MEDIASTORE, // + "/benchmark/mediastore/goldstandards/goldstandard_sad_id_2016.json", // + new ExpectedResults(.87, .93, .9, .99, .89, .99), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.81, .93, .87, .98, .86, .98), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(.89, .72, .68, .94, .72, .96), //Expected MME results (Mock) + new ExpectedResults(.89, .72, .68, .94, .72, .96), //Expected MME results (No-Mock) + new ExpectedResults(.76, .79, .77, .97, .76, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(.76, .79, .77, .97, .76, .99), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/mediastore/diagrams_2016/ArchitectureWithCache.png") // + ), // + TEASTORE( // + Project.TEASTORE, // + "/benchmark/teastore/goldstandards/goldstandard_sad_id_2018.json", // + new ExpectedResults(.95, .74, .83, .97, .82, 1), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.65, .74, .69, .93, .65, .95), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(1, .70, .80, .96, .83, 1), //Expected MME results (Mock) + new ExpectedResults(.96, .70, .78, .95, .80, 1), //Expected MME results (No-Mock) + new ExpectedResults(1, .74, .85, .98, .85, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(1, .74, .85, .98, .85, 1), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/teastore/diagrams_2018/Overview.jpg") // + ), // + TEAMMATES( // + Project.TEAMMATES, // + "/benchmark/teammates/goldstandards/goldstandard_sad_id_2023.json", // + new ExpectedResults(.60, .67, .63, .98, .62, .99), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.37, .25, .30, .97, .29, .99), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(.61, .70, .53, .96, .58, .97), //Expected MME results (Mock) + new ExpectedResults(.61, .70, .53, .96, .58, .97), //Expected MME results (No-Mock) + new ExpectedResults(.73, .88, .80, .98, .79, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(.73, .88, .80, .98, .79, .99), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/teammates/diagrams_2023/highlevelArchitecture.png", "/benchmark/teammates/diagrams_2023/packageDiagram.png") // + ), // + BIGBLUEBUTTON( // + Project.BIGBLUEBUTTON, // + "/benchmark/bigbluebutton/goldstandards/goldstandard_sad_id_2021.json", // + new ExpectedResults(.79, .72, .75, .97, .74, .99), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.76, .61, .67, .97, .66, .99), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(.93, .38, .39, .96, .55, .99), //Expected MME results (Mock) + new ExpectedResults(.93, .38, .39, .96, .55, .99), //Expected MME results (No-Mock) + new ExpectedResults(.87, .82, .85, .98, .84, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(.87, .82, .85, .98, .84, .99), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/bigbluebutton/diagrams_2021/bbb-arch-overview.png") // + ), // + TEASTORE_HISTORICAL( // + Project.TEASTORE_HISTORICAL, // + "/benchmark/teastore/goldstandards/goldstandard_sad_id_2018.json", // + new ExpectedResults(1, .92, .96, .99, .95, 1), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.77, .92, .84, .96, .82, .96), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(1, .91, .91, .99, .95, 1), //Expected MME results (Mock) + new ExpectedResults(.84, .91, .87, .98, .86, .99), //Expected MME results (No-Mock) + new ExpectedResults(1, .93, .96, .99, .96, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(1, .93, .96, .99, .96, 1), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/teastore/diagrams_2018/Overview.jpg") // + ), // + TEAMMATES_HISTORICAL( // + Project.TEAMMATES_HISTORICAL, // + "/benchmark/teammates/goldstandards/goldstandard_sad_id_2015.json", // + new ExpectedResults(.62, .71, .66, .98, .66, .99), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.51, .39, .44, .98, .43, .99), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(.31, .69, .42, .93, .44, .94), //Expected MME results (Mock) + new ExpectedResults(.45, .69, .49, .95, .53, .96), //Expected MME results (No-Mock) + new ExpectedResults(.72, .76, .74, .98, .73, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(.68, .76, .72, .98, .71, .99), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/teammates/diagrams_2015/highlevelArchitecture.png", "/benchmark/teammates/diagrams_2015/packageDiagram.png") // + ), // + BIGBLUEBUTTON_HISTORICAL( // + Project.BIGBLUEBUTTON_HISTORICAL, // + "/benchmark/bigbluebutton/goldstandards/goldstandard_sad_id_2015.json", // + new ExpectedResults(.73, .91, .81, .98, .8, .98), //Expected Diagram-Sentence TLR results (Mock) + new ExpectedResults(.69, .77, .73, .97, .71, .98), //Expected Diagram-Sentence TLR results (No-Mock) + new ExpectedResults(.07, .20, .10, .73, -0.01, .79), //Expected MME results (Mock) + new ExpectedResults(.07, .20, .10, .73, -0.01, .79), //Expected MME results (No-Mock) + new ExpectedResults(.77, .61, .68, .97, .68, 1), //Expected SAD-SAM TLR results (Mock), + new ExpectedResults(.77, .61, .68, .97, .68, .99), //Expected SAD-SAM TLR results (No-Mock), + List.of("/benchmark/bigbluebutton/diagrams_2015/bbb-arch-overview.png") // + ); + + private final Project baseProject; + private final String goldStandardDiagrams; + + private final ExpectedResults expectedDiagramSentenceTlrResultsMock; + private final ExpectedResults expectedDiagramSentenceTlrResultsNoMock; + private final ExpectedResults expectedMMEResultsMock; + private final ExpectedResults expectedMMEResultsNoMock; + private final ExpectedResults expectedSadSamTlrResultsMock; + private final ExpectedResults expectedSadSamTlrResultsNoMock; + + private final SortedSet diagramResourceNames; + + private final ArchitectureModelType architectureModelType; + private final SortedSet resourceNames; + + /** + * Sole constructor for a project with diagrams. + * + * @param project the base {@link Project} that is extended + * @param goldStandardDiagrams the name of the JSON file containing the combined gold standard + * @param expectedDiagramSentenceTlrResultsMock the {@link ExpectedResults} for the Diagram-Sentence TLR using gold standard diagrams + * @param expectedDiagramSentenceTlrResultsNoMock the {@link ExpectedResults} for the Diagram-Sentence TLR + * @param expectedMMEResultsMock the {@link ExpectedResults} for the MME inconsistency detection using gold standard diagrams + * @param expectedMMEResultsNoMock the {@link ExpectedResults} for the MME inconsistency detection using the diagram recognition + * @param expectedSadSamTlrResultsMock the {@link ExpectedResults} for the SAD SAM TLR using gold standard diagrams + * @param expectedSadSamTlrResultsNoMock the {@link ExpectedResults} for the SAD SAM TLR + * @param diagramResourceNames a set of diagram-related resources + */ + DiagramProject(Project project, String goldStandardDiagrams, ExpectedResults expectedDiagramSentenceTlrResultsMock, + ExpectedResults expectedDiagramSentenceTlrResultsNoMock, ExpectedResults expectedMMEResultsMock, ExpectedResults expectedMMEResultsNoMock, + ExpectedResults expectedSadSamTlrResultsMock, ExpectedResults expectedSadSamTlrResultsNoMock, List diagramResourceNames) { + //We need to keep the paths as well, because the actual files are just temporary at this point due to jar packaging + this.goldStandardDiagrams = goldStandardDiagrams; + this.baseProject = project; + this.expectedDiagramSentenceTlrResultsMock = expectedDiagramSentenceTlrResultsMock; + this.expectedDiagramSentenceTlrResultsNoMock = expectedDiagramSentenceTlrResultsNoMock; + this.expectedMMEResultsMock = expectedMMEResultsMock; + this.expectedMMEResultsNoMock = expectedMMEResultsNoMock; + this.expectedSadSamTlrResultsMock = expectedSadSamTlrResultsMock; + this.expectedSadSamTlrResultsNoMock = expectedSadSamTlrResultsNoMock; + this.diagramResourceNames = new TreeSet<>(diagramResourceNames); + this.architectureModelType = setupArchitectureModelType(); + var set = new TreeSet<>(project.getResourceNames()); + set.add(goldStandardDiagrams); + set.addAll(diagramResourceNames); + resourceNames = set; + } + + @Override + public String getProjectName() { + return this.name(); + } + + @Override + public SortedSet getResourceNames() { + return new TreeSet<>(resourceNames); + } + + /** + * TODO This should probably be part of {@link ArchitectureModelType} + */ + public Metamodel getMetamodel() { + return switch (architectureModelType) { + case PCM, UML -> Metamodel.ARCHITECTURE; + }; + } + + public ArchitectureModelType getArchitectureModelType() { + return architectureModelType; + } + + private ArchitectureModelType setupArchitectureModelType() { + if (baseProject.getModelResourceName().contains("/pcm/")) { + return ArchitectureModelType.PCM; + } else if (baseProject.getModelResourceName().contains("/uml/")) { + return ArchitectureModelType.UML; + } else { + throw new IllegalArgumentException( + "The model file could not be resolved to a known ArchitectureModelType. Please comply with the mandatory folder structure!"); + } + } + + /** + * Returns an {@link Optional} containing the project that has a name that equals the given name, ignoring case. + * + * @param name the name of the project + * @return the Optional containing the project with the given name or is empty if no such is found. + */ + public static Optional getFromName(String name) { + for (DiagramProject project : DiagramProject.values()) { + if (project.name().equalsIgnoreCase(name)) { + return Optional.of(project); + } + } + return Optional.empty(); + } + + /** + * {@return the list of historical diagram projects} + */ + public static List getHistoricalProjects() { + return filterForHistoricalProjects(List.of(values())); + } + + /** + * {@return the list of non-historical diagram projects} + */ + public static List getNonHistoricalProjects() { + return filterForNonHistoricalProjects(List.of(values())); + } + + private static > List filterForHistoricalProjects(Collection unfilteredProjects) { + return filterForProjects(unfilteredProjects, p -> p.name().endsWith("HISTORICAL")); + } + + private static > List filterForNonHistoricalProjects(Collection unfilteredProjects) { + return filterForProjects(unfilteredProjects, p -> !p.name().endsWith("HISTORICAL")); + } + + private static > List filterForProjects(Collection unfilteredProjects, Predicate filter) { + return unfilteredProjects.stream().filter(filter).toList(); + } + + @Override + public String getDiagramsGoldStandardResourceName() { + return goldStandardDiagrams; + } + + @Override + public File getDiagramsGoldStandardFile() { + return ProjectHelper.loadFileFromResources(goldStandardDiagrams); + } + + @Override + public ExpectedResults getExpectedDiagramSentenceTlrResultsWithMock() { + return expectedDiagramSentenceTlrResultsMock; + } + + @Override + public ExpectedResults getExpectedDiagramSentenceTlrResults() { + return expectedDiagramSentenceTlrResultsNoMock; + } + + @Override + public ExpectedResults getExpectedMMEResults() { + return expectedMMEResultsNoMock; + } + + @Override + public ExpectedResults getExpectedMMEResultsWithMock() { + return expectedMMEResultsMock; + } + + @Override + public ExpectedResults getExpectedSadSamResults() { + return expectedSadSamTlrResultsNoMock; + } + + @Override + public ExpectedResults getExpectedSadSamResultsWithMock() { + return expectedSadSamTlrResultsMock; + } + + @Override + public Set getDiagramTraceLinks(List sentences) { + return getDiagramTraceLinks(sentences, baseProject.getTextResourceName()); + } + + @Override + public Map> getDiagramTraceLinksAsMap(List sentences) { + var traceLinks = getDiagramTraceLinks(sentences); + return traceLinks.stream().collect(Collectors.groupingBy(DiagramGoldStandardTraceLink::getTraceType)); + } + + private Set getDiagramTraceLinks(List sentences, @Nullable String textGoldstandard) { + return getDiagramsGoldStandard().stream().flatMap(d -> d.getTraceLinks(sentences, textGoldstandard).stream()).collect(Collectors.toSet()); + } + + @Override + public Set getDiagramsGoldStandard() { + try { + var objectMapper = createObjectMapper(); + var file = getDiagramsGoldStandardFile(); + objectMapper.setInjectableValues(new InjectableValues.Std().addValue(DiagramProject.class, this)); + return new LinkedHashSet<>(List.of(objectMapper.readValue(file, DiagramsGS.class).diagrams)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Override + public SortedSet getDiagramResourceNames() { + return new TreeSet<>(diagramResourceNames); + } + + @Override + public List> getDiagramData() { + return getDiagramResourceNames().stream().map(rn -> new Pair<>(rn, ProjectHelper.loadFileFromResources(rn))).toList(); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagramTLR.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagramTLR.java new file mode 100644 index 000000000..e7ee4e506 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagramTLR.java @@ -0,0 +1,69 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramGoldStandardTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramTextTraceLink; +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.TraceType; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; + +/** + * This interface represents an interface which contains a set of + * {@link DiagramTextTraceLink Diagram-Sentence Trace Links} for the underlying {@link GoldStandardProject}. + */ +public interface GoldStandardDiagramTLR extends GoldStandardProject { + /** + * {@return the set of diagram-sentence trace links} The sentence numbers from the gold standard are resolved to full sentences using the provided list of + * sentences. + * + * @param sentences sentences of the text + */ + Set getDiagramTraceLinks(List sentences); + + /** + * {@return a map of diagram-sentence trace links to their corresponding trace type} The sentence numbers from the gold standard are resolved to full + * sentences using the provided list of sentences. + * + * @param sentences sentences of the text + */ + Map> getDiagramTraceLinksAsMap(List sentences); + + /** + * Returns the expected results from the diagram-sentence traceability link recovery using gold standard diagrams + * + * @return the expectedDiagramTraceLinkResults + */ + ExpectedResults getExpectedDiagramSentenceTlrResultsWithMock(); + + /** + * Returns the expected results from the diagram-sentence traceability link recovery + * + * @return the expectedDiagramTraceLinkResults + */ + ExpectedResults getExpectedDiagramSentenceTlrResults(); + + /** + * {@return the expected results for MME detection} + */ + ExpectedResults getExpectedMMEResults(); + + /** + * {@return the expected results for MME detection using the gold standard diagrams} + */ + ExpectedResults getExpectedMMEResultsWithMock(); + + /** + * {@return the expected SAD-SAM trace link results} + */ + ExpectedResults getExpectedSadSamResults(); + + /** + * {@return the expected SAD-SAM trace link results using the gold standard diagrams} + */ + ExpectedResults getExpectedSadSamResultsWithMock(); +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagrams.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagrams.java new file mode 100644 index 000000000..350e067b9 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagrams.java @@ -0,0 +1,43 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import java.io.File; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.DiagramGS; +import edu.kit.kastel.mcse.ardoco.core.common.tuple.Pair; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; + +/** + * This interface represents a gold standard, which contains a set of {@link edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram Diagrams} for the + * underlying {@link GoldStandardProject}. + */ +public interface GoldStandardDiagrams extends GoldStandardProject { + /** + * {@return the resource name that represents the diagrams gold standard for this project} + */ + String getDiagramsGoldStandardResourceName(); + + /** + * {@return the File that contains the gold standard for this project} + */ + File getDiagramsGoldStandardFile(); + + /** + * {@return the set of manually extracted diagrams from the gold standard} + */ + Set getDiagramsGoldStandard(); + + /** + * {@return the set of diagram-related resources} For example, the list contains the names of the diagram image resources. + */ + SortedSet getDiagramResourceNames(); + + /** + * {@return the list of diagram-related resources as name and file pair} For example, the list contains a pair with the name and file of each diagram image + * resources. + */ + List> getDiagramData(); +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagramsWithTLR.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagramsWithTLR.java new file mode 100644 index 000000000..8c85b305f --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/GoldStandardDiagramsWithTLR.java @@ -0,0 +1,13 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.DiagramTextTraceLink; + +/** + * This interface represents a combined gold standard. The gold standard contains the + * {@link edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.Diagram Diagrams} and + * {@link DiagramTextTraceLink Diagram-Sentence Trace Links} associated with the underlying + * {@link edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject}. + */ +public interface GoldStandardDiagramsWithTLR extends GoldStandardDiagrams, GoldStandardDiagramTLR { +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/StageTest.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/StageTest.java new file mode 100644 index 000000000..93c028332 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/StageTest.java @@ -0,0 +1,386 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +import org.eclipse.collections.impl.factory.SortedMaps; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.kit.kastel.mcse.ardoco.core.configuration.ConfigurationUtility; +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.data.DeepCopy; +import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractExecutionStage; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.Informant; +import edu.kit.kastel.mcse.ardoco.core.pipeline.agent.PipelineAgent; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; + +/** + * This test base class can be used for testing stages. Conceptually, a stage test is divided into the pre-test runner and the test runner. The pre-test runner + * is executed once per project. The pre-test runner is defined and executed in {@link #runPreTestRunner} by the implementing subclasses. It should contain all + * stages that are considered pre-requisites to the stage this test is supposed to test. The resulting data repository is cloned (deep copy) and used as a basis + * for the test runner, which needs to be implemented in {@link #runTestRunner}. Each execution of the stage needs to produce an instance of the provided + * record. Thus, subclasses should implement {@link #runComparable} to provide such a record.

    This class also provides functionality to automatically + * test if the results of a stage are invariant to a specific amount of repetitions. The repetition test occur on a stage, agent and informant level, where each + * is individually tested for invariance.

    The class provides the two environment variables {@link #ENV_DEBUG} and {@link #ENV_DEBUG_KEEP_CACHE} to + * configure the caching behaviour. By default, no persistent caching occurs. {@link #ENV_DEBUG} enables CLI prompts at the beginning of the stage test, which + * ask whether results should be cached (for debugging purposes, comparison to prior runs et cetera). {@link #ENV_DEBUG_KEEP_CACHE} prevents the pre-run cache + * from being wiped prior before all tests are executed, which means that the pre-requisites do not have to be run again. This is useful, if a stage has a large + * number of pre-requisite stages, which do not change frequently, but require a lot of time to execute. + * + * @param The stage tested by this class + * @param The type of project that should be used + * @param The record produced by the execution of the test runners. The record must implement {@link V#equals(Object)} independent of the identity of its + * constituents. This is necessary, because the equality of the record is considered when checking for non-deterministic behaviour. The record should + * summarize the relevant results of the stage + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public abstract class StageTest implements Serializable { + /** + * Enables a prompt that sets the {@link #CACHING} system property used by {@link #debugCacheIfCachingFlag}. + */ + private static final String ENV_DEBUG = "debug"; + /** + * Whether the cache of pre-test run data repositories should be cleared before all tests of this stage are executed. + */ + private static final String ENV_DEBUG_KEEP_CACHE = "debugKeepCache"; + /** + * Enables caching in {@link #debugCacheIfCachingFlag}. + */ + private static final String CACHING = "stageTestCaching"; + private static final Logger logger = LoggerFactory.getLogger(StageTest.class); + private final transient T stage; + private final transient List allProjects; + private final transient Set> agents; + private final transient Map, Set>> informantsMap; + private final transient Map> dataRepositoryCaches = new HashMap<>(); + + /** + * Sole constructor of stage tests. + * + * @param stage dummy instance of the stage that should be tested + * @param allProjects all projects that should be used for testing + */ + public StageTest(T stage, U[] allProjects) { + this.stage = stage; + this.agents = ConfigurationUtility.getAgents(stage); + this.informantsMap = ConfigurationUtility.getInformantsMap(stage); + this.allProjects = List.of(allProjects); + } + + /** + * {@return the data repository that is created by executing the pre-runner for the project} + * + * @param project the project that is executed by the pre-runner + */ + private DataRepository setup(U project) { + logger.info("Run PreTestRunner for {}", project.getProjectName()); + var preRunDataRepository = runPreTestRunner(project); + logger.info("Finished PreTestRunner for {}", project.getProjectName()); + return preRunDataRepository; + } + + /** + * Runs the test-runner for the specified project. Before the test-runner is executed, the pre-runner data repository is retrieved (Either via execution of + * the pre-runner or from the cache). + * + * @param project the project + * @return the data repository produced by the test-runner + */ + protected DataRepository run(U project) { + return run(project, SortedMaps.mutable.empty()); + } + + /** + * Runs the test-runner for the specified project using the supplied additional configurations. Before the test-runner is executed, the pre-runner data + * repository is retrieved (Either via execution of the pre-runner or from the cache). + * + * @param project the project + * @param additionalConfigurations a map of additional configurations + * @return the data repository produced by the test-runner + */ + protected DataRepository run(U project, SortedMap additionalConfigurations) { + return run(project, additionalConfigurations, true); + } + + /** + * Runs the test-runner for the specified project using the supplied additional configurations. Before the test-runner is executed, the pre-runner data + * repository is retrieved (Either via execution of the pre-runner or from the cache). + * + * @param project the project + * @param additionalConfigurations a map of additional configurations + * @param cachePreRun if false, the cache is ignored and the pre-runner is executed again + * @return the data repository produced by the test-runner + */ + protected DataRepository run(U project, SortedMap additionalConfigurations, boolean cachePreRun) { + var preRunDataRepository = getDataRepository(project, cachePreRun); + logger.info("Run TestRunner for {}", project.getProjectName()); + var dataRepository = runTestRunner(project, additionalConfigurations, preRunDataRepository); + logger.info("Finished TestRunner for {}", project.getProjectName()); + return dataRepository; + } + + /** + * If {@link #ENV_DEBUG} is set, a CLI prompt will ask the user whether they want to cache the provided object in the test data cache with the provided + * identifier. This can be used to cache serializable objects for debugging between program executions. + * + * @param id the identifier of the test data cache + * @param obj the object that should be cached + */ + protected void debugCacheWithPrompt(String id, Serializable obj) { + if (Boolean.parseBoolean(System.getenv().getOrDefault(ENV_DEBUG, Boolean.FALSE.toString()))) { + System.out.println("Cache " + obj.getClass().getSimpleName() + " at " + id + "? y/n:"); + if (new Scanner(System.in).nextLine().equals("y")) { + cache(id, obj); + } + } else { + logger.info("Set \"" + ENV_DEBUG + "=true\" to enable caching prompts"); + } + } + + /** + * If {@link #ENV_DEBUG} is set, a CLI prompt will ask the user whether they want to enable caching. If caching is disabled, calls to + * {@link #debugCacheIfCachingFlag} are ignored. + */ + protected void debugSetCachingFlag() { + if (Boolean.parseBoolean(System.getenv().getOrDefault(ENV_DEBUG, Boolean.FALSE.toString()))) { + System.out.println("Enable caching? y/n:"); + if (new Scanner(System.in).nextLine().equals("y")) { + System.setProperty(CACHING, "true"); + } + } else { + logger.info("Set \"" + ENV_DEBUG + "=true\" to enable caching prompts"); + } + } + + /** + * If caching is enabled, the provided object is cached in the test data cache with the provided identifier. Otherwise, calls to this function are ignored. + * This can be used to cache serializable objects for debugging between program executions. + * + * @param id the identifier of the test data cache + * @param obj the object that should be cached + */ + protected void debugCacheIfCachingFlag(String id, Serializable obj) { + if (Boolean.parseBoolean(System.getProperty(CACHING, "false"))) { + cache(id, obj); + } + } + + /** + * The provided object is cached in the test data cache with the provided identifier. This can be used to cache serializable objects for debugging between + * program executions. + * + * @param id the identifier of the test data cache + * @param obj the object that should be cached + */ + protected void cache(String id, Serializable obj) { + try (TestDataCache drCache = new TestDataCache<>(stage.getClass(), obj.getClass(), id, "cache/") { + }) { + drCache.cache(obj); + } + } + + /** + * {@return the cached object from the test data cache with the specified identifier} The provided class should match the serialized object. + * + * @param id the identifier of the test data cache + * @param cls the class object of the serialized object + * @param the type of the serialized object + */ + protected W getCached(String id, Class cls) { + try (TestDataCache drCache = new TestDataCache<>(stage.getClass(), cls, id, "cache/")) { + return drCache.getOrRead(); + } + } + + /** + * {@return the pre-runner data repository for the specified project} The first execution of the pre-runner is cached to speed up repeated calls to the + * function for the same project and pipeline configuration. If the data repository is retrieved from the cache, a deep copy is provided that can be + * modified without restraints. + * + * @param project the project + * @param cachePreRun if false, the cache is ignored and the pre-runner is executed again + */ + + @DeepCopy + protected DataRepository getDataRepository(U project, boolean cachePreRun) { + if (!cachePreRun) { + return this.setup(project); + } + + try (TestDataRepositoryCache drCache = dataRepositoryCaches.computeIfAbsent(project, dp -> new TestDataRepositoryCache<>(stage.getClass(), + project))) { + return drCache.get(this::setup); + } + } + + /** + * Runs the test-runner for the specified project with the supplied additional configurations using {@link #run} and creates a record that summarizes the + * results of the run. + * + * @param project the project + * @param additionalConfigurations a map of additional configurations + * @param cachePreRun if false, the cache is ignored and the pre-runner is executed again + * @return the record that is produced from the test-runner data repository + */ + protected abstract V runComparable(U project, SortedMap additionalConfigurations, boolean cachePreRun); + + /** + * Runs the test-runner for the specified project with the supplied additional configurations using {@link #run} and creates a record that summarizes the + * results of the run. + * + * @param project the project + * @param additionalConfigurations a map of additional configurations + * @return the record that is produced from the test-runner data repository + */ + protected V runComparable(U project, SortedMap additionalConfigurations) { + return runComparable(project, additionalConfigurations, true); + } + + /** + * Runs the test-runner for the specified project using {@link #run} and creates a record that summarizes the results of the run. + * + * @param project the project + * @param cachePreRun if false, the cache is ignored and the pre-runner is executed again + * @return the record that is produced from the test-runner data repository + */ + protected V runComparable(U project, boolean cachePreRun) { + return runComparable(project, SortedMaps.mutable.empty(), cachePreRun); + } + + /** + * Runs the test-runner for the specified project using {@link #run} and creates a record that summarizes the results of the run. + * + * @param project the project + * @return the record that is produced from the test-runner data repository + */ + protected V runComparable(U project) { + return runComparable(project, SortedMaps.mutable.empty(), true); + } + + /** + * Runs the pre-runner and returns the data repository produced by it. The pre-runner should include all stages that are considered pre-requisites to the + * execution of the test-runner. For simple pre-runners, it is advisable to define an + * {@link edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner AnonymousRunner} inside this function. A more complex pre-runner should be + * encapsulated in a dedicated {@link edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner ArDoCoRunner} test class. It is advisable to run the + * runner without saving, e.g. {@link ArDoCoRunner#runWithoutSaving()}. + * + * @param project the project that is executed by the pre-runner + * @return the original data repository that was produced by the pre-runner + */ + protected abstract DataRepository runPreTestRunner(U project); + + /** + * Runs the test-runner and returns the data repository produced by it. The test-runner should include the stage being tested. For test-runners, it is + * advisable to define an {@link edu.kit.kastel.mcse.ardoco.core.execution.runner.AnonymousRunner AnonymousRunner} inside this function and passing the + * {@code preRunDataRepository} to it. + * + * @param project the project that is executed by the test-runner + * @param additionalConfigurations additional configurations for the test-runner execution + * @param preRunDataRepository a deep copy of the pre-runner data repository + * @return the original data repository that was produced by the test-runner + */ + protected abstract DataRepository runTestRunner(U project, SortedMap additionalConfigurations, + @DeepCopy DataRepository preRunDataRepository); + + private static final int REPETITIONS = 2; + + /** + * If {@link #ENV_DEBUG_KEEP_CACHE} is not set, this function resets the pre-runner data repository caches for all projects. + */ + @BeforeAll + protected void resetAllTestDataRepositoryCaches() { + resetAllTestDataRepositoryCaches(false); + } + + /** + * If {@code force=true} or {@link #ENV_DEBUG_KEEP_CACHE} is not set, this function resets the pre-runner data repository caches for all projects. + * + * @param force whether a reset should be forced + */ + protected void resetAllTestDataRepositoryCaches(boolean force) { + debugSetCachingFlag(); + if (!force && Boolean.parseBoolean(System.getenv().getOrDefault(ENV_DEBUG_KEEP_CACHE, "false"))) { + logger.warn("Keeping caches, careful! Set \"" + ENV_DEBUG_KEEP_CACHE + "=false\" to disable persistent caching"); + } else { + for (U d : allProjects) { + try (var drCache = new TestDataRepositoryCache<>(stage.getClass(), d)) { + drCache.cache(null); + } + } + } + } + + /** + * Runs the stage multiple times using the same pre-runner data and checks whether the results are equal. + */ + @DisplayName("Repetition Test Stage") + @Test + @Order(-1) + void stageRepetitionTest() { + var results = new ArrayList(REPETITIONS); + for (var i = 0; i < REPETITIONS; i++) { + logger.info("Stage {} repetition {}/{}", stage.getClass().getSimpleName(), i + 1, REPETITIONS); + results.add(runComparable(allProjects.get(0))); + } + Assertions.assertEquals(1, results.stream().distinct().toList().size()); + } + + /** + * Runs the agent multiple times using the same pre-runner data and checks whether the results are equal. All other agents are disabled. + * + * @param clazzAgent the class object of the pipeline agent that is being tested + */ + @DisplayName("Repetition Test Agents") + @ParameterizedTest(name = "{0}") + @MethodSource("getAgents") + @Order(-2) + void agentRepetitionTest(Class clazzAgent) { + var results = new ArrayList(REPETITIONS); + for (var i = 0; i < REPETITIONS; i++) { + logger.info("Agent {} repetition {}/{}", clazzAgent.getSimpleName(), i + 1, REPETITIONS); + results.add(runComparable(allProjects.get(0), ConfigurationUtility.enableAgents(stage.getClass(), Set.of(clazzAgent)))); + } + Assertions.assertEquals(1, results.stream().distinct().toList().size()); + } + + /** + * Runs the informant multiple times using the same pre-runner data and checks whether the results are equal. All other agents and all other informants are + * disabled. + * + * @param clazzInformant the class object of the pipeline informants that is being tested + */ + @DisplayName("Repetition Test Informants") + @ParameterizedTest(name = "{0}") + @MethodSource("getInformants") + @Order(-3) + void informantRepetitionTest(Class clazzInformant) { + var results = new ArrayList(REPETITIONS); + for (var i = 0; i < REPETITIONS; i++) { + logger.info("Informant {} repetition {}/{}", clazzInformant.getSimpleName(), i + 1, REPETITIONS); + results.add(runComparable(allProjects.get(0), ConfigurationUtility.enableInformants(stage, Set.of(clazzInformant)))); + } + Assertions.assertEquals(1, results.stream().distinct().toList().size()); + } + + /** + * {@return all class objects of the agents from the stage} + */ + public Set> getAgents() { + return this.agents; + } + + /** + * {@return all class objects of the informants from the stage} + */ + public Set> getInformants() { + return informantsMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/TestDataCache.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/TestDataCache.java new file mode 100644 index 000000000..dd8071488 --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/TestDataCache.java @@ -0,0 +1,38 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import java.io.File; +import java.io.Serializable; + +import com.fasterxml.jackson.core.type.TypeReference; + +import edu.kit.kastel.mcse.ardoco.core.common.util.SerializableFileBasedCache; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractExecutionStage; + +/** + * Persistent cache for {@link StageTest} test data. For any given stage with the name X, the test data is cached in ArDoCo's user directory folder in the + * {@code /test/X} directory. + * + * @param the type of serializable object that can be contained in this cache + */ +public class TestDataCache extends SerializableFileBasedCache { + protected final Class stage; + + /** + * Creates a new test data cache with the specified identifier in the specified subfolder of the test data cache directory. + * + * @param stage the stage that this test data is associated with + * @param cls the class object of objects that are contained by this cache + * @param identifier the identifier of the cache + * @param subFolder the sub-folder in the /test/X directory, must end with '/' + */ + public TestDataCache(Class stage, Class cls, String identifier, String subFolder) { + super(cls, identifier, "test" + File.separator + stage.getSimpleName() + File.separator + subFolder); + this.stage = stage; + } + + public TestDataCache(Class stage, TypeReference typeReference, String identifier, String subFolder) { + super(typeReference, identifier, "test" + File.separator + stage.getSimpleName() + File.separator + subFolder); + this.stage = stage; + } +} diff --git a/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/TestDataRepositoryCache.java b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/TestDataRepositoryCache.java new file mode 100644 index 000000000..e6f21380d --- /dev/null +++ b/tests/tests-resources/src/main/java/edu/kit/kastel/mcse/ardoco/tests/eval/TestDataRepositoryCache.java @@ -0,0 +1,78 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import java.util.HashMap; +import java.util.function.Function; +import java.util.prefs.Preferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; + +import edu.kit.kastel.mcse.ardoco.core.data.DataRepository; +import edu.kit.kastel.mcse.ardoco.core.data.DeepCopy; +import edu.kit.kastel.mcse.ardoco.core.pipeline.AbstractExecutionStage; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; + +/** + * Persistent cache for {@link StageTest} a pre-runner data repository. For any given project, the pre-runner data repository is cached in ArDoCo's user + * directory folder in the {@code data-repositories/} sub-folder of the {@link TestDataCache test data directory}. + * + * @param the type of project + */ +public class TestDataRepositoryCache extends TestDataCache> { + private static final Logger logger = LoggerFactory.getLogger(TestDataRepositoryCache.class); + private final T project; + + /** + * Creates a new test data repository cache for the given stage and project. + * + * @param stage the stage + * @param project the project + */ + public TestDataRepositoryCache(Class stage, T project) { + super(stage, new TypeReference>() { + }, project.getProjectName(), "data-repositories/"); + this.project = project; + } + + @Override + public HashMap getDefault() { + return new HashMap<>(); + } + + /** + * Gets a deep copy of the cached data repository. If no data repository is cached, the mapping function is used to compute it and the result is cached, + * deep copied and returned. The cache is automatically invalidated and reset, if the source files of a project (such as the text, gold standards, etc.) + * have changed. + * + * @param mappingFunction a function to compute the data repository of a project + * @return a deep copy of the data repository + */ + @DeepCopy + public DataRepository get(Function mappingFunction) { + checkVersion(); + + var testData = getOrRead(); + if (!testData.containsKey(project)) { + testData.put(project, mappingFunction.apply(project)); + this.write(testData); + } + + return testData.get(project).deepCopy(); + } + + /** + * Checks whether the version of the source files has changed. + */ + private void checkVersion() { + var versionPref = "version-" + getClass().getSimpleName() + "-" + stage.getSimpleName(); + var versionProject = project.getSourceFilesVersion(); + if (Preferences.userNodeForPackage(project.getClass()).getLong(versionPref, -1L) != versionProject) { + Preferences.userNodeForPackage(project.getClass()).putLong(versionPref, versionProject); + logger.warn("{}'s source files have changed, resetting {} file", project.getProjectName(), getIdentifier()); + resetFile(); + } + } +} diff --git a/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBoxTest.java b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBoxTest.java new file mode 100644 index 000000000..111aaee6c --- /dev/null +++ b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoundingBoxTest.java @@ -0,0 +1,64 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class BoundingBoxTest { + double epsilon = 0.000001d; + BoundingBox topLeft = new BoundingBox(0, 0, 10, 10); + BoundingBox bottomLeft = new BoundingBox(0, 10, 10, 20); + BoundingBox middle = new BoundingBox(5, 5, 15, 15); + BoundingBox subMiddle = new BoundingBox(7, 7, 13, 13); + BoundingBox topRight = new BoundingBox(10, 0, 20, 10); + BoundingBox bottomRight = new BoundingBox(10, 10, 20, 20); + + @Test + void area() { + assertEquals(100.0, new BoundingBox(5, 5, 15, 15).area(), epsilon); + assertEquals(200.0, new BoundingBox(5, 5, 25, 15).area(), epsilon); + assertEquals(200.0, new BoundingBox(5, 5, 15, 25).area(), epsilon); + } + + @Test + void intersect() { + assertEquals(middle.area(), middle.intersect(middle).orElseThrow().area(), epsilon); + assertEquals(25, middle.intersect(topLeft).orElseThrow().area(), epsilon); + assertEquals(25, middle.intersect(topRight).orElseThrow().area(), epsilon); + assertEquals(25, middle.intersect(bottomLeft).orElseThrow().area(), epsilon); + assertEquals(25, middle.intersect(bottomRight).orElseThrow().area(), epsilon); + } + + @Test + void union() { + assertEquals(middle.area(), middle.union(middle), epsilon); + assertEquals(topLeft.area() + topRight.area(), topLeft.union(topRight), epsilon); + assertEquals(175, topLeft.union(middle), epsilon); + } + + @Test + void intersectionOverUnion() { + assertEquals(1, middle.intersectionOverUnion(middle), epsilon); + assertEquals(0, topLeft.intersectionOverUnion(topRight), epsilon); + assertEquals(25.0 / 175.0, topLeft.intersectionOverUnion(middle), epsilon); + } + + @Test + void contains() { + assertEquals(1, middle.contains(middle), epsilon); + assertEquals(1, middle.contains(subMiddle), epsilon); + assertEquals(0.36, subMiddle.contains(middle), epsilon); + assertEquals(0.0, subMiddle.contains(middle, true), epsilon); + assertEquals(0, topLeft.contains(topRight), epsilon); + assertEquals(0.25, topLeft.contains(middle), epsilon); + } + + @Test + void containsEntirely() { + assertTrue(middle.containsEntirely(middle)); + assertTrue(middle.containsEntirely(subMiddle)); + assertFalse(topLeft.containsEntirely(topRight)); + assertFalse(topLeft.containsEntirely(middle)); + } +} diff --git a/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoxGSTest.java b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoxGSTest.java new file mode 100644 index 000000000..21c2ef1e2 --- /dev/null +++ b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/BoxGSTest.java @@ -0,0 +1,29 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class BoxGSTest { + public static final BoxGS DUMMY_BOX_GS = new BoxGS(DiagramGSTest.DUMMY_DIAGRAM_GS, DummyObjects.DUMMY_BOUNDING_BOX, new TextBox[] { + DummyObjects.DUMMY_TEXT_BOX }, new BoxGS[] {}, new TraceLinkGS[] { DummyObjects.DUMMY_TRACE_LINK_GS }); + + @DisplayName("Evaluate Serialize BoxG") + @Test + void serialize() throws IOException, ClassNotFoundException { + var byteArrayOutputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(byteArrayOutputStream).writeObject(DUMMY_BOX_GS); + var byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + var deserialized = (BoxGS) new ObjectInputStream(byteArrayInputStream).readObject(); + + assertEquals(DUMMY_BOX_GS, deserialized); + } +} diff --git a/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramGSTest.java b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramGSTest.java new file mode 100644 index 000000000..c56b77b55 --- /dev/null +++ b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DiagramGSTest.java @@ -0,0 +1,37 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.junit.jupiter.api.Test; + +import edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject; + +public class DiagramGSTest { + public static final DiagramGS DUMMY_DIAGRAM_GS = new DiagramGS(DiagramProject.TEAMMATES, "SomePath.jpg", new BoxGS[] {}); + + @Test + void serialize() throws IOException, ClassNotFoundException { + var serialize = DUMMY_DIAGRAM_GS; + var byteArrayOutputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(byteArrayOutputStream).writeObject(serialize); + var byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + var deserialized = (DiagramGS) new ObjectInputStream(byteArrayInputStream).readObject(); + + assertEquals(serialize.getResourceName(), deserialized.getResourceName()); + assertTrue(serialize.getBoxes().containsAll(deserialized.getBoxes())); + assertTrue(deserialized.getBoxes().containsAll(serialize.getBoxes())); + assertTrue(serialize.getTextBoxes().containsAll(deserialized.getTextBoxes())); + assertTrue(deserialized.getTextBoxes().containsAll(serialize.getTextBoxes())); + assertTrue(serialize.getTraceLinks(null).containsAll(deserialized.getTraceLinks(null))); + assertTrue(deserialized.getTraceLinks(null).containsAll(serialize.getTraceLinks(null))); + assertEquals(serialize.getDiagramProject(), deserialized.getDiagramProject()); + } +} diff --git a/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DummyObjects.java b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DummyObjects.java new file mode 100644 index 000000000..2d37aacc2 --- /dev/null +++ b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/diagramrecognition/DummyObjects.java @@ -0,0 +1,11 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition; + +import edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks.TraceType; + +public class DummyObjects { + public static final BoundingBox DUMMY_BOUNDING_BOX = new BoundingBox(0, 0, 100, 100); + public static final TextBox DUMMY_TEXT_BOX = new TextBox(DUMMY_BOUNDING_BOX, 1.0, "Lorem Ipsum"); + public static final TraceLinkGS DUMMY_TRACE_LINK_GS = new TraceLinkGS("SomeName", new int[] { 0, 42, 404 }, new TypedTraceLinkGS[] { new TypedTraceLinkGS( + new int[] { 3, 7, 11 }, TraceType.ENTITY_COREFERENCE) }); +} diff --git a/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramTextTraceLinkTest.java b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramTextTraceLinkTest.java new file mode 100644 index 000000000..7f46b36e1 --- /dev/null +++ b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/core/api/models/tracelinks/DiagramTextTraceLinkTest.java @@ -0,0 +1,76 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.core.api.models.tracelinks; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.eclipse.collections.api.factory.Sets; +import org.eclipse.collections.api.set.ImmutableSet; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import edu.kit.kastel.mcse.ardoco.core.api.diagramrecognition.BoxGSTest; +import edu.kit.kastel.mcse.ardoco.core.api.text.Phrase; +import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; +import edu.kit.kastel.mcse.ardoco.core.api.text.Word; + +class DiagramTextTraceLinkTest { + public static final Word mockWord = Mockito.mock(Word.class, Mockito.withSettings().serializable()); + public static final Sentence mockSentence = Mockito.mock(Sentence.class, Mockito.withSettings().serializable()); + public static final Phrase mockPhrase = Mockito.mock(Phrase.class, Mockito.withSettings().serializable()); + + static { + Mockito.doReturn("The SomeText example").when(mockPhrase).getText(); + Mockito.doReturn("SomeText").when(mockWord).getText(); + Mockito.doReturn(0).when(mockWord).getPosition(); + Mockito.doReturn(0).when(mockWord).getSentenceNo(); + Mockito.doReturn(mockPhrase).when(mockWord).getPhrase(); + Mockito.doReturn(mockSentence).when(mockWord).getSentence(); + Mockito.doReturn("The SomeText example is part of a sentence").when(mockSentence).getText(); + Mockito.doReturn(0).when(mockSentence).getSentenceNumber(); + Mockito.doReturn(1).when(mockSentence).getSentenceNumberForOutput(); + } + + public static final DiagramTextTraceLink DUMMY_DIAGRAM_TEXT_TRACE_LINK_SENTENCE = new DiagramGoldStandardTraceLink(BoxGSTest.DUMMY_BOX_GS, mockSentence, + "SomeIdentifier", "SomeStandard.json"); + public static final DiagramTextTraceLink DUMMY_DIAGRAM_TEXT_TRACE_LINK_WORD = new DiagramWordTraceLink(BoxGSTest.DUMMY_BOX_GS, mockWord, "SomeIdentifier", + 0.5, null); + + public static List getDummyDiaTexTraceLinks() { + return List.of(DUMMY_DIAGRAM_TEXT_TRACE_LINK_SENTENCE, DUMMY_DIAGRAM_TEXT_TRACE_LINK_WORD); + } + + @DisplayName("Evaluate Serialize DiaTextTraceLinks") + @ParameterizedTest(name = "{0}") + @MethodSource("getDummyDiaTexTraceLinks") + void serialize(DiagramTextTraceLink diagramTextTraceLink) throws IOException, ClassNotFoundException { + var byteArrayOutputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(byteArrayOutputStream).writeObject(diagramTextTraceLink); + var byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + var deserialized = (DiagramTextTraceLink) new ObjectInputStream(byteArrayInputStream).readObject(); + + assertEquals(diagramTextTraceLink, deserialized); + } + + @DisplayName("Evaluate Serialize DiaTextTraceLinks") + @Test + void serialize() throws IOException, ClassNotFoundException { + var byteArrayOutputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(byteArrayOutputStream).writeObject(Sets.immutable.fromStream(getDummyDiaTexTraceLinks().stream())); + var byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); + var deserialized = (ImmutableSet) new ObjectInputStream(byteArrayInputStream).readObject(); + + assertTrue(getDummyDiaTexTraceLinks().containsAll(deserialized.toList())); + assertTrue(deserialized.containsAll(getDummyDiaTexTraceLinks())); + } +} diff --git a/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/tests/eval/DiagramProjectTest.java b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/tests/eval/DiagramProjectTest.java new file mode 100644 index 000000000..91303cbfb --- /dev/null +++ b/tests/tests-resources/src/test/java/edu/kit/kastel/mcse/ardoco/tests/eval/DiagramProjectTest.java @@ -0,0 +1,23 @@ +/* Licensed under MIT 2023-2024. */ +package edu.kit.kastel.mcse.ardoco.tests.eval; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class DiagramProjectTest { + + @DisplayName("Test Diagram Project") + @ParameterizedTest(name = "{0}") + @MethodSource("edu.kit.kastel.mcse.ardoco.tests.eval.DiagramProject#getNonHistoricalProjects") + @Order(1) + void getDiagramsFromGoldstandard(DiagramProject diagramProject) throws IOException { + assertEquals(-1L, Files.mismatch(diagramProject.getDiagramsGoldStandardFile().toPath(), diagramProject.getDiagramsGoldStandardFile().toPath())); + } +} diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadCodeTraceabilityLinkRecoveryEvaluation.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadCodeTraceabilityLinkRecoveryEvaluation.java index 18d0cb604..d0b1f8f27 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadCodeTraceabilityLinkRecoveryEvaluation.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadCodeTraceabilityLinkRecoveryEvaluation.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration; import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.OUTPUT; @@ -21,11 +21,10 @@ import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCoForSadCodeTraceabilityLinkRecovery; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; -class SadCodeTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { +class SadCodeTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { @Override protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { @@ -36,8 +35,7 @@ protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { @Override protected ArDoCoRunner getAndSetupRunner(CodeProject codeProject) { String name = codeProject.name().toLowerCase(); - Project textProject = codeProject.getProject(); - File textInput = textProject.getTextFile(); + File textInput = codeProject.getTextFile(); File inputCode = getInputCode(codeProject); SortedMap additionalConfigsMap = new TreeMap<>(); File outputDir = new File(OUTPUT); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamCodeTraceabilityLinkRecoveryEvaluation.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamCodeTraceabilityLinkRecoveryEvaluation.java index e4dc071e4..2147a0d1e 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamCodeTraceabilityLinkRecoveryEvaluation.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamCodeTraceabilityLinkRecoveryEvaluation.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration; import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.OUTPUT; @@ -22,10 +22,9 @@ import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCoForSadSamCodeTraceabilityLinkRecovery; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; -class SadSamCodeTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { +class SadSamCodeTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { @Override protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { @@ -36,9 +35,8 @@ protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { @Override protected ArDoCoRunner getAndSetupRunner(CodeProject codeProject) { String name = codeProject.name().toLowerCase(); - Project textProject = codeProject.getProject(); - File textInput = textProject.getTextFile(); - File inputArchitectureModel = codeProject.getProject().getModelFile(); + File textInput = codeProject.getTextFile(); + File inputArchitectureModel = codeProject.getModelFile(); File inputCode = getInputCode(codeProject); SortedMap additionalConfigsMap = new TreeMap<>(); File outputDir = new File(OUTPUT); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamTraceabilityLinkRecoveryEvaluation.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamTraceabilityLinkRecoveryEvaluation.java index a034b2a8f..6c3be7eba 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamTraceabilityLinkRecoveryEvaluation.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SadSamTraceabilityLinkRecoveryEvaluation.java @@ -1,10 +1,7 @@ -/* Licensed under MIT 2021-2023. */ +/* Licensed under MIT 2021-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration; -import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.DATA_MAP; -import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.OUTPUT; -import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.PROJECT_RESULTS; -import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.RESULTS; +import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.*; import java.io.File; import java.io.IOException; @@ -34,19 +31,17 @@ import edu.kit.kastel.mcse.ardoco.core.execution.ConfigurationHelper; import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.TLRUtil; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files.TLGoldStandardFile; /** - * Integration test that evaluates the traceability link recovery capabilities of ArDoCo. Runs on the projects that are defined in the enum {@link Project}. + * Integration test that evaluates the traceability link recovery capabilities of ArDoCo. */ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class SadSamTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { - +public class SadSamTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { @Override protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { var traceLinks = arDoCoResult.getAllTraceLinks(); @@ -54,18 +49,17 @@ protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { } @Override - protected ArDoCoResult runTraceLinkEvaluation(CodeProject codeProject) { - var result = super.runTraceLinkEvaluation(codeProject); - DATA_MAP.put(codeProject.getProject(), result); + protected ArDoCoResult runTraceLinkEvaluation(T project) { + var result = super.runTraceLinkEvaluation(project); + DATA_MAP.put(project, result); return result; } @Override - protected ArDoCoRunner getAndSetupRunner(CodeProject codeProject) { - var project = codeProject.getProject(); + protected ArDoCoRunner getAndSetupRunner(T project) { var additionalConfigsMap = ConfigurationHelper.loadAdditionalConfigs(project.getAdditionalConfigurationsFile()); - String name = project.name().toLowerCase(); + String name = project.getProjectName(); File inputModel = project.getModelFile(); File inputText = project.getTextFile(); File outputDir = new File(OUTPUT); @@ -76,14 +70,12 @@ protected ArDoCoRunner getAndSetupRunner(CodeProject codeProject) { } @Override - protected ExpectedResults getExpectedResults(CodeProject codeProject) { - var project = codeProject.getProject(); + protected ExpectedResults getExpectedResults(T project) { return project.getExpectedTraceLinkResults(); } @Override - protected ImmutableList getGoldStandard(CodeProject codeProject) { - var project = codeProject.getProject(); + protected ImmutableList getGoldStandard(T project) { return project.getTlrGoldStandard(); } @@ -116,8 +108,8 @@ protected EvaluationResults calculateEvaluationResults(ArDoCoResult arDo return results; } - protected ArDoCoResult getArDoCoResult(Project project) { - String name = project.name().toLowerCase(); + public ArDoCoResult getArDoCoResult(T project) { + String name = project.getProjectName(); var inputModel = project.getModelFile(); var inputText = project.getTextFile(); @@ -146,7 +138,7 @@ protected ArDoCoResult getArDoCoResult(String name, File inputText, File inputMo * @param project the result's project * @param arDoCoResult the result */ - protected static void checkResults(Project project, ArDoCoResult arDoCoResult) { + public static void checkResults(GoldStandardProject project, ArDoCoResult arDoCoResult) { var modelIds = arDoCoResult.getModelIds(); var modelId = modelIds.stream().findFirst().orElseThrow(); @@ -162,11 +154,11 @@ protected static void checkResults(Project project, ArDoCoResult arDoCoResult) { } - private static void logAndSaveProjectResult(Project project, ArDoCoResult arDoCoResult, EvaluationResults results, + private static void logAndSaveProjectResult(GoldStandardProject project, ArDoCoResult arDoCoResult, EvaluationResults results, ExpectedResults expectedResults) { if (logger.isInfoEnabled()) { - String projectName = project.name().toLowerCase(); - TestUtil.logResultsWithExpected(logger, projectName, results, expectedResults); + String projectName = project.getProjectName(); + TestUtil.logExtendedResultsWithExpected(logger, SadSamTraceabilityLinkRecoveryEvaluation.class, projectName, results, expectedResults); var data = arDoCoResult.dataRepository(); printDetailedDebug(results, data); @@ -197,8 +189,8 @@ private static void compareResultWithExpected(EvaluationResults results, .phiCoefficient() + " is below the expected minimum value " + expectedResults.phiCoefficient())); } - static void writeDetailedOutput(Project project, ArDoCoResult arDoCoResult) { - String name = project.name().toLowerCase(); + public static void writeDetailedOutput(GoldStandardProject project, ArDoCoResult arDoCoResult) { + String name = project.getProjectName(); var path = Path.of(OUTPUT).resolve(name); try { Files.createDirectories(path); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SamCodeTraceabilityLinkRecoveryEvaluation.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SamCodeTraceabilityLinkRecoveryEvaluation.java index 3e6a3e36d..a5bad6ef8 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SamCodeTraceabilityLinkRecoveryEvaluation.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/SamCodeTraceabilityLinkRecoveryEvaluation.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration; import static edu.kit.kastel.mcse.ardoco.core.tests.integration.TraceLinkEvaluationIT.OUTPUT; @@ -21,7 +21,7 @@ import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; -class SamCodeTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { +class SamCodeTraceabilityLinkRecoveryEvaluation extends TraceabilityLinkRecoveryEvaluation { @Override protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { @@ -33,7 +33,7 @@ protected boolean resultHasRequiredData(ArDoCoResult arDoCoResult) { protected ArDoCoForSamCodeTraceabilityLinkRecovery getAndSetupRunner(CodeProject codeProject) { String name = codeProject.name().toLowerCase(); File inputCode = getInputCode(codeProject); - File inputArchitectureModel = codeProject.getProject().getModelFile(); + File inputArchitectureModel = codeProject.getModelFile(); SortedMap additionalConfigsMap = new TreeMap<>(); File outputDir = new File(OUTPUT); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationIT.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationIT.java index 8fe7e9bde..da51c1d28 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationIT.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationIT.java @@ -9,7 +9,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -18,14 +18,7 @@ import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.list.MutableList; import org.eclipse.collections.api.tuple.Pair; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -41,6 +34,7 @@ import edu.kit.kastel.mcse.ardoco.core.execution.ArDoCo; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.calculator.ResultCalculatorUtil; @@ -53,7 +47,7 @@ import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files.TLSummaryFile; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class TraceLinkEvaluationIT { +public class TraceLinkEvaluationIT { protected static final Logger logger = LoggerFactory.getLogger(TraceLinkEvaluationIT.class); @@ -62,9 +56,9 @@ class TraceLinkEvaluationIT { protected static final String LOGGING_ARDOCO_CORE = "org.slf4j.simpleLogger.log.edu.kit.kastel.mcse.ardoco.core"; protected static AtomicBoolean analyzeCodeDirectly = ANALYZE_CODE_DIRECTLY; - protected static final List>> RESULTS = new ArrayList<>(); + protected static final List>> RESULTS = new ArrayList<>(); protected static final MutableList> PROJECT_RESULTS = Lists.mutable.empty(); - protected static final Map DATA_MAP = new EnumMap<>(Project.class); + protected static final Map DATA_MAP = new HashMap<>(); @BeforeAll static void beforeAll() { @@ -142,7 +136,7 @@ private static > List filterForProjects(Collection unfil @ParameterizedTest(name = "{0}") @MethodSource("getNonHistoricalCodeProjects") @Order(1) - void evaluateSadSamCodeTlrFullIT(CodeProject project) { + protected void evaluateSadSamCodeTlrFullIT(CodeProject project) { analyzeCodeDirectly.set(true); if (analyzeCodeDirectly.get()) cleanUpCodeRepository(project); @@ -157,7 +151,7 @@ void evaluateSadSamCodeTlrFullIT(CodeProject project) { @ParameterizedTest(name = "{0}") @EnumSource(value = CodeProject.class, mode = EnumSource.Mode.MATCH_NONE, names = "^.*HISTORICAL$") @Order(2) - void evaluateSamCodeTlrFullIT(CodeProject project) { + protected void evaluateSamCodeTlrFullIT(CodeProject project) { analyzeCodeDirectly.set(true); if (analyzeCodeDirectly.get()) cleanUpCodeRepository(project); @@ -171,23 +165,23 @@ void evaluateSamCodeTlrFullIT(CodeProject project) { @ParameterizedTest(name = "{0}") @MethodSource("getNonHistoricalCodeProjects") @Order(9) - void evaluateSadSamCodeTlrIT(CodeProject project) { + protected void evaluateSadSamCodeTlrIT(CodeProject codeProject) { analyzeCodeDirectly.set(false); if (analyzeCodeDirectly.get()) - cleanUpCodeRepository(project); + cleanUpCodeRepository(codeProject); var evaluation = new SadSamCodeTraceabilityLinkRecoveryEvaluation(); - var results = evaluation.runTraceLinkEvaluation(project); + var results = evaluation.runTraceLinkEvaluation(codeProject); Assertions.assertNotNull(results); - TraceabilityLinkRecoveryEvaluation.resultMap.put(project.getProject(), results); + TraceabilityLinkRecoveryEvaluation.resultMap.put(codeProject, results); } @DisplayName("Evaluate SAM-Code TLR") @ParameterizedTest(name = "{0}") @MethodSource("getNonHistoricalCodeProjects") @Order(10) - void evaluateSamCodeTlrIT(CodeProject project) { + protected void evaluateSamCodeTlrIT(CodeProject project) { analyzeCodeDirectly.set(false); if (analyzeCodeDirectly.get()) cleanUpCodeRepository(project); @@ -201,12 +195,8 @@ void evaluateSamCodeTlrIT(CodeProject project) { @ParameterizedTest(name = "{0}") @MethodSource("getNonHistoricalCodeProjects") @Order(20) - void evaluateSadSamTlrIT(CodeProject project) { - analyzeCodeDirectly.set(false); - if (analyzeCodeDirectly.get()) - cleanUpCodeRepository(project); - - var evaluation = new SadSamTraceabilityLinkRecoveryEvaluation(); + protected void evaluateSadSamTlrIT(T project) { + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluation<>(); var results = evaluation.runTraceLinkEvaluation(project); Assertions.assertNotNull(results); } @@ -216,10 +206,8 @@ void evaluateSadSamTlrIT(CodeProject project) { @ParameterizedTest(name = "{0}") @MethodSource("getHistoricalProjects") @Order(21) - void evaluateSadSamTlrHistoricalIT(Project project) { - analyzeCodeDirectly.set(false); - - var evaluation = new SadSamTraceabilityLinkRecoveryEvaluation(); + protected void evaluateSadSamTlrHistoricalIT(T project) { + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluation<>(); ArDoCoResult arDoCoResult = evaluation.getArDoCoResult(project); Assertions.assertNotNull(arDoCoResult); @@ -237,11 +225,11 @@ void evaluateSadSamTlrHistoricalIT(Project project) { @ParameterizedTest(name = "{0}") @EnumSource(value = Project.class) @Order(29) - void compareSadSamTlRForPcmAndUmlIT(Project project) { + protected void compareSadSamTlRForPcmAndUmlIT(Project project) { String name = project.name(); var inputText = project.getTextFile(); - var evaluation = new SadSamTraceabilityLinkRecoveryEvaluation(); + var evaluation = new SadSamTraceabilityLinkRecoveryEvaluation<>(); var ardocoRunForPCM = evaluation.getArDoCoResult(project); Assertions.assertNotNull(ardocoRunForPCM); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationSadCodeDirectIT.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationSadCodeDirectIT.java index 2e8871623..9b99705dc 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationSadCodeDirectIT.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceLinkEvaluationSadCodeDirectIT.java @@ -8,7 +8,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -28,7 +28,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.calculator.ResultCalculatorUtil; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.TestLink; @@ -46,9 +46,9 @@ class TraceLinkEvaluationSadCodeDirectIT { protected static final String LOGGING_ARDOCO_CORE = "org.slf4j.simpleLogger.log.edu.kit.kastel.mcse.ardoco.core"; - protected static final List>> RESULTS = new ArrayList<>(); + protected static final List>> RESULTS = new ArrayList<>(); protected static final MutableList> PROJECT_RESULTS = Lists.mutable.empty(); - protected static final Map DATA_MAP = new EnumMap<>(Project.class); + protected static final Map DATA_MAP = new HashMap<>(); @BeforeAll static void beforeAll() { diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceabilityLinkRecoveryEvaluation.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceabilityLinkRecoveryEvaluation.java index 01b476492..34e5097e6 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceabilityLinkRecoveryEvaluation.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/TraceabilityLinkRecoveryEvaluation.java @@ -1,8 +1,8 @@ -/* Licensed under MIT 2023. */ +/* Licensed under MIT 2023-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration; import java.io.File; -import java.util.EnumMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -28,35 +28,34 @@ import edu.kit.kastel.mcse.ardoco.core.execution.runner.ArDoCoRunner; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; import edu.kit.kastel.mcse.ardoco.core.tests.eval.CodeProject; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ExpectedResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.ResultMatrix; -public abstract class TraceabilityLinkRecoveryEvaluation { +public abstract class TraceabilityLinkRecoveryEvaluation { protected static final Logger logger = LoggerFactory.getLogger(TraceabilityLinkRecoveryEvaluation.class); private static final String WARNING_NO_CODE_MODEL = "Could not get code model to enroll gold standard. Using not enrolled gold standard!"; // The path separator is to show that a code entry is not a class but rather a directory that ends with, currently, a "/" (unix style) // If the path separator in the gold standards are changed, this needs to update public static final String GOLD_STANDARD_PATH_SEPARATOR = "/"; - protected static Map resultMap = new EnumMap<>(Project.class); + protected static Map resultMap = new LinkedHashMap<>(); - protected ArDoCoResult runTraceLinkEvaluation(CodeProject codeProject) { - var project = codeProject.getProject(); + protected ArDoCoResult runTraceLinkEvaluation(T project) { ArDoCoResult result = resultMap.get(project); if (result == null || !resultHasRequiredData(result)) { - ArDoCoRunner runner = getAndSetupRunner(codeProject); + ArDoCoRunner runner = getAndSetupRunner(project); result = runner.run(); } Assertions.assertNotNull(result); - var goldStandard = getGoldStandard(codeProject); + var goldStandard = getGoldStandard(project); goldStandard = enrollGoldStandard(goldStandard, result); var evaluationResults = calculateEvaluationResults(result, goldStandard); - ExpectedResults expectedResults = getExpectedResults(codeProject); - TestUtil.logExtendedResultsWithExpected(logger, this, codeProject.name(), evaluationResults, expectedResults); + ExpectedResults expectedResults = getExpectedResults(project); + TestUtil.logExtendedResultsWithExpected(logger, this, project.getProjectName(), evaluationResults, expectedResults); compareResults(evaluationResults, expectedResults); return result; } @@ -74,7 +73,7 @@ protected File getInputCode(CodeProject codeProject) { return inputCode; } - protected abstract ArDoCoRunner getAndSetupRunner(CodeProject codeProject); + protected abstract ArDoCoRunner getAndSetupRunner(T project); private void prepareCode(CodeProject codeProject) { File codeLocation = new File(codeProject.getCodeLocation()); @@ -84,9 +83,9 @@ private void prepareCode(CodeProject codeProject) { } } - protected abstract ExpectedResults getExpectedResults(CodeProject codeProject); + protected abstract ExpectedResults getExpectedResults(T project); - protected abstract ImmutableList getGoldStandard(CodeProject codeProject); + protected abstract ImmutableList getGoldStandard(T project); protected abstract ImmutableList enrollGoldStandard(ImmutableList goldStandard, ArDoCoResult result); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLDiffFile.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLDiffFile.java index 561f1d089..8c0111a28 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLDiffFile.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLDiffFile.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files; import java.io.IOException; @@ -15,7 +15,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.TestLink; @@ -33,19 +33,20 @@ private TLDiffFile() { /** * Writes out the differences of new and old results. - * + * * @param targetFile file to write into * @param newProjectResults new results * @param oldProjectResults old results * @param dataMap the mapping of Project to ArDoCoResult of the new run * @throws IOException if writing fails */ - public static void save(Path targetFile, Collection>> newProjectResults, - Collection>> oldProjectResults, Map dataMap) throws IOException { + public static void save(Path targetFile, Collection>> newProjectResults, + Collection>> oldProjectResults, Map dataMap) + throws IOException { // Assumption: Both collections contain the same projects - newProjectResults = newProjectResults.stream().sorted(Comparator.comparing(x -> x.getOne().name())).toList(); - oldProjectResults = oldProjectResults.stream().sorted(Comparator.comparing(x -> x.getOne().name())).toList(); + newProjectResults = newProjectResults.stream().sorted(Comparator.comparing(x -> x.getOne().getProjectName())).toList(); + oldProjectResults = oldProjectResults.stream().sorted(Comparator.comparing(x -> x.getOne().getProjectName())).toList(); var builder = new StringBuilder(); @@ -70,7 +71,7 @@ public static void save(Path targetFile, Collection> oldProjectResult : oldProjectResults) { + for (Pair> oldProjectResult : oldProjectResults) { var project = oldProjectResult.getOne(); var newResultOptional = newProjectResults.stream().filter(r -> r.getOne().equals(project)).findAny(); if (newResultOptional.isEmpty()) { @@ -79,7 +80,7 @@ public static void save(Path targetFile, Collection loadLinks(Project project) throws IOException { - Path path = project.getTlrGoldStandardFile().toPath(); + public static MutableList loadLinks(GoldStandardProject goldStandardProject) throws IOException { + Path path = goldStandardProject.getTlrGoldStandardFile().toPath(); List lines = Files.readAllLines(path); return Lists.mutable.ofAll(lines.stream() diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLLogFile.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLLogFile.java index f012d74f7..11e8adcac 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLLogFile.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLLogFile.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files; import java.io.IOException; @@ -13,7 +13,7 @@ import org.eclipse.collections.api.tuple.Pair; import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.TestLink; @@ -35,7 +35,7 @@ private TLLogFile() { * @param projectResults the results to write out * @throws IOException if writing to file system fails */ - public static void append(Path targetFile, List>> projectResults) throws IOException { + public static void append(Path targetFile, List>> projectResults) throws IOException { List> results = projectResults.stream().map(Pair::getTwo).toList(); var builder = new StringBuilder(); @@ -49,19 +49,9 @@ public static void append(Path targetFile, List(projectResults); - sortedResults.sort(Comparator.comparing(x -> x.getOne().name())); - for (Pair> projectResult : sortedResults) { - String alias = switch (projectResult.getOne()) { - case MEDIASTORE -> "MS"; - case BIGBLUEBUTTON -> "BBB"; - case BIGBLUEBUTTON_HISTORICAL -> "BBB-H"; - case TEAMMATES -> "TM"; - case TEAMMATES_HISTORICAL -> "TM-H"; - case TEASTORE -> "TS"; - case TEASTORE_HISTORICAL -> "TS-H"; - case JABREF -> "JR"; - case JABREF_HISTORICAL -> "JR-H"; - }; + sortedResults.sort(Comparator.comparing(x -> x.getOne().getProjectName())); + for (Pair> projectResult : sortedResults) { + String alias = projectResult.getOne().getAlias(); EvaluationResults result = projectResult.getTwo(); String precision = NUMBER_FORMAT.format(result.precision()); String recall = NUMBER_FORMAT.format(result.recall()); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLModelFile.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLModelFile.java index d694560c6..21acb9750 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLModelFile.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLModelFile.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files; import java.io.IOException; @@ -9,11 +9,10 @@ import edu.kit.kastel.mcse.ardoco.core.api.models.ModelInstance; import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; /** - * This helper-class offers functionality to write out information about the models as seen by ArDoCo after evaluation - * of TLR. + * This helper-class offers functionality to write out information about the models as seen by ArDoCo after evaluation of TLR. */ public class TLModelFile { @@ -25,19 +24,19 @@ private TLModelFile() { /** * Writes out information about models to the target file. - * + * * @param targetFile the file to write to * @param dataMap the data map to extract model information for each project * @throws IOException if writing to file system fails */ - public static void save(Path targetFile, Map dataMap) throws IOException { + public static void save(Path targetFile, Map dataMap) throws IOException { var projects = dataMap.keySet().stream().sorted().toList(); var builder = new StringBuilder(); - for (Project project : projects) { + for (GoldStandardProject project : projects) { var projectData = dataMap.get(project); - builder.append("# ").append(project.name()); + builder.append("# ").append(project.getProjectName()); builder.append(LINE_SEPARATOR).append(LINE_SEPARATOR); for (var modelId : projectData.getModelIds()) { diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLPreviousFile.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLPreviousFile.java index af9f1e800..92560ddc2 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLPreviousFile.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLPreviousFile.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files; import java.io.IOException; @@ -19,6 +19,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.TestLink; @@ -40,10 +41,11 @@ private TLPreviousFile() { * @return the previous results * @throws IOException if file access fails */ - public static Collection>> load(Path sourceFile, final Map DATA_MAP) throws IOException { + public static Collection>> load(Path sourceFile, + final Map DATA_MAP) throws IOException { List lines = Files.readAllLines(sourceFile); Map> foundLinkMap = new HashMap<>(); - List>> results = new ArrayList<>(); + List>> results = new ArrayList<>(); for (String line : lines) { var parts = line.split(",", -1); @@ -80,21 +82,22 @@ public static Collection>> load(Path s * @param projectResults results to save * @throws IOException if writing to file system fails */ - public static void save(Path targetFile, Collection>> projectResults, Logger logger) throws IOException { + public static void save(Path targetFile, Collection>> projectResults, Logger logger) + throws IOException { if (Files.exists(targetFile)) { logger.warn("File with the results of the previous evaluation run already exists."); return; // do not overwrite } var sortedResults = new ArrayList<>(projectResults); - sortedResults.sort(Comparator.comparing(x -> x.getOne().name())); + sortedResults.sort(Comparator.comparing(x -> x.getOne().getProjectName())); var builder = new StringBuilder(); - for (Pair> projectResult : sortedResults) { + for (Pair> projectResult : sortedResults) { EvaluationResults result = projectResult.getTwo(); for (TestLink foundLink : result.getFound()) { - builder.append(projectResult.getOne().name()); + builder.append(projectResult.getOne().getProjectName()); builder.append(','); builder.append(foundLink.modelId()); builder.append(','); diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSentenceFile.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSentenceFile.java index dbae4ebdc..0d35a8697 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSentenceFile.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSentenceFile.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files; import java.io.IOException; @@ -11,11 +11,10 @@ import edu.kit.kastel.mcse.ardoco.core.api.output.ArDoCoResult; import edu.kit.kastel.mcse.ardoco.core.api.text.Sentence; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; /** - * This helper-class offers functionality to write out the sentences as seen by ArDoCo after the evaluation runs for TLR - * are done. + * This helper-class offers functionality to write out the sentences as seen by ArDoCo after the evaluation runs for TLR are done. */ public class TLSentenceFile { private static final String LINE_SEPARATOR = System.lineSeparator(); @@ -26,19 +25,19 @@ private TLSentenceFile() { /** * Write out the sentences from the given data map to the target file - * + * * @param targetFile file to write to * @param dataMap data to extract the sentences from * @throws IOException if writing to file system fails */ - public static void save(Path targetFile, Map dataMap) throws IOException { + public static void save(Path targetFile, Map dataMap) throws IOException { var projects = dataMap.keySet().stream().sorted().toList(); var builder = new StringBuilder(); - for (Project project : projects) { + for (GoldStandardProject project : projects) { ImmutableList sentences = dataMap.get(project).getText().getSentences(); - builder.append("# ").append(project.name()); + builder.append("# ").append(project.getProjectName()); builder.append(LINE_SEPARATOR).append(LINE_SEPARATOR); for (Sentence sentence : sentences) { diff --git a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSummaryFile.java b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSummaryFile.java index 6586b35b2..8c3851048 100644 --- a/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSummaryFile.java +++ b/tests/tests-tlr/src/test/java/edu/kit/kastel/mcse/ardoco/core/tests/integration/tlrhelper/files/TLSummaryFile.java @@ -1,4 +1,4 @@ -/* Licensed under MIT 2022-2023. */ +/* Licensed under MIT 2022-2024. */ package edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.files; import java.io.IOException; @@ -18,7 +18,7 @@ import edu.kit.kastel.mcse.ardoco.core.api.text.Text; import edu.kit.kastel.mcse.ardoco.core.common.util.CommonUtilities; import edu.kit.kastel.mcse.ardoco.core.tests.TestUtil; -import edu.kit.kastel.mcse.ardoco.core.tests.eval.Project; +import edu.kit.kastel.mcse.ardoco.core.tests.eval.GoldStandardProject; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.EvaluationResults; import edu.kit.kastel.mcse.ardoco.core.tests.eval.results.calculator.ResultCalculatorUtil; import edu.kit.kastel.mcse.ardoco.core.tests.integration.tlrhelper.TestLink; @@ -42,8 +42,8 @@ private TLSummaryFile() { * @param dataMap the outcomes (data) of the runs * @throws IOException if writing to file system fails */ - public static void save(Path targetFile, Collection>> results, Map dataMap) - throws IOException { + public static void save(Path targetFile, Collection>> results, + Map dataMap) throws IOException { var sortedResults = results.stream().sorted().toList(); var builder = new StringBuilder(); @@ -60,8 +60,8 @@ public static void save(Path targetFile, Collection dataMap, StringBuilder builder, - Pair> projectResult) { + private static void appendProjectResultSummary(Map dataMap, StringBuilder builder, + Pair> projectResult) { var data = dataMap.get(projectResult.getOne()); var text = data.getText(); @@ -76,7 +76,7 @@ private static void appendProjectResultSummary(Map dataMa var falseNegatives = result.falseNegatives(); var falseNegCount = falseNegatives.size(); - builder.append("# ").append(projectResult.getOne().name()); + builder.append("# ").append(projectResult.getOne().getProjectName()); builder.append(LINE_SEPARATOR).append(LINE_SEPARATOR); builder.append("Summary:").append(LINE_SEPARATOR); @@ -96,7 +96,7 @@ private static void appendProjectResultSummary(Map dataMa } } - private static void appendOverallResults(List>> projectResults, StringBuilder builder) { + private static void appendOverallResults(List>> projectResults, StringBuilder builder) { var results = Lists.mutable.ofAll(projectResults.stream().map(Pair::getTwo).toList()); var weightedResults = ResultCalculatorUtil.calculateWeightedAverageResults(results.toImmutable()); var macroResults = ResultCalculatorUtil.calculateAverageResults(results.toImmutable());