350 lines
13 KiB
Nim
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()
|