使用 Groovy 管理非营利组织的供应链

让我们使用 Groovy 来解决慈善机构的配送问题。
77 位读者喜欢这篇文章。
Secret ingredient in open source

Opensource.com

我非常喜欢 Java 有很多原因,但也许最重要的是,它设计中独特的静态类型和面向对象性的结合。然而,当需要快速解决方案时,特别是处理数据的“解决后即可忘记”的问题时,我通常会选择 Groovy(或有时是 Python),尤其是当解决我问题的库存在且文档完善时。有时甚至 awk 也可以。但我一直想开始更多地使用 Julia,还有 Go

我时不时会遇到不同类型的问题,当问题足够简洁时,有时我会用几种语言来解决它,只是为了更多地了解每种语言如何解决问题。

最近,一位不懂编程的同事向我介绍了一个这样的问题。问题是这样的:

XYZ 社区中的许多人每天都在努力维持生计。社区的就业机会有限,而且往往是低薪的。生活成本相对较高:水、电和医疗保健都很昂贵。高等教育,无论是学术性的还是技术性的,都意味着要搬到最近的城市。从好的方面来说,社区很小而且关系紧密。人们在各自的经济条件下尽可能地互相帮助。

COVID-19 在经济上严重打击了这个社区。尽管目前还没有感染病例,但镇上两家主要的雇主正面临 финансовые ruin,几乎解雇了所有工人。政府已经提供了帮助,但帮助的金额对于那些挣扎最严重的家庭来说还不够。

一家全国性慈善机构的当地分会在获得了一些资金,用于向有需要的家庭提供支持。为了尽可能地利用这笔资金,该慈善机构安排购买大批量的食品和家庭用品,然后将这些大批量物品分成价值大致相等的家庭包。他们的问题是,如何做到这一点?

我的同事认为也许我可以帮他制作一个电子表格来处理分配问题。然而,对我来说,这似乎是一个完美的小问题,可以用一个小程序来解决。步骤可能是什么?

  1. 将大包装拆开成单个单元。
  2. 当还有单元剩余时
    1. 拿一个新的包裹。
    2. 将包裹价值设置为零。
    3. 当包裹价值低于理想包裹价值且还有单元剩余时
      1. 随机挑选一个单元。
      2. 如果该单元不在包裹中,并且如果添加它后包裹价值不会太高
        1. 将单元移动到包裹中。
        2. 将包裹价值增加单元价格。

这看起来是一个不错的初步方案。它也似乎是一个非常适合用 Groovy 实现的小算法。

Groovy 解决方案

在 Java 中,我发现自己声明实用程序类来保存数据元组(新的 record 功能在这方面会很棒)。在 Groovy 中,我倾向于使用语言对 map 的支持。让我们使用 map 列表来保存从批发商处购买的大宗商品

def packs = [
    [item:'Rice',brand:'Best Family',units:10,price:5650,quantity:1],
    [item:'Spaghetti',brand:'Best Family',units:1,price:327,quantity:10],
    [item:'Sardines',brand:'Fresh Caught',units:3,price:2727,quantity:3],
    [item:'Chickpeas',brand:'Southern Style',units:2,price:2600,quantity:5],
    [item:'Lentils',brand:'Southern Style',units:2,price:2378,quantity:5],
    [item:'Vegetable oil',brand:'Crafco',units:12,price:10020,quantity:1],
    [item:'UHT milk',brand:'Atlantic',units:6,price:4560,quantity:2],
    [item:'Flour',brand:'Neighbor Mills',units:10,price:5200,quantity:1],
    [item:'Tomato sauce',brand:'Best Family',units:1,price:190,quantity:10],
    [item:'Sugar',brand:'Good Price',units:1,price:565,quantity:10],
    [item:'Tea',brand:'Superior',units:5,price:2720,quantity:2],
    [item:'Coffee',brand:'Colombia Select',units:2,price:4180,quantity:5],
    [item:'Tofu',brand:'Gourmet Choice',units:1,price:1580,quantity:10],
    [item:'Bleach',brand:'Blanchite',units:5,price:3550,quantity:2],
    [item:'Soap',brand:'Sunny Day',units:6,price:1794,quantity:2]]

这里有一大包 10 袋米和 10 大包,每包一袋意大利面。在上面,变量 packs 被设置为 map 列表(实际上是底层的 Java ArrayList) (实际上是底层的 Java HashMap)。由于 Groovy 是动态类型的(默认情况下是这样),我使用 def 来声明 packs 变量,并且很高兴在我的 map 中同时拥有 StringInteger 值。

是的,这些价格看起来确实有点奇怪,但这个问题发生在货币不同的地方。

下一步是解包这些大包装。解包单件大包装的米会产生 10 个单位的米;也就是说,产生的单元总数是 units * quantity。Groovy 提供了一个名为 collectMany 的便捷函数,可以用来展平列表的列表,因此执行解包的代码非常简单

def units = packs.collectMany { pack -> 
    [[item:pack.item, brand:pack.brand, price:(pack.price / pack.units)]] *
		(pack.units * pack.quantity)
}

请注意,collectMany 接受 Closure 作为其参数;所以这是一种本地声明的函数,带有一个参数 pack,它返回一个包含 (units * quantity) 个 map 的列表,每个 map 都包含来自相应大包装的商品、品牌和计算出的单价。这里值得注意的是,Groovy 乘法运算符 (*) 左侧是列表,右侧是数字 (N) 将生成一个列表,其中原始项目按顺序复制 N 次。

最后一步是将单元重新包装到包裹中以进行分发。但首先,我需要更具体地了解理想的包裹价值,并且当只剩下少量单元时,我最好不要过于限制

def valueIdeal = 5000
def valueMax = valueIdeal * 1.1

好的!让我们重新包装包裹

def rnd = new Random()
def hamperNumber = 0    // [1]

while (units.size()) {  // [2]
    hamperNumber++
    def hamper = []
    def value = 0       // [2.1]
    for (boolean canAdd = true; canAdd; ) {        // [2.2]
        int u = rnd.nextInt(units.size())          // [2.2.1]
        canAdd = false                             // [2.2.2]
        for (int o = 0; o < units.size(); o++) {   // [2.2.3]
            int uo = (u + o) % units.size()
            def unit = units[uo]                   // [2.2.3.1]
            if (units.size() < 3 ||
			!(unit in hamper) &&
			(value + unit.price) < valueMax) { // [2.2.3.2]
                hamper.add(unit)
                value += unit.price
                units.remove(uo)                   // [2.2.3.3]
                canAdd = units.size() > 0
                break                              // [2.2.3.4]
            }
        }                                          // [2.2.4]
    }
    println ""
    println "Hamper $hamperNumber value $value:"
    hamper.each { item ->
        printf "%-25s%-25s%7.2f\n",item.item,item.brand,item.price
    }                                                                   // [2.3]
    println "Remaining units ${units.size()} average price = $avgPrice" // [2.4]
}

一些澄清,注释中带有括号的数字(例如,[1])对应于下面的澄清

  • 1. 初始化 Groovy 的随机数生成器和包裹编号。
  • 2. 只要有更多可用单元,此 while {} 循环就会将单元重新分配到包裹中
    • 2.1 递增包裹编号,获取一个新的空包裹(单元列表),并将其价值设置为 0。
    • 2.2 此 for {} 循环将尽可能多地将单元添加到包裹中
      • 2.2.1 获取一个介于零和剩余单元数减 1 之间的随机数。
      • 2.2.2 假设您找不到更多要添加的单元。
      • 2.2.3 此 for {} 循环,从随机选择的索引开始,将尝试查找可以添加到包裹中的单元。
        • 2.2.3.1 确定要查看哪个单元。
        • 2.2.3.2 如果只剩下少量单元,或者如果添加该单元后包裹的价值不会太高,则将此单元添加到包裹中。
        • 2.2.3.3 将单元添加到包裹中,将包裹价值增加单元价格,并从可用单元列表中删除该单元。
        • 2.2.3.4 只要还有单元剩余,您就可以添加更多,因此跳出此循环以继续查找。
      • 2.2.4 从此 for {} 循环退出时,如果您检查了每个剩余单元并且找不到一个可以添加到包裹中的单元,则包裹已完成;否则,您找到了一个并且可以继续查找更多。
    • 2.3 打印包裹的内容。
    • 2.4 打印剩余单元信息。

当您运行此代码时,输出看起来像

Hamper 1 value 5414:
Vegetable oil            Crafco                    835.00
Coffee                   Colombia Select          2090.00
Tofu                     Gourmet Choice           1580.00
Sardines                 Fresh Caught              909.00
Remaing units 151

Hamper 2 value 5309:
Flour                    Neighbor Mills            520.00
Sugar                    Good Price                565.00
Vegetable oil            Crafco                    835.00
Coffee                   Colombia Select          2090.00
Rice                     Best Family               565.00
Tomato sauce             Best Family               190.00
Tea                      Superior                  544.00
Remaing units 144

Hamper 3 value 5395:
Flour                    Neighbor Mills            520.00
UHT milk                 Atlantic                  760.00
Tomato sauce             Best Family               190.00
Tofu                     Gourmet Choice           1580.00
Spaghetti                Best Family               327.00
Sugar                    Good Price                565.00
Sardines                 Fresh Caught              909.00
Tea                      Superior                  544.00
Remaing units 136

…

Hamper 23 value 5148:
Flour                    Neighbor Mills            520.00
Tea                      Superior                  544.00
Chickpeas                Southern Style           1300.00
Lentils                  Southern Style           1189.00
Vegetable oil            Crafco                    835.00
UHT milk                 Atlantic                  760.00
Remaing units 3

Hamper 24 value 3955:
Chickpeas                Southern Style           1300.00
Sugar                    Good Price                565.00
Coffee                   Colombia Select          2090.00
Remaing units 0

最后一个包裹的内容和价值被缩写了。

结束语

请注意,关于是否能够将单元添加到包裹中,有一些繁琐的事情。基本上,您在单元列表中选择一个随机位置,并从该位置开始遍历列表,直到找到一个价格允许将其包含在内的单元,或者直到您耗尽列表为止。此外,当只剩下少量物品时,您只需将它们扔到最后一个包裹中。

另一个值得一提的问题:这不是一种特别有效的方法。从 ArrayLists 中删除元素,让 Groovy 使用其默认的 BigDecimal,以及其他一些因素使得这不太适合处理大型重新分配问题。尽管如此,它在我的老旧双核机器上运行速度仍然很快。

最后一个想法——使用 while { … }for { … }?真的吗?不是一些很酷的函数式代码?恐怕是这样。我无法想到一种在 Groovy 中使用 map 和 reduce 风格的闭包与随机选择单元进行重新包装协同工作的方法。你能想到吗?

在另一篇文章中,我将用 Python 解决这个问题,未来的文章将用 Java、Julia 和 Go 来解决。

接下来阅读什么
标签
Chris Hermansen portrait Temuco Chile
自从 1978 年毕业于不列颠哥伦比亚大学以来,我几乎总是在使用某种计算机。自 2005 年以来,我一直是全职 Linux 用户,从 1986 年到 2005 年一直是全职 Solaris 和 SunOS 用户,在那之前是 UNIX System V 用户。

评论已关闭。

© 2025 open-source.net.cn. All rights reserved.