Electron-Svelte-Recipe-Planner/NimRecipe/ColdFusionlib.nim
Ialbassort 6f36c5fd71 asd
2021-12-16 04:03:57 -05:00

350 lines
13 KiB
Nim

#This is the library used to create recipes
import httpclient, json, sets, strutils, tables, sequtils, nimpy, marshal, re
from NimRecipe import Recipe, Recipeline, fancyamount, htmlconversion
let client = newHttpClient()
let fields = ["carbohydrates", "fat", "protein", "sodium", "sugars", "cholesterol", "fiber"]
const token = readFile("token.txt")
#this stuff is all to organize the stuff we read in json
type packed* = tuple
name : string
makeup : OrderedTable[string, float]
mass : float
type WrappedNode* = object
Node*: JsonNode
type smartVolume = tuple
metric : float
grams : int
type NeutrientMakup* = ref object of RootObj
carbs* : float
fat* : float
protein* : float
sodium* : float
sugars* : float
cholesterol* : float
fiber* : float
#I feel like this will come in handy
#this simply really just makes sure that every hit has the nutrients makes everything much more safe.
#last thing you want is your server to crash on startup.
type Hit* = ref object of NeutrientMakup
name : string
type ReturnedItem* = object
query : string
returned : seq[Hit]
match : float
method getItems*(a : WrappedNode) : JsonNode {.base.} =
doAssert a.Node["hits"]["hits"].len() != 0
return a.Node["hits"]["hits"]
method getNeutrient*(a : Hit) : NeutrientMakup {.base.} =
return NeutrientMakup(carbs : a.carbs, fat: a.fat, protein: a.protein, sodium : a.sodium, sugars : a.sugars, cholesterol: a.cholesterol, fiber : a.fiber)
method tablereturn(a : NeutrientMakup) : OrderedTable[string, float] {.base.} =
return {"carbohydrates" : a.carbs, "fat" : a.fat, "protein" : a.protein, "sodium" : a.sodium, "sugars": a.sugars, "cholesterol": a.cholesterol, "fiber" : a.fiber }.toOrderedTable()
let python = pyImport("nimServerStuff")
proc listOfNouns(name: string) : seq[string] =
return python.listOfNouns(name).to(seq[string])
proc generateReturnedItem(query : string, input : WrappedNode|seq[Hit]) : ReturnedItem =
var conversionlist = initOrderedTable[string, seq[float]]()
#saves operations
var newfloat : seq[float]
for x in fields:
conversionlist[x]=newfloat
when input is seq[Hit]:
return ReturnedItem(query: query, returned: input)
when input is WrappedNode:
var constructingnodes : seq[Hit]
for item in input.getItems():
#temp thing.
if item["_source"]["country"].getStr() != "US":
continue
var nutrients : seq[float]
let name = item["_source"]["name_translations"]["en"].getStr()
for field in fields:
try: nutrients.add(item["_source"]["nutrients"][field]["per_hundred"].getFloat())
except: nutrients.add(0.0)
if nutrients.len != fields.len():
continue
#Todo: move to ordered list...
constructingnodes.add(returnHitDrty(name, nutrients))
#hopefully makes the seq immutable but i dunno.
let staticHitSeq = constructingnodes
return ReturnedItem(query: query, returned: staticHitSeq, match: 0.0)
#pasta because expanding its type definition would have ripples so far im not dealing with it.
proc avg(a : seq[float] | seq[string]) : float =
when a is seq[float]:
return (a.foldl(a+b))/len(a).toFloat()
else:
var newx : seq[float]
for x in a:
newx.add(parseFloat(x))
return (newx.foldl(a+b))/len(newx).toFloat()
proc getAveregenutrients*(a : ReturnedItem) : (NeutrientMakup, string) =
var conversionlist = initOrderedTable[string, seq[float]]()
var newfloat : seq[float]
#creates a seq for every conversion list entry.
for x in fields:
conversionlist[x]=newfloat
#this interesting code...........
for x in a.returned:
var newseq = tablereturn(getNeutrient(x))
for x in fields:
conversionlist[x].add(newseq[x])
for x in conversionlist.pairs:
newfloat.add(avg(x[1]))
return (NeutrientMakup(carbs : newfloat[0], fat: newfloat[1], protein: newfloat[2], sodium: newfloat[3],
sugars: newfloat[4], cholesterol: newfloat[5], fiber : newfloat[6]), a.query)
proc returnHitDrty(name : string, nutrients : seq[float]) : Hit =
return Hit(name: name, carbs : nutrients[0], fat: nutrients[1], protein: nutrients[2],
sodium: nutrients[3], sugars: nutrients[4], cholesterol: nutrients[5], fiber: nutrients[6])
proc recieveNutrients*(input: string) : ReturnedItem =
#this uses foodrepo's DB to query our recipes name, parsing only keywords....
#So, Nim isn't science yet. I need to use python to use native language code.
#Its slow and janky and sucks; but building native lang from the ground up requires an english degree or something.
#That is a SEPARATE project that I personally don't have much interest in doing
let query = listOfNouns(input).join(" ")
client.headers = newHttpHeaders({ "Content-Type": "application/vnd.api+json",
"Authorization" : "Token token="+token})
let body = %*{
"_source": {
"includes": [
"nutrients.protein.per_hundred",
"nutrients.fiber.per_hundred",
"nutrients.sodium.per_hundred",
"nutrients.fat.per_hundred",
"nutrients.cholesterol.per_hundred",
"nutrients.sugars.per_hundred",
"nutrients.carbohydrates.per_hundred",
"name_translations.en",
"country"
]
},
"size": 20,
"query": {
"query_string": {
"fields": [
"name_translations.en"
],
"query": query
}
}, #why? I dunno
}
let response = client.request("https://www.foodrepo.org/api/v3/products/_search", httpMethod = HttpPost, body = $body)
let parse = parseJson(response.body)
if parse["hits"]["hits"].len() == 0:
echo query & " returns an empty hit.........."
return generateReturnedItem(query, WrappedNode(Node: parse))
#This is hand ported from a python port of a C one. I don't know specifically how all of this works.
proc dice_coefficient*(a : string, b : string) : float =
let a = normalize(a)
let b = normalize(b)
if a.len() == 0 or b.len() == 0:
return 0.0
var abigramlist : seq[string]
var bbigramlist : seq[string]
#Nim's slices are a lot different than pythons.
for x in 0..len(a)-2:
abigramlist.add(a[x .. x+1])
for x in 0..len(b)-2:
bbigramlist.add(b[x..x+1])
let aendlist = toHashSet(abigramlist)
let bendlist = toHashSet(bbigramlist)
let overlap = len(intersection(aendlist, bendlist))
return overlap * 2/(len(aendlist) + len(bendlist))
#this doesn't make the most sense, but it allows me to have the broadest type support.
proc highestKey*[T](input : OrderedTable[T, float]): seq[T]=
var highest : float = 0.0
when input is OrderedTable[string, float]:
var returnseq : seq[string]
for tableentry, value in input.pairs:
if value > highest:
highest = value
returnseq.setLen(0)
returnseq.add(tableentry)
if value == highest:
returnseq.add(tableentry)
return returnseq
when input is OrderedTable[int, float]:
var returnseq : seq[int]
for pair in input.pairs:
#if the float associated with the index in pair[0] is larger, it resets the list and then add its
if pair[1] > highest:
highest = pair[1]
returnseq.setLen(0)
returnseq.add(pair[0])
if pair[1] == highest:
returnseq.add(pair[0])
return returnseq
method highestMatches(a : ReturnedItem) : ReturnedItem {.base.} =
#i learned a lot about tables here
#this function took me like 2 hours
#i am furious
var returnlist : seq[Hit]
var ihatetables = initOrderedTable[int, float]()
for x in 0..a.returned.len()-1:
ihatetables[x] = dice_coefficient(a.query, a.returned[x].name)
var key = highestKey(ihatetables)
for x in key:
returnlist.add(a.returned[x])
return ReturnedItem(query: a.query, returned: returnlist)
#proc printNutrient(a : NeutrientMakup, b : string) =
# let table = a.tablereturn
#var sum : seq[float]
#echo b
#for field in fields:
# echo field & ": " & $table[field] & " Per 100 Grams"
# sum.add(table[field])
# echo "total: " & $sum.foldl(a+b)
proc filterNumber(a : string) : seq[string] =
let split = a.split(re"[^0-9]")
var splitparse = filter(split, proc(x: string) : bool = not x.isEmptyOrWhiteSpace)
return splitparse
method toString*(a : fancyamount) : string {.base.} =
if not a.amountnum.contains("Unspecified"):
return a.amountnum & " " & a.measure & ": "
else: return a.amountnum & ": "
#so this cool. We convert volumetric to liters but mass to grams for some reason, i don't know why.
let VolumeMeasure* = {["cup", "cups"] : 0.236588, ["quart", "quarts"] : 0.9463519999972047,
["tablespoons", "tablespoon"] : 0.0147868, ["teaspoon", "teaspoons"] : 0.00492892,
["pint", "pints"] : 0.473176, ["gallon", "gallons"] : 3.785407999988819,
["pound", "pounds"] : 435.592, ["ounce", "ounces"] : 28.3495, ["grams", "gram"] : 1.0}.toOrderedTable()
proc compileFromFile*() : seq[Recipe] =
let outputrecipes = to[seq[Recipe]](readFile("output.json"))
return outputrecipes
proc packArchive*(input : Recipe) : string =
var output : seq[packed]
var rec : OrderedTable[string, float]
var modifier : float
var recFull : (NeutrientMakup, string)
#this code is about to get ugly
#So in order to convert from american !FUN! numbers, we need to get its density conversions.
#we find the item based on its closested match according to the dice coefficent.
let volumeTable = to[OrderedTable[string, smartVolume]](readFile("densityTable.json"))
var blank = {"carbohydrates" : 0.0, "fat" : 0.0, "protein" : 0.0, "sodium" : 0.0, "sugars": 0.0, "cholesterol": 0.0, "fiber" : 0.0}.toOrderedTable()
for lines in input.lines:
let name = lines.ingredients
if name == "<!DOCTYPE html>":
continue
#if it is unspecified we skip a lot of things
if lines.fancy.amountnum.contains("Unspecified"):
rec = blank
output.add((name, rec, 1.0))
continue
else:
#so this fetched the code
try:
let recFull = getAveregenutrients(highestMatches(recieveNutrients(name)))
rec = recFull[0].tablereturn
except:
#i forgot what this does.
raise newException(OutOfMemDefect, "Failed to gAN")
var fancy = lines.fancy
var fancyamount = fancy.amountnum
var fancymeasure = fancy.measure
var volume : float
var amount : float
try:
if fancyamount.contains("/") and fancyamount.contains(" "):
var temp = fancyamount.split(" ")[1].split("/")
amount = (parseInt(temp[0])/parseInt(temp[1])) + parseFloat(fancyamount.split(" ")[0])
elif fancyamount.contains("/") and not fancyamount.contains(" "):
var temp = fancyamount.split("/")
amount = parseInt(temp[0])/parseInt(temp[1])
elif fancyamount.contains "to":
if fancymeasure == "none":
amount = 1
else:
#we do an averege of all ints in the string.
amount = avg(filterNumber(fancyamount))
else:
for x in htmlconversion[].keys:
if fancyamount.contains x:
var temp = htmlconversion[][x].split("/")
var fract = parseFloat(temp[0])/parseFloat(temp[1])
var filtertemp = filterNumber(fancyamount)
if not filtertemp.len() in [0, 1]:
var a = filtertemp[0]
amount = parseFloat(a) + fract
else:
amount = fract
break
else:
amount = parseInt(fancyamount).toFloat
except:
raise newException(OSError, "Failed to parse " & fancyamount & " " & $fancymeasure)
for x in VolumeMeasure.keys:
if x.contains(fancymeasure):
volume = VolumeMeasure[x]
#smartnum is the amount of volume times the amount given
#ie 1.5 cup -> 1.5 * 0.236588 (which is metric for a cup)
var smartnum = volume * amount
#this does the volume specific guessing.
if not ["pounds", "pound", "ounce", "ounces", "grams", "gram"].contains fancy.measure:
#this creates a table of keys and their dice value to the input
var lookuptable = initOrderedTable[string, float]()
for entrys in volumeTable.keys:
lookuptable[entrys] = dice_coefficient(entrys, recFull[1])
#we use this to grab the tuple of data
var volumeref = volumeTable[highestKey(lookuptable)[0]]
var grams = volumeref[1]
var refvolume = volumeref[0]
#we then do some basic math to calculate it to "per 100 grams"
modifier = (grams.toFloat() * smartnum/refvolume) / 100
else:
#we dont need to do any of that if its in mass, because we can just get the grams
modifier = smartnum / 100
if fancymeasure == "none":
modifier = 1
output.add (name, rec, modifier)
return $$output
proc unpackArchive*(input : string) : seq[packed] =
return to[seq[packed]](input)
proc testFunctions() =
var test = unpackArchive(packArchive(compileFromFile()[0]))
for x in test:
echo x[0]
echo x[2]
echo x[1]
echo "\n"
quit()
if isMainModule:
testFunctions()