the bee says you are to apply sound changes. submissions may be written in any language.
a sound change is a change in the pronunciation of a language over time. sometimes, linguistics people get bored, make languages, and simulate their evolution. it's like playing with toys for them, except instead of toys they are languages and the way they are playing is by evolving them. when they are slightly less bored, they get a program to do this for them. today you will make that program.
let me show you with somenotation1. the easiest sound changes to apply just shift a sound to another:
f > h
but sound changes can also be conditioned by an environment (the sounds around it), so
ə > ∅ / __#
could mean the elision of word-final ə (# is a common symbol for a word boundary), or even
{p,t,k} > {b,d,g} / V__V
which seems to turn unvoiced plosives into their voiced counterparts when they are between vowels.
there is no fixed syntax for the sound changes; you may parse rules out of an input file, or even hardcode some pleasing ones into your program.
your challenge, given a lexicon, is to evolve the words in it according to a list of sound changes. as every language is allowed, there is no fixed API.
haha I did the bad for accessibility thing you're supposed to not do that they like on TVTropes where you make each word be a different link↩
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingSystem.Linq;usingSystem.Threading.Tasks;usingMicrosoft.EntityFrameworkCore;usingBaum.Phonology;usingBaum.Data;usingBaum.AvaloniaApp.Models;namespaceBaum.AvaloniaApp.Services;classProjectDatabase:IProjectDatabase{FileInfoFile{get;}publicProjectDatabase(FileInfofileInfo)=>File=fileInfo;publicasyncTaskAddAsync(LanguageModellanguageModel){usingvarcontext=newProjectContext(File);awaitcontext.Languages.AddAsync(newLanguage{Name=languageModel.Name,ParentId=languageModel.ParentId,SoundChange=languageModel.SoundChange,});awaitcontext.SaveChangesAsync();}publicasyncTaskUpdateAsync(LanguageModellanguageModel){usingvarcontext=newProjectContext(File);varlanguage=awaitcontext.Languages.FindAsync(languageModel.Id);if(language==null)thrownewInvalidOperationException("No language found in database");language.Name=languageModel.Name;language.SoundChange=languageModel.SoundChange;awaitcontext.SaveChangesAsync();}publicasyncTask<IEnumerable<LanguageModel>>GetChildrenAsync(intlanguageId){usingvarcontext=newProjectContext(File);varentity=awaitcontext.Languages.FindAsync(languageId);if(entity==null)thrownewInvalidOperationException();returnawait(fromlanguageincontext.Languageswherelanguage.ParentId==languageIdselectnewLanguageModel(language.Name,language.ParentId,language.SoundChange){Id=language.Id}).ToArrayAsync();}publicasyncTask<IEnumerable<LanguageModel>>GetLanguagesAsync(){usingvarcontext=newProjectContext(File);returnawaitcontext.Languages.Select(l=>newLanguageModel(l.Name,l.ParentId,l.SoundChange){Id=l.Id}).ToArrayAsync();}asyncTask<WordModel>GetWordAsync(intwordId){usingvarcontext=newProjectContext(File);varword=awaitcontext.Words.FindAsync(wordId);if(word==null)thrownewInvalidOperationException("Word doesn't exist");returnnewWordModel(word.Name,word.IPA){Transient=false,Id=word.Id,AncestorId=word.AncestorId,LanguageId=word.LanguageId,};}publicasyncTask<IEnumerable<WordModel>>GetWordsAsync(intlanguageId,PhonologyDatadata){usingvarcontext=newProjectContext(File);varlanguage=awaitcontext.Languages.FindAsync(languageId);if(language==null)thrownewInvalidOperationException("No language found in database");List<WordModel>words=new();awaitforeach(varwordincontext.Entry(language).Collection(l=>l.Words).Query().AsAsyncEnumerable()){words.Add(newWordModel(word.Name,word.IPA){Transient=false,Id=word.Id,AncestorId=word.AncestorId,LanguageId=word.LanguageId,});}if(language.ParentId!=null){varparentWords=awaitGetWordsAsync((int)language.ParentId,data);foreach(varparentWordinparentWords){SoundChange.TryApply(parentWord.IPA,language.SoundChange,data,outvarIPA);words.Add(newWordModel(parentWord.Name,IPA){Transient=true,LanguageId=languageId,AncestorId=parentWord.Transient?parentWord.AncestorId:parentWord.Id});}}returnwords;}publicasyncTask<IEnumerable<WordModel>>GetAncestryAsync(WordModelword,PhonologyDatadata){usingvarcontext=newProjectContext(File);if(word.AncestorId==null)returnEnumerable.Empty<WordModel>();varancester=awaitcontext.Words.FindAsync(word.AncestorId);if(ancester==null)thrownewInvalidOperationException("Ancestor does not exist");varwordLanguage=awaitcontext.Languages.FindAsync(word.LanguageId);if(wordLanguage==null)thrownewInvalidOperationException("Language does not exist");awaitcontext.Entry(wordLanguage).Reference(l=>l.Parent).LoadAsync();List<Language>languageChain=new(){};while(wordLanguage.Id!=ancester.LanguageId){if(!string.IsNullOrEmpty(wordLanguage.SoundChange))languageChain.Add(wordLanguage);awaitcontext.Entry(wordLanguage).Reference(l=>l.Parent).LoadAsync();wordLanguage=wordLanguage.Parent??thrownewInvalidOperationException("Ancestor is not an ancestor");}List<WordModel>wordChain=new();wordChain.Add(new(ancester.Name,ancester.IPA){Transient=false,AncestorId=ancester.AncestorId,LanguageId=ancester.LanguageId,});foreach(LanguageintermediateinEnumerable.Reverse(languageChain)){varlast=wordChain.Last();SoundChange.TryApply(last.IPA,intermediate.SoundChange,data,outvarnext);// TODO? Possibly do some error handling or notification here instead of just skippingif(last.IPA==next)continue;wordChain.Add(newWordModel(last.Name,next){Transient=true,AncestorId=ancester.Id,LanguageId=intermediate.Id});}returnwordChain;}publicasyncTask<WordModel>AddAsync(WordModelword){usingvarcontext=newProjectContext(File);varlanguage=awaitcontext.Languages.FindAsync(word.LanguageId);if(language==null)thrownewInvalidOperationException("Language doesn't exist");varentry=awaitcontext.Words.AddAsync(newWord{Language=language,Name=word.Name,IPA=word.IPA,AncestorId=word.AncestorId,});awaitcontext.SaveChangesAsync();word.Id=entry.Entity.Id;word.Transient=false;returnword;}publicasyncTaskUpdateAsync(WordModelwordModel){usingvarcontext=newProjectContext(File);varword=awaitcontext.Words.FindAsync(wordModel.Id);if(word==null)thrownewInvalidOperationException("Word doesn't exist");word.Name=wordModel.Name;word.IPA=wordModel.IPA;word.AncestorId=wordModel.AncestorId;awaitcontext.SaveChangesAsync();}publicboolHasMigrations(){usingvarcontext=newProjectContext(File);returncontext.Database.GetMigrations().Any();}publicasyncTaskMigrateAsync(){usingvarcontext=newProjectContext(File);awaitcontext.Database.MigrateAsync();}publicvoidSaveToFile(FileInfofileInfo){if(fileInfo!=File){File.CopyTo(fileInfo.FullName,true);// TODO: Prompt user to confirm overwrite}}}
usingSystem.IO;usingSystem.Reactive;usingSystem.Threading.Tasks;usingAvalonia.Controls;usingAvalonia.ReactiveUI;usingReactiveUI;usingBaum.AvaloniaApp.ViewModels;namespaceBaum.AvaloniaApp.Views;publicpartialclassMainWindow:ReactiveWindow<MainWindowViewModel>{publicMainWindow(){InitializeComponent();this.WhenActivated(d=>{d(ViewModel!.RequestFileInteraction.RegisterHandler(ShowOpenFileDialog));d(ViewModel.RequestSaveFileInteraction.RegisterHandler(ShowSaveFileDialog));d(ViewModel.RequestTemporaryFileInteraction.RegisterHandler(GetTemporaryFile));d(ViewModel.ConfirmMigrationInteraction.RegisterHandler(ShowMigrationConfirmationDialog));});}asyncTaskShowMigrationConfirmationDialog(InteractionContext<Unit,bool>interaction){vardialog=newMigrationConfirmationWindow();dialog.DataContext=newMigrationConfirmationWindowViewModel();varresult=awaitdialog.ShowDialog<bool?>(this);interaction.SetOutput(result??false);}publicvoidGetTemporaryFile(InteractionContext<Unit,FileInfo>interaction){// TODO: I should probably clean up temp files after usage// https://stackoverflow.com/questions/400140/how-do-i-automatically-delete-temp-files-in-cinteraction.SetOutput(newFileInfo(Path.GetTempFileName()));}publicasyncTaskShowOpenFileDialog(InteractionContext<Unit,FileInfo?>interaction){vardialog=newOpenFileDialog{AllowMultiple=false,Filters=new(){newFileDialogFilter{Extensions=new(){"db","baum"}}}};varselections=awaitdialog.ShowAsync(this);if(selectionsis[varfilePath]){interaction.SetOutput(newFileInfo(filePath));}else{interaction.SetOutput(null);}}publicasyncTaskShowSaveFileDialog(InteractionContext<Unit,FileInfo?>interaction){vardialog=newSaveFileDialog{Filters=new(){newFileDialogFilter{Extensions=new(){"baum"}}}};varsaveFile=awaitdialog.ShowAsync(this);if(saveFile!=null){interaction.SetOutput(newFileInfo(saveFile));}else{interaction.SetOutput(null);}}}
<ProjectSdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>WinExe</OutputType><TargetFrameworks>net6.0;net7.0</TargetFrameworks><LangVersion>11.0</LangVersion><Nullable>enable</Nullable><BuiltInComInteropSupport>true</BuiltInComInteropSupport><ApplicationManifest>app.manifest</ApplicationManifest><PublishSingleFile>true</PublishSingleFile></PropertyGroup><ItemGroup><FolderInclude="Models\"/><AvaloniaResourceInclude="Assets\**"/></ItemGroup><ItemGroup><TrimmerRootAssemblyInclude="Avalonia.Themes.Fluent"/></ItemGroup><ItemGroup><PackageReferenceInclude="Avalonia"Version="0.10.19"/><PackageReferenceInclude="Avalonia.Desktop"Version="0.10.19"/><!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--><PackageReferenceCondition="'$(Configuration)' == 'Debug'"Include="Avalonia.Diagnostics"Version="0.10.19"/><PackageReferenceInclude="Avalonia.ReactiveUI"Version="0.10.19"/><PackageReferenceInclude="PolySharp"Version="1.12.1"><IncludeAssets>runtime;build;native;contentfiles;analyzers;buildtransitive</IncludeAssets><PrivateAssets>all</PrivateAssets></PackageReference><PackageReferenceInclude="XamlNameReferenceGenerator"Version="1.6.1"/></ItemGroup><ItemGroup><ProjectReferenceInclude="..\Baum.Phonology\Baum.Phonology.csproj"/><ProjectReferenceInclude="..\Baum.Data\Baum.Data.csproj"/></ItemGroup></Project>
Program.csUnicode text, UTF-8 (with BOM) text, with CRLF line terminators
1 2 3 4 5 6 7 8 910111213141516171819202122
usingAvalonia;usingAvalonia.ReactiveUI;usingSystem;namespaceBaum.AvaloniaApp;classProgram{// Initialization code. Don't use any Avalonia, third-party APIs or any// SynchronizationContext-reliant code before AppMain is called: things aren't initialized// yet and stuff might break.[STAThread]publicstaticvoidMain(string[]args)=>BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);// Avalonia configuration, don't remove; also used by visual designer.publicstaticAppBuilderBuildAvaloniaApp()=>AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace().UseReactiveUI();}
app.manifestUnicode text, UTF-8 (with BOM) text, with CRLF line terminators
1 2 3 4 5 6 7 8 9101112131415161718
<?xml version="1.0" encoding="utf-8"?><assemblymanifestVersion="1.0"xmlns="urn:schemas-microsoft-com:asm.v1"><!-- This manifest is used on Windows only. Don't remove it as it might cause problems with window transparency and embeded controls. For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests --><assemblyIdentityversion="1.0.0.0"name="AvaloniaTest.Desktop"/><compatibilityxmlns="urn:schemas-microsoft-com:compatibility.v1"><application><!-- A list of the Windows versions that this application has been tested on and is designed to work with. Uncomment the appropriate elements and Windows will automatically select the most compatible environment. --><!-- Windows 10 --><supportedOSId="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/></application></compatibility></assembly>
namespaceBaum.Phonology.Notation;publicabstractrecordMatchNode{publicabstractTAccept<T>(IMatchNodeVisitor<T>visitor);}publicrecordFeatureSetMatchNode(IReadOnlySet<Feature>Included,IReadOnlySet<Feature>Excluded):MatchNode{publicoverrideTAccept<T>(IMatchNodeVisitor<T>visitor)=>visitor.Visit(this);}publicrecordSoundMatchNode(IReadOnlySet<Feature>Features):MatchNode{publicoverrideTAccept<T>(IMatchNodeVisitor<T>visitor)=>visitor.Visit(this);}// List is used for enumeration ordering guaranteespublicrecordMatchListNode(List<MatchNode>Nodes):MatchNode{publicoverrideTAccept<T>(IMatchNodeVisitor<T>visitor)=>visitor.Visit(this);}publicrecordEmptyNode:MatchNode{publicoverrideTAccept<T>(IMatchNodeVisitor<T>visitor)=>visitor.Visit(this);}publicrecordEndMatchNode:MatchNode{publicoverrideTAccept<T>(IMatchNodeVisitor<T>visitor)=>visitor.Visit(this);}
usingBaum.Rewrite;namespaceBaum.Phonology.Notation;publicrefstructNotationParser{publicstaticIRewriter<IReadOnlySet<Feature>>Parse(stringsource,PhonologyDatadata)=>newNotationParser(source,data).Parse();IEnumerator<Token>_tokens;bool_isValid;PhonologyData_data;Token?CurrentToken=>_isValid?_tokens.Current:null;publicNotationParser(stringsource,PhonologyDatadata){_data=data;_tokens=newTokenization(source,data).GetEnumerator();Advance();}publicIRewriter<IReadOnlySet<Feature>>Parse(){varmatch=NextMatchNode();Consume<DerivationSymbol>();varreplace=NextMatchNode();varrewriter=replace.Accept(match.Accept(newSoundChangeRewriteParser()));if(_isValid){Consume<Slash>();rewriter=ParseCondition(rewriter);}if(_isValid)thrownewException("Incomplete parse: There are still tokens left, but it couln't be parsed");returnrewriter;}List<MatchNode>NextMatchSequence(){List<MatchNode>matchNodes=new();while(_isValid&&_tokens.CurrentisSoundTokenorOpenBracketorOpenBraceorEndToken){matchNodes.Add(NextMatchNode());}returnmatchNodes;}// MatchNode : IPASymbol// | FeatureSet// | List// | EndMatchNodeNextMatchNode(){switch(CurrentToken){caseSoundToken{Features:varfeatures}:Advance();returnnewSoundMatchNode(features);caseEndToken:Advance();returnnewEndMatchNode();caseOpenBracket:returnNextFeatureSet();caseOpenBrace:returnNextMatchList();default:thrownewNotImplementedException();}}FeatureSetMatchNodeNextFeatureSet(){Advance();HashSet<Feature>included=new(),excluded=new();while(true){if(CurrentTokenisPositiveFeature{Feature:varpositive}){Advance();included.Add(positive);}elseif(CurrentTokenisNegativeFeature{Feature:varnegative}){Advance();excluded.Add(negative);}else{break;}if(CurrentTokenisComma)Advance();}Consume<CloseBracket>();returnnewFeatureSetMatchNode(included,excluded);}MatchNodeNextMatchList(){Consume<OpenBrace>();List<MatchNode>nodes=new();while(true){if(CurrentTokenisSoundTokenorOpenBracketorOpenBraceorEndToken){nodes.Add(NextMatchNode());}else{break;}if(CurrentTokenisComma)Advance();}Consume<CloseBrace>();if(nodes.Any())returnnewMatchListNode(nodes);elsereturnnewEmptyNode();}IRewriter<IReadOnlySet<Feature>>ParseCondition(IRewriter<IReadOnlySet<Feature>>changeRewriter){varrewriter=newSequenceRewriter<IReadOnlySet<Feature>>();foreach(varmatchinNextMatchSequence())rewriter.Add(match.Accept(newSoundMatchRewriteMatchParser()));Consume<Underscore>();rewriter.Add(changeRewriter);foreach(varmatchinNextMatchSequence())rewriter.Add(match.Accept(newSoundMatchRewriteMatchParser()));returnrewriter;}voidConsume<T>()whereT:Token{if(_tokens.CurrentisT){Advance();}else{thrownewInvalidOperationException();}}voidAdvance()=>_isValid=_tokens.MoveNext();}
usingBaum.Rewrite;usingBaum.Phonology.Notation;namespaceBaum.Phonology.Notation;classSoundChangeRewriteParser:IMatchNodeVisitor<IMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>>{publicIMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>Visit(FeatureSetMatchNodematchNode)=>newSoundMatchRewriteParser(featureSet=>featureSet.IsSupersetOf(matchNode.Included)&&!featureSet.Intersect(matchNode.Excluded).Any());publicIMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>Visit(SoundMatchNodematchNode)=>newSoundMatchRewriteParser(featureSet=>featureSet.SetEquals(matchNode.Features));publicIMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>Visit(EmptyNodematchNode)=>newEmptyMatchRewriteParser();publicIMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>Visit(MatchListNodematchNode)=>newMatchListRewriteParser(matchNode.Nodes);publicIMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>Visit(EndMatchNodenode)=>newEndMatchRewriteParser();}classSoundMatchRewriteParser:IMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>{Predicate<IReadOnlySet<Feature>>Match;publicSoundMatchRewriteParser(Predicate<IReadOnlySet<Feature>>match)=>Match=match;publicIRewriter<IReadOnlySet<Feature>>Visit(FeatureSetMatchNodereplaceNode)=>newMatchRewriter<IReadOnlySet<Feature>>(Match,match=>new[]{newHashSet<Feature>(match.Except(replaceNode.Excluded).Union(replaceNode.Included))});publicIRewriter<IReadOnlySet<Feature>>Visit(SoundMatchNodereplaceNode)=>newMatchRewriter<IReadOnlySet<Feature>>(Match,new[]{replaceNode.Features});publicIRewriter<IReadOnlySet<Feature>>Visit(EmptyNodenode)=>newMatchRewriter<IReadOnlySet<Feature>>(Match,Enumerable.Empty<IReadOnlySet<Feature>>());publicIRewriter<IReadOnlySet<Feature>>Visit(MatchListNodereplaceNode)// a > {b,c} makes no sense=>thrownewException("Cannot decide between replacements in list");publicIRewriter<IReadOnlySet<Feature>>Visit(EndMatchNodenode)=>thrownewException("Word terminator is not a valid replacement");}classMatchListRewriteParser:IMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>{List<MatchNode>MatchNodes{get;set;}publicMatchListRewriteParser(List<MatchNode>matchNodes)=>MatchNodes=matchNodes;publicIRewriter<IReadOnlySet<Feature>>Visit(FeatureSetMatchNodereplaceNode)=>newAlternativeRewriter<IReadOnlySet<Feature>>(MatchNodes.Select(matchNode=>matchNode.Accept(newSoundChangeRewriteParser()).Visit(replaceNode)));publicIRewriter<IReadOnlySet<Feature>>Visit(SoundMatchNodereplaceNode)=>newAlternativeRewriter<IReadOnlySet<Feature>>(MatchNodes.Select(matchNode=>matchNode.Accept(newSoundChangeRewriteParser()).Visit(replaceNode)));publicIRewriter<IReadOnlySet<Feature>>Visit(MatchListNodereplaceNode)=>newAlternativeRewriter<IReadOnlySet<Feature>>(Enumerable.Zip(MatchNodes,replaceNode.Nodes).Select(pair=>pair.Second.Accept(pair.First.Accept(newSoundChangeRewriteParser()))));publicIRewriter<IReadOnlySet<Feature>>Visit(EmptyNodereplaceNode)=>newAlternativeRewriter<IReadOnlySet<Feature>>(MatchNodes.Select(matchNode=>matchNode.Accept(newSoundChangeRewriteParser()).Visit(replaceNode)));publicIRewriter<IReadOnlySet<Feature>>Visit(EndMatchNodenode)=>thrownewException("Word terminator is not a valid replacement");}classEndMatchRewriteParser:IMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>{publicIRewriter<IReadOnlySet<Feature>>Visit(FeatureSetMatchNodereplaceNode)// # > [+voiced] makes little sense=>thrownewException("Cannot add or subtract features from the word terminator");publicIRewriter<IReadOnlySet<Feature>>Visit(SoundMatchNodereplaceNode)=>newEndRewriter<IReadOnlySet<Feature>>(new[]{replaceNode.Features});publicIRewriter<IReadOnlySet<Feature>>Visit(EmptyNodereplaceNode)// # > {} also makes no sense=>thrownewException("Cannot replace nothing with nothing");publicIRewriter<IReadOnlySet<Feature>>Visit(MatchListNodenode)=>thrownewException("Cannot decide between replacements in list");publicIRewriter<IReadOnlySet<Feature>>Visit(EndMatchNodenode)=>thrownewException("Word terminator is not a valid replacement");}classEmptyMatchRewriteParser:IMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>{publicIRewriter<IReadOnlySet<Feature>>Visit(FeatureSetMatchNodereplaceNode)// {} > [+voiced] makes little sense=>thrownewException("Cannot add or subtract features from the empty symbol");publicIRewriter<IReadOnlySet<Feature>>Visit(SoundMatchNodereplaceNode)=>newEmptyRewriter<IReadOnlySet<Feature>>(new[]{replaceNode.Features});publicIRewriter<IReadOnlySet<Feature>>Visit(EmptyNodereplaceNode)// {} > {} also makes no sense=>thrownewException("Cannot replace nothing with nothing");publicIRewriter<IReadOnlySet<Feature>>Visit(MatchListNodenode)=>thrownewException("Cannot decide between replacements in list");publicIRewriter<IReadOnlySet<Feature>>Visit(EndMatchNodenode)=>thrownewException("Word terminator is not a valid replacement");}
usingBaum.Rewrite;usingBaum.Phonology.Notation;namespaceBaum.Phonology.Notation;// Only does matching, not replacingclassSoundMatchRewriteMatchParser:IMatchNodeVisitor<IRewriter<IReadOnlySet<Feature>>>{publicIRewriter<IReadOnlySet<Feature>>Visit(FeatureSetMatchNodematchNode)=>newMatchRewriter<IReadOnlySet<Feature>>(featureSet=>featureSet.IsSupersetOf(matchNode.Included)&&!featureSet.Intersect(matchNode.Excluded).Any());publicIRewriter<IReadOnlySet<Feature>>Visit(SoundMatchNodematchNode)=>newMatchRewriter<IReadOnlySet<Feature>>(featureSet=>featureSet.SetEquals(matchNode.Features));publicIRewriter<IReadOnlySet<Feature>>Visit(EmptyNodenode)=>newEmptyRewriter<IReadOnlySet<Feature>>(Enumerable.Empty<IReadOnlySet<Feature>>());publicIRewriter<IReadOnlySet<Feature>>Visit(MatchListNodenode)=>newAlternativeRewriter<IReadOnlySet<Feature>>(node.Nodes.Select(node=>node.Accept(this)));publicIRewriter<IReadOnlySet<Feature>>Visit(EndMatchNodenode)=>newEndRewriter<IReadOnlySet<Feature>>(Enumerable.Empty<IReadOnlySet<Feature>>());}
Tokenizer.csASCII text, with CRLF line terminators
namespaceBaum.Phonology;publicclassPhonologyData{IEnumerable<Sound>_sounds;publicPhonologyData(IEnumerable<Sound>sounds)=>_sounds=sounds;// TODO? Should this just return null?publicSoundGetStartSound(stringsymbol)=>_sounds.Where(sound=>symbol.StartsWith(sound.Symbol)).MaxBy(sound=>sound.Symbol.Length)??thrownewException($"There is no sound for {symbol}");publicSoundGetSound(IEnumerable<Feature>features)=>Enumerable.Single(_sounds.Where(sound=>sound.Features.SetEquals(features)));publicIReadOnlySet<Sound>GetSounds(IReadOnlySet<Feature>includedFeatures,IReadOnlySet<Feature>excludedFeatures)=>newHashSet<Sound>(_sounds.Where(sound=>includedFeatures.IsSubsetOf(sound.Features)&&!excludedFeatures.Intersect(sound.Features).Any()));}
usingBaum.Rewrite;usingBaum.Phonology.Notation;namespaceBaum.Phonology;publicclassSoundChange{publicstaticboolTryApply(stringinitial,stringrule,PhonologyDatadata,outstringafter){try{varrewriter=NotationParser.Parse(rule,data);varchange=newSoundChange{PhonologyData=data,Rewriter=rewriter};after=change.Apply(initial);returntrue;}catch(Exception)// TODO: Specialize exception{after=initial;returnfalse;}}publicrequiredIRewriter<IReadOnlySet<Feature>>Rewriter{get;set;}publicrequiredPhonologyDataPhonologyData{get;set;}stringApply(stringstr){varfeatureString=newTokenization(str,PhonologyData).Select(sound=>((SoundToken)sound).Features);// TODO: Not quite sure if this algorithm is actually the best way to do this// Replaces every match in the stringintmaxLength=featureString.Count()*3;for(intpos=0;pos<featureString.Count();++pos){// Prevents infinite insertions like {} > p / p_ where the Count keeps growingif(pos>maxLength)thrownewException("Word tripled in length, triggering infinite loop protection");varrewrites=Rewriter.Rewrite(featureString,pos);if(rewrites.Any()){varreplacement=rewrites.MaxBy(pair=>pair.RewritePosition);featureString=featureString.Take(pos).Concat(replacement.Rewrite).Concat(featureString.Skip(replacement.RewritePosition));}}returnstring.Concat(featureString.Select(f=>PhonologyData.GetSound(f).Symbol));}}
namespaceBaum.Phonology.Tests;publicclassFeatureSetTest{PhonologyDatastubData=new(new[]{newSound("a",newHashSet<Feature>(){new("vowel"),new("open")}),newSound("e",newHashSet<Feature>(){new("vowel"),new("close-mid")}),newSound("m",newHashSet<Feature>(){new("consonant"),new("nasal")}),newSound("p",newHashSet<Feature>(){new("consonant"),new("plosive")}),newSound("b",newHashSet<Feature>(){new("consonant"),new("plosive"),new("voiced")}),});// p > [+voice]// pat > bat// p > [+voice] / _a// pat > bat, put > put// Or conditions// Syllable/word boundary conditions}
NotationTest.csUnicode text, UTF-8 text, with CRLF line terminators
usingSystem.Collections;usingSystem.Diagnostics.CodeAnalysis;usingSystem.Linq;namespaceBaum.Rewrite;publicclassSequenceRewriter<T>:IRewriter<T>,IEnumerable<IRewriter<T>>{List<IRewriter<T>>Rewriters=new();#region IRewriter<T>// TODO: Consider: If Rewriters is empty, should it throw, return an empty result, or return an unchanged resultpublicIEnumerable<RewritePair<T>>Rewrite(IEnumerable<T>sequence,intstartPosition){varrewritePairs=Rewriters.First().Rewrite(sequence,startPosition);foreach(varrewriterinRewriters.Skip(1)){rewritePairs=rewritePairs.SelectMany(pair=>rewriter.Rewrite(sequence,pair.RewritePosition).Select(nextPair=>newRewritePair<T>{Rewrite=pair.Rewrite.Concat(nextPair.Rewrite),RewritePosition=nextPair.RewritePosition}));}returnrewritePairs;}#endregion#region IEnumerable<IRewriter<T>>publicIEnumerator<IRewriter<T>>GetEnumerator(){thrownewNotImplementedException();}IEnumeratorIEnumerable.GetEnumerator(){thrownewNotImplementedException();}#endregionpublicvoidAdd(IRewriter<T>rewriter)=>Rewriters.Add(rewriter);}
post a comment