compactAllIris method

IriCompactionResult compactAllIris(
  1. RdfGraph graph,
  2. Map<String, String> customPrefixes, {
  3. String? baseUri,
})

Implementation

IriCompactionResult compactAllIris(
  RdfGraph graph,
  Map<String, String> customPrefixes, {
  String? baseUri,
}) {
  final prefixCandidates = {
    ..._namespaceMappings.asMap(),
  };
  prefixCandidates
      .removeWhere((key, value) => customPrefixes.values.contains(value));
  prefixCandidates.addAll(customPrefixes);

  final usedPrefixes = <String, String>{};
  // Indexed by IriRole.index (0..IriRole.values.length-1) to avoid record-tuple
  // allocations — each map lookup used to create a (IriTerm, IriRole) heap object.
  final compactIris = <IriTerm, List<CompactIri?>>{};
  // Create an inverted index for quick lookup
  final iriToPrefixMap = {
    for (final e in prefixCandidates.entries) e.value: e.key
  };
  if (iriToPrefixMap.length != prefixCandidates.length) {
    throw ArgumentError(
      'Duplicate namespace URIs found in prefix candidates: $prefixCandidates',
    );
  }

  // Snapshot the original prefix keys before the main loop so the
  // post-processing step can distinguish user- / stdlib-provided prefixes
  // from generated ones without inspecting the mappings object again.
  final originalCandidateKeys = Set<String>.from(prefixCandidates.keys);

  // Iterate over triples directly — no intermediate list, no record-tuple
  // allocations. Each unique (IriTerm, IriRole) pair is processed exactly once;
  // duplicates are skipped via the per-term slot list.
  // Tracks the next free index per generated base-prefix (e.g. 'mo' → 3 means
  // 'mo1' and 'mo2' are taken). Passed to getOrGeneratePrefix so the
  // numbered-suffix search is O(1) amortized instead of O(N²).
  final prefixCounters = <String, int>{};
  void processIri(IriTerm iri, IriRole role) {
    final slots = compactIris[iri];
    if (slots != null && slots[role.index] != null) {
      return; // already resolved
    }
    final compacted = compactIri(
        iri, role, baseUri, iriToPrefixMap, prefixCandidates, customPrefixes,
        prefixCounters: prefixCounters);
    final dest =
        slots ?? List<CompactIri?>.filled(IriRole.values.length, null);
    if (slots == null) compactIris[iri] = dest;
    dest[role.index] = compacted;
    if (compacted
        case PrefixedIri(
          prefix: var prefix,
          namespace: var namespace,
        )) {
      // Add the prefix to all relevant maps
      usedPrefixes[prefix] = namespace;
      final oldNamespace = prefixCandidates[prefix];
      final oldPrefix = iriToPrefixMap[namespace];
      if (oldNamespace != null && oldNamespace != namespace) {
        throw ArgumentError(
          'Namespace conflict for prefix "$prefix": '
          'already mapped to "$oldNamespace", cannot map to "$namespace".',
        );
      }
      if (oldPrefix != null && oldPrefix != prefix) {
        throw ArgumentError(
          'Prefix conflict for namespace "$namespace": '
          'already mapped to "$oldPrefix", cannot map to "$prefix".',
        );
      }
      // Update candidates with new prefix
      prefixCandidates[prefix] = namespace;
      iriToPrefixMap[namespace] = prefix; // Update inverse mapping
    }
  }

  for (final triple in graph.triples) {
    if (triple.subject is IriTerm) {
      processIri(triple.subject as IriTerm, IriRole.subject);
    }
    if (triple.predicate is IriTerm) {
      processIri(triple.predicate as IriTerm, IriRole.predicate);
    }
    if (triple.object is IriTerm) {
      processIri(
        triple.object as IriTerm,
        triple.predicate == Rdf.type ? IriRole.type : IriRole.object,
      );
    } else if (triple.object is LiteralTerm) {
      processIri((triple.object as LiteralTerm).datatype, IriRole.datatype);
    }
  }

  // Post-processing: deterministic generated-prefix numbering.
  //
  // Generated prefix numbers (ns1, ns2, …) would otherwise depend on the
  // order in which namespaces are first encountered during triple iteration.
  // Because triple order can vary across decode→encode round-trips, we
  // re-number every generated prefix by sorting its namespace URI
  // alphabetically within its base-prefix group (e.g. all "ns*" entries),
  // producing stable, order-independent output.
  {
    final generated = usedPrefixes.entries
        .where((e) => !originalCandidateKeys.contains(e.key))
        .toList();

    if (generated.isNotEmpty) {
      // Group by base prefix (strip trailing digits).
      // e.g. "ns42" → "ns", "mo1" → "mo".
      final digitSuffix = RegExp(r'\d+$');
      final groups = <String, List<(String prefix, String namespace)>>{};
      for (final e in generated) {
        final base = e.key.replaceFirst(digitSuffix, '');
        (groups[base] ??= []).add((e.key, e.value));
      }

      final renaming = <String, String>{}; // oldPrefix → newPrefix
      for (final MapEntry(key: base, value: pairs) in groups.entries) {
        // Single-member groups are already stable: only one namespace competed
        // for this base prefix, so its assigned number (or lack thereof) is
        // fully determined by _tryGeneratePrefixFromUrl and is independent of
        // triple encounter order.
        if (pairs.length <= 1) continue;
        // Sort by namespace URI — makes numbering input-order-independent.
        pairs.sort((a, b) => a.$2.compareTo(b.$2));
        var num = 1;
        for (final (oldPrefix, _) in pairs) {
          // Skip indices that collide with a pre-existing non-generated
          // candidate so those prefixes are never shadowed.
          while (originalCandidateKeys.contains('$base$num')) {
            num++;
          }
          final newPrefix = '$base$num';
          num++;
          if (newPrefix != oldPrefix) renaming[oldPrefix] = newPrefix;
        }
      }

      if (renaming.isNotEmpty) {
        // Rebuild usedPrefixes, prefixCandidates, and iriToPrefixMap.
        // Renaming may form cycles (e.g. ns2↔ns8), so collect all the
        // namespace values BEFORE modifying the maps to avoid overwriting
        // an entry that another renaming still needs to read.
        final toAdd = <String, String>{}; // newPrefix → namespace
        for (final MapEntry(key: old, value: neo) in renaming.entries) {
          final ns = usedPrefixes.remove(old)!;
          prefixCandidates.remove(old);
          iriToPrefixMap[ns] = neo;
          toAdd[neo] = ns;
        }
        usedPrefixes.addAll(toAdd);
        prefixCandidates.addAll(toAdd);
        // Update every compactIris slot that references a renamed prefix.
        for (final slots in compactIris.values) {
          for (int i = 0; i < slots.length; i++) {
            final slot = slots[i];
            if (slot is PrefixedIri) {
              final neo = renaming[slot.prefix];
              if (neo != null) {
                slots[i] = PrefixedIri(neo, slot.namespace, slot.localPart);
              }
            }
          }
        }
      }
    }
  }

  return IriCompactionResult(
      prefixes: usedPrefixes, compactIris: compactIris);
}