From d3ebe61577535d0a370e350bb2362c24b5c45b9e Mon Sep 17 00:00:00 2001 From: Sorrel Bri Date: Tue, 3 Mar 2020 01:20:06 -0800 Subject: [PATCH] add latl parse for features --- src/reducers/reducer.features.js | 10 +- src/reducers/reducer.features.test.js | 13 + src/reducers/reducer.latl.js | 171 ++++++++++++- src/reducers/reducer.latl.test.js | 344 +++++++++++++++++++++++++- src/reducers/reducer.results.js | 2 +- 5 files changed, 508 insertions(+), 32 deletions(-) diff --git a/src/reducers/reducer.features.js b/src/reducers/reducer.features.js index 9288cf6..8c0e839 100644 --- a/src/reducers/reducer.features.js +++ b/src/reducers/reducer.features.js @@ -80,9 +80,9 @@ export const addFeature = (state: stateType, action: featureAction): stateType = } export const deleteFeature = (state, action) => { - console.log('deleting') - const deletedFeature = action.value; - delete state.features[deletedFeature]; - console.log(state) - return {...state} + const deletedFeature = state.features[action.value]; + deletedFeature.positive.forEach(phone => delete phone.features[action.value]) + deletedFeature.negative.forEach(phone => delete phone.features[action.value]) + delete state.features[action.value]; + return state } \ No newline at end of file diff --git a/src/reducers/reducer.features.test.js b/src/reducers/reducer.features.test.js index 0e59ddb..8d1cd06 100644 --- a/src/reducers/reducer.features.test.js +++ b/src/reducers/reducer.features.test.js @@ -31,4 +31,17 @@ describe('Features', () => { ); }); + it('feature deletion returns new feature list', () => { + const action = {type: 'DELETE_FEATURE', value: 'occlusive'} + expect(stateReducer(state, action)).toEqual( + {...state, + features: {}, + phones: { + a: {features: {}, grapheme: 'a'}, + n: {features: {}, grapheme: 'n'} + } + } + ) + }) + }); \ No newline at end of file diff --git a/src/reducers/reducer.latl.js b/src/reducers/reducer.latl.js index e783318..073bff9 100644 --- a/src/reducers/reducer.latl.js +++ b/src/reducers/reducer.latl.js @@ -1,3 +1,5 @@ +import { stateReducer } from './reducer'; + export const setLatl = (state, action) => { let latl = action.value; return {...state, latl, parseResults: ''}; @@ -47,6 +49,14 @@ const parseLineBreak = (tree, token, index, tokens) => { return tree; } } + case 'feature--plus': { + // tree[tree.length - 1].type === 'feature'; + return tree; + } + case 'feature--minus': { + // tree[tree.length - 1].type === 'feature'; + return tree; + } default: return tree; } @@ -112,6 +122,32 @@ const parseReferent = (tree, token, index, tokens) => { case 'ruleSet': { return [...tree, { type: 'rule', value: token.value }] } + case 'feature': { + if (!lastNode.value) { + tree[tree.length - 1].value = token.value; + return tree; + } + } + case 'feature--plus': { + if (lastNode.value) { + lastNode.positivePhones = [...lastNode.positivePhones, token.value ] + } + else { + lastNode.value = token.value; + } + tree[tree.length - 1] = lastNode; + return [...tree] + } + case 'feature--minus': { + if (lastNode.value) { + lastNode.negativePhones = [...lastNode.negativePhones, token.value ] + } + else { + lastNode.value = token.value; + } + tree[tree.length - 1] = lastNode; + return [...tree] + } default: return [...tree, `unexpected referent ${token.value}`] } @@ -131,17 +167,26 @@ const parsePhone = (tree, token, index, tokens) => { const parseOpenBracket = (tree, token, index, tokens) => { const lastNode = tree[tree.length - 1]; - switch (lastNode.type) { - case 'epoch': - return [...tree, {type: 'rule', value: token.value}] - case 'rule': - tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value } - return tree; - case 'ruleSet': - return [...tree, {type: 'rule', value: token.value}] - default: - return [...tree, 'unexpected open bracket'] + if (lastNode) { + switch (lastNode.type) { + case 'epoch': + return [...tree, {type: 'rule', value: token.value}] + case 'rule': + tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value } + return tree; + case 'ruleSet': + return [...tree, {type: 'rule', value: token.value}]; + // case 'feature': + // return [{type: 'feature', positivePhones: [], negativePhones: []}]; + case 'feature--plus': + return [...tree, {type: 'feature', positivePhones: [], negativePhones: []}]; + case 'feature--minus': + return [...tree, {type: 'feature', positivePhones: [], negativePhones: []}]; + default: + return [...tree, 'unexpected open bracket'] + } } + return [{type: 'feature', positivePhones: [], negativePhones: []}] } const parseCloseBracket = (tree, token, index, tokens) => { @@ -150,33 +195,83 @@ const parseCloseBracket = (tree, token, index, tokens) => { case 'rule': tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value } return tree; + case 'feature--plus': + return tree; + case 'feature--minus': + return tree; default: return [...tree, 'unexpected close bracket'] } } +const parsePositiveAssignment = (tree, token, index, tokens) => { + const lastNode = tree[tree.length - 1]; + switch (lastNode.type) { + case 'feature': + tree[tree.length - 1].type = 'feature--plus' + return tree; + default: + return [...tree, 'unexpected positive assignment'] + } +} + +const parseNegativeAssignment = (tree, token, index, tokens) => { + const lastNode = tree[tree.length - 1]; + switch (lastNode.type) { + case 'feature': + tree[tree.length - 1].type = 'feature--minus' + return tree; + case 'feature--plus': + tree[tree.length - 1].type = 'feature--minus'; + return tree; + default: + return [...tree, 'unexpected negative assignment'] + } +} + const parsePlus = (tree, token, index, tokens) => { const lastNode = tree[tree.length - 1]; switch (lastNode.type) { case 'rule': tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value} return tree; + case 'feature': + tree[tree.length - 1] = {...lastNode, type: 'feature--plus'} + return tree; + case 'feature--minus': + tree[tree.length - 1] = {...lastNode, type: 'feature--minus'} + return tree; default: return [...tree, 'unexpected plus'] } } - + const parseMinus = (tree, token, index, tokens) => { const lastNode = tree[tree.length - 1]; switch (lastNode.type) { case 'rule': tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value} return tree; + case 'feature': + tree[tree.length - 1] = {...lastNode, type: 'feature--minus'} + return tree; default: return [...tree, 'unexpected minus'] } } +const parseEqual = (tree, token, index, tokens) => { + const lastNode = tree[tree.length - 1]; + switch (lastNode.type) { + case 'feature--plus': + return tree; + case 'feature--minus': + return tree; + default: + return [...tree, 'unexpected equal']; + } +} + const parseGreaterThan = (tree, token, index, tokens) => { const lastNode = tree[tree.length - 1]; switch (lastNode.type) { @@ -194,6 +289,10 @@ const parseSlash = (tree, token, index, tokens) => { case 'rule': tree[tree.length - 1] = {...lastNode, value: lastNode.value + token.value} return tree; + case 'feature--plus': + return tree; + case 'feature--minus': + return tree; default: return [...tree, 'unexpected slash'] } @@ -254,10 +353,16 @@ const generateNode = (tree, token, index, tokens) => { return parseOpenBracket(tree, token, index, tokens); case 'closeBracket': return parseCloseBracket(tree, token, index, tokens); + case 'positiveAssignment': + return parsePositiveAssignment(tree, token, index, tokens); + case 'negativeAssignment': + return parseNegativeAssignment(tree, token, index, tokens); case 'plus': return parsePlus(tree, token, index, tokens); case 'minus': return parseMinus(tree, token, index, tokens); + case 'equal': + return parseEqual(tree, token, index, tokens); case 'greaterThan': return parseGreaterThan(tree, token, index, tokens); case 'slash': @@ -279,7 +384,30 @@ const connectNodes = (tree, node, index, nodes) => { switch (node.type) { case 'epoch': delete node.type; - return {...tree, epochs: [...tree.epochs, {...node, index: tree.epochs.length} ]} + return {...tree, epochs: [...tree.epochs, {...node, index: tree.epochs.length } ] } + case 'feature': + node.feature = node.value; + delete node.value; + delete node.type; + return {...tree, features: [...tree.features, {...node } ] } + case 'feature--minus': + node.feature = node.value; + delete node.value; + delete node.type; + if (tree.features.length && tree.features[tree.features.length - 1].feature === node.feature) { + tree.features[tree.features.length - 1].negativePhones = node.negativePhones + return tree; + } + return {...tree, features: [...tree.features, {...node} ] } + case 'feature--plus': + delete node.type; + node.feature = node.value; + delete node.value; + if (tree.features.length && tree.features[tree.features.length - 1].feature === node.feature) { + tree.features[tree.features.length - 1].positivePhones = node.positivePhones + return tree; + } + return {...tree, features: [...tree.features, {...node} ] } default: return tree; } @@ -288,11 +416,18 @@ const connectNodes = (tree, node, index, nodes) => { export const buildTree = tokens => { const bareTree = { epochs: [], + features: [], + phones: [] } const nodes = tokens.reduce(addToken, []); // return nodes const tree = nodes.reduce(connectNodes, bareTree); - return tree; + const filterProps = Object.entries(tree).filter(([key, value]) => !value.length) + .map(([key, value]) => key) + return filterProps.reduce((tree, badProp) => { + delete tree[badProp]; + return tree; + }, tree); } export const generateAST = latl => { @@ -307,6 +442,16 @@ export const parseLatl = (state, action) => { try { const latl = state.latl; const AST = generateAST(latl); + const features = AST.features; + if (features) { + if (state.features) { + state = Object.keys(state.features).reduce((state, feature) => { + return stateReducer(state, {type: 'DELETE_FEATURE', value: feature}) + }, state) + } + state = features.reduce((state, feature) => stateReducer(state, {type:'ADD_FEATURE', value: feature}), state); + } + delete AST.features; Object.entries(AST).forEach(([key, value]) => state[key] = value); return { ...state, parseResults: 'latl parsed successfully', results:[] } } diff --git a/src/reducers/reducer.latl.test.js b/src/reducers/reducer.latl.test.js index 2ecc76c..40f1083 100644 --- a/src/reducers/reducer.latl.test.js +++ b/src/reducers/reducer.latl.test.js @@ -18,10 +18,10 @@ describe('LATL', () => { expect(tokens).toStrictEqual(tokenizedEpoch) }); - // it('returns tokens from well-formed latl feature definition', () => { - // const tokens = tokenize(featureDefinitionLatl); - // expect(tokens).toStrictEqual(tokenizedFeature); - // }); + it('returns tokens from well-formed latl feature definition', () => { + const tokens = tokenize(featureDefinitionLatl); + expect(tokens).toStrictEqual(tokenizedFeature); + }); // it('returns tokens from well-formed latl lexicon definition', () => { // const tokens = tokenize(lexiconDefinitionLatl); @@ -41,6 +41,22 @@ describe('LATL', () => { expect(tree).toStrictEqual(treeEpoch); }) + it('returns AST from well-formed feature tokens', () => { + const tree = buildTree(tokenizedFeature); + expect(tree).toStrictEqual(treeFeature); + }) + + it('parse returns state from well-formed feature latl', () => { + const state = initState(); + const setAction = { + type: 'SET_LATL', + value: featureDefinitionLatl + } + const latlState = stateReducer(state, setAction); + const parseState = parseLatl(latlState, {}); + expect(parseState).toStrictEqual(featureState) + }) + it('returns run from well-formed epoch latl', () => { const state = initState(); const setAction = { @@ -116,25 +132,327 @@ const epochState = { const featureDefinitionLatl = ` [+ PLOSIVE] = kp/p/b/d/t/g/k [- PLOSIVE] = m/n/s/z -[SONORANT +[SONORANT += m/n -= s/z/kp/p/b/d/t/g/k ] ` const tokenizedFeature = [ - { type: "openBracket", value: "[" }, { type: "plus", value: "+" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "PLOSIVE" }, { type: "closeBracket", value: "]" }, { type: "whiteSpace", value: "" }, - { type: "equal", value: "=" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "kp" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "p" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "b" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "d" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "t" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "g" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "k" }, { type: "whiteSpace", value: "" }, { type: 'lineBreak', value: '' }, - { type: "openBracket", value: "[" }, { type: "minus", value: "-" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "PLOSIVE" }, { type: "closeBracket", value: "]" }, { type: "whiteSpace", value: "" }, + {type: "openBracket", value: "[" }, { type: "plus", value: "+" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "PLOSIVE" }, { type: "closeBracket", value: "]" }, { type: "whiteSpace", value: "" }, + { type: "equal", value: "=" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "kp" }, { type: "slash", value: "/" }, { type: "referent", value: "p" }, { type: "slash", value: "/" }, { type: "referent", value: "b" }, { type: "slash", value: "/" }, { type: "referent", value: "d" }, { type: "slash", value: "/" }, { type: "referent", value: "t" }, { type: "slash", value: "/" }, { type: "referent", value: "g" }, { type: "slash", value: "/" }, { type: "referent", value: "k" }, { type: 'lineBreak', value: '' }, + {type: "openBracket", value: "[" }, { type: "minus", value: "-" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "PLOSIVE" }, { type: "closeBracket", value: "]" }, { type: "whiteSpace", value: "" }, { type: "equal", value: "=" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "m" }, { type: "slash", value: "/" }, { type: "referent", value: "n" }, { type: "slash", value: "/" }, { type: "referent", value: "s" }, { type: "slash", value: "/" }, { type: "referent", value: "z" }, { type: 'lineBreak', value: '' }, - { type: "openBracket", value: "[" }, { type: "referent", value: "SONORANT" }, { type: 'lineBreak', value: '' }, - { type: "whiteSpace", value: "" },{ type: "whiteSpace", value: "" },{ type: "positiveAssignment", value: "+=" }, { type: "whiteSpace", value: "" }, - { type: "referent", value: "m" }, { type: "slash", value: "/" }, { type: "whiteSpace", value: "" }, { type: "referent", value: "n" }, { type: 'lineBreak', value: '' }, - { type: "whiteSpace", value: "" }, { type: "whiteSpace", value: "" },{ type: "negativeAssignment", value: "-=" }, { type: "whiteSpace", value: "" }, - { type: "referent", value: "s" }, { type: "slash", value: "/" }, { type: "referent", value: "z" }, { type: "slash", value: "/" }, { type: "referent", value: "kp" }, { type: "slash", value: "/" }, { type: "referent", value: "p" }, { type: "slash", value: "/" }, { type: "referent", value: "b" }, { type: "slash", value: "/" }, { type: "referent", value: "d" }, { type: "slash", value: "/" }, { type: "referent", value: "t" }, { type: "slash", value: "/" }, { type: "referent", value: "g" }, { type: "slash", value: "/" }, { type: "referent", value: "k" }, { type: "whiteSpace", value: "" },{ type: 'lineBreak', value: '' }, + {type: "openBracket", value: "[" }, { type: "referent", value: "SONORANT" }, { type: 'lineBreak', value: '' }, + { type: "whiteSpace", value: "" }, { type: "positiveAssignment", value: "+=" }, { type: "whiteSpace", value: "" }, + { type: "referent", value: "m" }, { type: "slash", value: "/" }, { type: "referent", value: "n" }, { type: 'lineBreak', value: '' }, + { type: "whiteSpace", value: "" }, { type: "negativeAssignment", value: "-=" }, { type: "whiteSpace", value: "" }, + { type: "referent", value: "s" }, { type: "slash", value: "/" }, { type: "referent", value: "z" }, { type: "slash", value: "/" }, { type: "referent", value: "kp" }, { type: "slash", value: "/" }, { type: "referent", value: "p" }, { type: "slash", value: "/" }, { type: "referent", value: "b" }, { type: "slash", value: "/" }, { type: "referent", value: "d" }, { type: "slash", value: "/" }, { type: "referent", value: "t" }, { type: "slash", value: "/" }, { type: "referent", value: "g" }, { type: "slash", value: "/" }, { type: "referent", value: "k" }, { type: 'lineBreak', value: '' }, { type: "closeBracket", value: "]" }, ] +const treeFeature = { features: [ + { + feature: 'PLOSIVE', + positivePhones: ['kp', 'p', 'b', 'd', 't', 'g', 'k'], + negativePhones: ['m', 'n', 's', 'z'] + }, + { + feature: 'SONORANT', + positivePhones: ['m', 'n'], + negativePhones: ['s' ,'z' ,'kp' ,'p' ,'b' ,'d' ,'t' ,'g' ,'k'] + } +]} + +const featureState = { + ...initState(), + features: { + PLOSIVE: { + negative: [ + { + features: { + PLOSIVE: false, + SONORANT: true, + }, + grapheme: "m", + }, + { + features: { + PLOSIVE: false, + SONORANT: true, + }, + grapheme: "n", + }, + { + features: { + PLOSIVE: false, + SONORANT: false, + }, + grapheme: "s", + }, + { + features: { + PLOSIVE: false, + SONORANT: false, + }, + grapheme: "z", + }, + ], + positive: [ + { + features: { + PLOSIVE: true, + }, + grapheme: "kp", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "p", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "b", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "d", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "t", + ʰ: { + features: {}, + grapheme: "tʰ", + }, + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "g", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "k", + p: { + features: { + SONORANT: false, + }, + grapheme: "kp", + }, + }, + ], +}, + SONORANT: { + negative: [ + { + features: { + PLOSIVE: false, + SONORANT: false, + }, + grapheme: "s", + }, + { + features: { + PLOSIVE: false, + SONORANT: false, + }, + grapheme: "z", + }, + { + features: { + SONORANT: false, + }, + grapheme: "kp", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "p", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "b", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "d", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "t", + ʰ: { + features: {}, + grapheme: "tʰ", + }, + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "g", + }, + { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "k", + p: { + features: { + SONORANT: false, + }, + grapheme: "kp", + }, + }, + ], + positive: [ + { + features: { + PLOSIVE: false, + SONORANT: true, + }, + grapheme: "m", + }, + { + features: { + PLOSIVE: false, + SONORANT: true, + }, + grapheme: "n", + }, + ], +}, }, + parseResults: 'latl parsed successfully', + latl: featureDefinitionLatl, + phones: { + a: { + features: {}, + grapheme: "a", + }, + b: { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "b", + }, + d: { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "d", + }, + g: { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "g", + }, + k: { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "k", + p: { + features: { + SONORANT: false, + }, + grapheme: "kp", + }, + }, + m: { + features: { + PLOSIVE: false, + SONORANT: true, + }, + grapheme: "m", + }, + n: { + features: { + PLOSIVE: false, + SONORANT: true, + }, + grapheme: "n", + }, + p: { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "p", + }, + s: { + features: { + PLOSIVE: false, + SONORANT: false, + }, + grapheme: "s", + }, + t: { + features: { + PLOSIVE: true, + SONORANT: false, + }, + grapheme: "t", + ʰ: { + features: {}, + grapheme: "tʰ", + }, + }, + u: { + features: {}, + grapheme: "u", + }, + z: { + features: { + PLOSIVE: false, + SONORANT: false, + }, + grapheme: "z", + }, + ə: { + features: {}, + grapheme: "ə", + }, + ɯ: { + features: {}, + grapheme: "ɯ", + }, + } +} + const lexiconDefinitionLatl = ` /PROTO kpn diff --git a/src/reducers/reducer.results.js b/src/reducers/reducer.results.js index 73557cd..cb1c659 100644 --- a/src/reducers/reducer.results.js +++ b/src/reducers/reducer.results.js @@ -211,7 +211,7 @@ const transformLexemeInitial = (newLexeme, pre, post, position, phoneme, index, if (!isEnvironmentBoundByRule(lexemeBundle.slice(index + position.length, index + post.length + position.length), post)) return [...newLexeme, phoneme]; const newPhoneme = transformPhoneme(phoneme, newFeatures[0], features); // if deletion occurs - if (!newPhoneme.grapheme) return [ ...newLexeme] ; + if (!newPhoneme || !newPhoneme.grapheme) return [ ...newLexeme] ; return [...newLexeme, newPhoneme]; }