Perl how to access a value of nested hash without specific some middle keys (wildcard) - perl

I trying to fetch the facebook mobile posts.
$VAR1 = {
"mf_story_key" => "225164133113094",
"page_id" => "102820022014173",
"page_insights" => {
"102820022014173" => {
"actor_id" => "102820022014173",
"page_id" => "102820022014173",
"page_id_type" => "page",
"post_context" => {
"publish_time" => "1641702174",
"story_name" => "EntStatusCreationStory"
},
"psn" => "EntStatusCreationStory",
"role" => 1,
}
},
"story_attachment_style" => "album",
};
$publish_time = $VAR1->{page_insights}{102820022014173}{post_context}{publish_time};
If 102820022014173 is a dynamic value, how do I access the publish_time value without specific it?

You need to get the keys to the page_insights hash and then iterate through them.
use strict;
use warnings;
use 5.010;
my $post = {
"mf_story_key" => "225164133113094",
"page_id" => "102820022014173",
"page_insights" => {
"102820022014173" => {
"actor_id" => "102820022014173",
"page_id" => "102820022014173",
"page_id_type" => "page",
"post_context" => {
"publish_time" => "1641702174",
"story_name" => "EntStatusCreationStory"
},
"psn" => "EntStatusCreationStory",
"role" => 1,
}
},
"story_attachment_style" => "album",
};
my $insights = $post->{page_insights};
my #insight_ids = keys %{$insights};
for my $id ( #insight_ids ) {
say "ID $id was published at ",
$insights->{$id}{post_context}{publish_time};
}
gives
ID 102820022014173 was published at 1641702174

for my $page_insight ( values( %{ $VAR1->{page_insights} } ) ) {
my $publish_time = $page_insight->{post_context}{publish_time};
...
}
If there's always going to exactly one element,
my $page_insight = ( values( %{ $VAR1->{page_insights} } )[0];
my $publish_time = $page_insight->{post_context}{publish_time};
...
(You can combine the two statements if you so desire.)

Related

is it possible to return different models in an API response

This is my first time posting here so please pardon my errors:
I have a search functionality whose route is:
Route::get('/search', 'SearchController#index');
Currently, I have an eloquent relationship where products has many deals. is it possible to return a single level deep array doing the following:
If the product has an active deal, return the deal only;
Otherwise, return the product itself.
here's what I earlier implemented in my Product.php:
public function deals()
{
return $this->hasMany(Deal::class, 'product_id');
}
Deal.php
public function product()
{
return $this->hasOne(Product::class, 'id', 'product_id');
}
SearchController:
public function index(Request $request)
{
$per_page = $request->per_page ?? 10;
$products = Product::query()->latest()
->when($request->query('filter'), function ($query) use ($request) {
$query->with('deals')->where('title', 'LIKE', "%$request->filter%");
})
->when($request->query('category'), function ($query) use ($request) {
$query->with('deals')->whereHas('categories', function ($q) use ($request) {
$q->where('title', 'LIKE', "%$request->category%");
});
})
->paginate($per_page);
return new PaginatedCollection($products, ProductResource::class);
}
and in my ProductResource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray($request)
{
$details = array_filter($this->details ?: [], function ($d) {
return $d != "";
});
$personalizedOptions = array_filter($this->personalized_options ?: [], function ($o) {
return $o != "";
});
return [
'id' => $this->id,
'createdAt' => $this->created_at,
'updatedAt' => $this->updated_at,
'title' => $this->title,
'sellerId' => $this->sellerId,
'description' => $this->description,
'categories' => CategoryResource::collection($this->categories),
'details' => $details,
'active' => (bool) $this->active,
'defaultPreviewImageId' => $this->default_preview_image_id,
'originalPrice' => $this->originalPrice,
'shippingPrice' => $this->shippingPrice,
'shippingWeightLbs' => $this->shippingWeightLbs,
'shippingWeightOz' => $this->shippingWeightOz,
'shippingMaxDays' => $this->shipping_max_days,
'shippingMinDays' => $this->shipping_min_days,
'personalized' => (bool) $this->personalized,
'personalizedOptions' => $personalizedOptions,
'deals' => $this->deals ?? null,
'options' => ProductOptionResource::collection($this->productOptions),
'images' => ImageResource::collection($this->images->whereNull('meta')),
'preview' => new ImageResource($this->images->where('meta', '=', 'preview')->first()),
];
}
}
Now, I have refactored the ProductResource to this but it's all returning null response
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
public function toArray($request)
{
$details = array_filter($this->details ?: [], function ($d) {
return $d != "";
});
$personalizedOptions = array_filter($this->personalized_options ?: [], function ($o) {
return $o != "";
});
if($this->deals){
DealResource::collection($this->deals);
}else{
return [
'id' => $this->id,
'createdAt' => $this->created_at,
'updatedAt' => $this->updated_at,
'title' => $this->title,
'sellerId' => $this->sellerId,
'description' => $this->description,
'categories' => CategoryResource::collection($this->categories),
'details' => $details,
'active' => (bool) $this->active,
'defaultPreviewImageId' => $this->default_preview_image_id,
'originalPrice' => $this->originalPrice,
'shippingPrice' => $this->shippingPrice,
'shippingWeightLbs' => $this->shippingWeightLbs,
'shippingWeightOz' => $this->shippingWeightOz,
'shippingMaxDays' => $this->shipping_max_days,
'shippingMinDays' => $this->shipping_min_days,
'personalized' => (bool) $this->personalized,
'personalizedOptions' => $personalizedOptions,
// 'deals' => $this->deals ?? null,
'options' => ProductOptionResource::collection($this->productOptions),
'images' => ImageResource::collection($this->images->whereNull('meta')),
'preview' => new ImageResource($this->images->where('meta', '=', 'preview')->first()),
];
}
}
}
The reason why it may be giving the null result because of the condition check. it is returning an array you need to update it to this.
if(count($this->deals))
this will check if the deal array contains an element in the array. if not it will return products.

Optimal sampling from the database

I make a sample around the city, the city has areas, they have complexes, and apartment complexes. In the end, I need to get an apartment for a certain city, indicating the associated filters (district, complex, etc.). At the moment I did this:
public function actionIndex()
{
$cities = Cities::find()->where(['id' => Yii::$app->request->get('city_id')])->with([
'districts' => function ($query){
$query->filterWhere([
'id' => Yii::$app->request->get('district_id'),
]);
},
'districts.complexes' => function ($query) {
$query->filterWhere([
'id' => Yii::$app->request->get('complex_id'),
'type_id' => Yii::$app->request->get('complex_type_id'),
'developer_id' => Yii::$app->request->get('developer_id'),
]);
},
'districts.complexes.apartments' => function ($query) {
$query->filterWhere([
'amount_room' => Yii::$app->request->get('amount_room'),
'yardage' => Yii::$app->request->get('yardage'),
'level' => Yii::$app->request->get('level'),
'price' => Yii::$app->request->get('price'),
]);
},
])->all();
$query = [];
foreach ($cities as $city) {
foreach ($city->districts as $district) {
foreach ($district->complexes as $complex) {
foreach ($complex->apartments as $apartment) {
$query[] = $apartment;
}
}
}
}
return new ArrayDataProvider([
'allModels' => $query,
]);
}
But it looks kind of crooked, maybe I went the wrong way, and can I do it better?
UPD: I did almost like Yasin Patel.
$cities = Cities::find()
//->select('cities.id') // list your attributes comma saperated
->leftJoin('districts','cities.id=districts.city_id') // join districts table
->leftJoin('complex','districts.id=complex.district_id') // join complex table
->leftJoin('apartment','complex.id=apartment.complex_id') // join apartment table
->where(['cities.id' => Yii::$app->request->get('city_id')])
->andWhere(['cities.id' => Yii::$app->request->get('city_id')])
->filterWhere(['districts.id' => Yii::$app->request->get('district_id')])
->filterWhere(['complex.id' => Yii::$app->request->get('complex_id')])
->filterWhere(['complex.type_id' => Yii::$app->request->get('complex_type_id')])
->filterWhere(['complex.developer_id' => Yii::$app->request->get('developer_id')])
->filterWhere(['apartment.amount_room' => Yii::$app->request->get('amount_room')])
->filterWhere(['apartment.yardage' => Yii::$app->request->get('yardage')])
->filterWhere(['apartment.level' => Yii::$app->request->get('level')])
->filterWhere(['apartment.price' => Yii::$app->request->get('price')]);
return new ActiveDataProvider([
'query' => $cities,
]);
Returns:
[
{
"name":"City1",
"region":{
"id":7,
"name":"Region city1."
}
}
]
But how now to choose all the apartments found?
Try this query.
$cities = Cities::find()
->select('city.id') // list your attributes comma saperated
->leftJoin('districts','city.id=districts.city_id') // join districts table
->leftJoin('complexes','districts.id=complexes.districts_id') // join complexes table
->leftJoin('apartments','complexes.id=apartments.complexe_id') // join apartments table
->where(['id' => Yii::$app->request->get('city_id')])
->andWhere(['city.id' => Yii::$app->request->get('district_id')])
->andWhere(['districts.id' => Yii::$app->request->get('district_id')])
->andWhere(['complexes.id' => Yii::$app->request->get('complex_id')])
->andWhere(['complexes.type_id' => Yii::$app->request->get('complex_type_id')])
->andWhere(['complexes.developer_id' => Yii::$app->request->get('developer_id')])
->andWhere(['apartments.amount_room' => Yii::$app->request->get('amount_room')])
->andWhere(['apartments.yardage' => Yii::$app->request->get('yardage')])
->andWhere(['apartments.level' => Yii::$app->request->get('level')])
->andWhere(['apartments.price' => Yii::$app->request->get('price')])
->asArray();
Use your table name and attributes names as needed.
Than use this query in ArrayDataProvider.
return new ArrayDataProvider([
'allModels' => $query,
]);
This will return you records as array.
It turned out to optimize the code in this way::
public function actionIndex()
{
$query = Apartment::find()
->joinWith('complex')
->joinWith('complex.district')
->joinWith('complex.district.city')
->where(['cities.id' => Yii::$app->request->get('city_id')])
->filterWhere(['districts.id' => Yii::$app->request->get('district_id')])
->filterWhere(['complex.id' => Yii::$app->request->get('complex_id')])
->filterWhere(['complex.type_id' => Yii::$app->request->get('complex_type_id')])
->filterWhere(['complex.developer_id' => Yii::$app->request->get('developer_id')])
->filterWhere(['apartment.amount_room' => Yii::$app->request->get('amount_room')])
->filterWhere(['apartment.yardage' => Yii::$app->request->get('yardage')])
->filterWhere(['apartment.level' => Yii::$app->request->get('level')])
->filterWhere(['apartment.price' => Yii::$app->request->get('price')]);
return new ActiveDataProvider([
'query' => $query,
]);
}
Thanks to everyone for their help.

How do I extract the value from a nested hash with unknown level of nesting?

What is the best way to walk a nested hash structure to get the value when there can be an unknown number of nested levels?
For example the hash could be any of the following or any level of nesting.
my $hash = { 'known' => { 'a' => { 'b' => 'value' } } };
my $hash = { 'known' => { 'a' => { 'b' => { 'c' => 'value' } } } };
my $hash = { 'known' => { 'a' => { 'b' => { 'c' => { 'd' => 'value' } } } } };
The keys a,b,c below could be any value.
I was thinking I could do it with a recursive function that extracts the key and value and the current level, checks to see if the value is a reference to
a hash and if so calls itself, otherwise I have the value?
An easy-to-read approach:
#!/usr/bin/env perl
use strict;
use warnings;
my $hash = { 'known' => { 'a' => { 'b' => 'value' } } };
print get_deep_values($hash);
sub get_deep_values {
my $hash = shift;
if (ref($hash) eq 'HASH') {
get_deep_values( (values %$hash)[0] )
}
else {
return $hash;
}
}
Output:
value
Yes, recursion works,
use v5.16;
use warnings;
my $hash = { 'known' => { 'a' => { 'b' => 'value' } } };
print sub { map { ref() ? __SUB__->(values %$_) : $_ } #_ }->($hash);
output
value
I assume none of the nested hashes have more than a single element?
A simple iteration is all that's necessary
use strict;
use warnings;
use 5.010;
my #hashes = (
{ 'known' => { 'a' => { 'b' => 'value1' } } },
{ 'known' => { 'a' => { 'b' => { 'c' => 'value2' } } } },
{ 'known' => { 'a' => { 'b' => { 'c' => { 'd' => 'value3' } } } } },
);
say get_value($_) for #hashes;
sub get_value {
my ($hash) = #_;
($hash) = values %$hash while ref $hash eq 'HASH';
$hash;
}
output
value1
value2
value3

Create deep hash mapping in perl

Below is my Code with the Hash
#!/usr/bin/perl
use warnings;
use JSON::PP; # Just 'use JSON;' on most systems
my %name = (
'sl' => {
'fsd' => {
'conf' => {
'ul' => '/sl/fsd/conf/ul',
'si' => '/sl/fsd/conf/si',
'ho1' => '/sl/fsd/conf/ho1'
}
}
},
're' => {
'fsd' => {
'cron' => {
'README' => '/re/fsd/cron/README'
},
'bin' => {
'db' => {
'smart.p_add_tag' => '/re/fsd/bin/db/smart.p_add_tag',
'smart.p_tag_partition' => '/re/fsd/bin/db/smart.p_tag_partition',
'smart.p_add_tag_type' => '/re/fsd/bin/db/smart.p_add_tag_type'
}
},
'doc' => {
'SMART' => '/re/fsd/doc/SMART',
'README' => '/re/fsd/doc/README'
},
'data' => {
'README' => '/re/fsd/data/README'
},
'conf' => {
'al1' => '/re/fsd/conf/al1',
'file' => '/re/fsd/conf/file',
'ho' => '/re/fsd/conf/ho',
'al3' => '/re/fsd/conf/al3',
'hst' => '/re/fsd/conf/hst',
'us' => '/re/fsd/conf/us',
'README' => '/re/fsd/conf/README',
'al2' => '/re/fsd/conf/al2'
}
}
}
);
(my $root) = keys %name;
my %nodes = ();
my %tree = ();
my #queue = ($root);
list_children(\%name, \#queue, \%nodes) while #queue;
my $tree = build_tree($root, \%nodes);
my $json = JSON::PP->new->pretty; # prettify for human consumption
print $json->encode($tree);
sub list_children {
my $adjac = shift;
my $queue = shift;
my $nodes = shift;
my $node = shift #$queue;
my #children = keys %{$adjac->{$node}};
#children = grep { ! exists $nodes->{$_}} #children;
$nodes->{$node} = \#children;
push #$queue, #children;
}
sub build_tree {
my $root = shift;
my $nodes = shift;
my #children;
for my $child (#{$nodes->{$root}}) {
push #children, build_tree($child, $nodes);
}
my %h = ('text' => $root,
'children' => \#children);
return \%h;
}
I'm trying to output JSONified hash, but it is only traversing upto two levels. whereas i need it to traverse all upto the last child node of each parent. Can someone please help to achieve this.
Below is current output
{
"text" : "sl",
"children" : [
{
"text" : "fsd",
"children" : []
}
]
}
Normally, transforming the hash, and then json-ing is not the most efficient idea, because you're going to make one traversal to transform the hash and JSON's going to make one to json-ify it, and JSON is a type of transform of a hash.
However, JSON is usually done with XS, which means that the second traversal is faster, at least. That and JSON behavior is standardized.
use 5.016;
use strict;
use warnings;
use Data::Dumper ();
use JSON;
my $hash
= {
'Foods' => {
'fruits' => {
'orange' => '1',
'apple' => '2',
},
'Vegetables' => {
'tomato' => '3',
'carrot' => '1',
'cabbage' => '2',
}
}
};
sub descend {
my ( $structure, $block ) = #_;
my $res;
while ( my ( $k, $v ) = each %$structure ) {
$block->( $structure, $k, $v );
if ( ref( $v ) eq 'HASH' ) {
$res = descend( $v, $block );
}
}
return $res;
}
my $new = {};
my $curr = $new;
descend( $hash => sub {
my ( $lvl, $k, $v ) = #_;
my $node = { text => $k };
$curr->{children} //= [];
push $curr->{children}, $node;
if ( ref( $v ) eq 'HASH' ) {
$curr = $node;
}
else {
$node->{children} = { text => $v };
}
});
# allow for the root-level special case, and retrieve the first child.
$new = $new->{children}[0];
say Data::Dumper->Dump( [ $new ], [ '$new' ] );
say JSON->new->encode( $new );

How to merge HashRef in Moose attribute writer?

Having a simple example code
use Modern::Perl;
use Data::Dumper;
package My;
use Moose;
use Method::Signatures::Simple;
has 'result' => (
is => 'rw',
isa => 'HashRef',
default => sub{{}},
clearer => 'clear_result'
);
method run {
$self->clear_result; #clearing the result
$self->result( $self->run_part1 );
$self->do_something;
$self->result( $self->run_part3 ); #need merge
}
method do_something {
$self->result( $self->run_part2 ); #need merge
}
method run_part1 { return { aaa => 'aaa' } }
method run_part2 { return { bbb => 'bbb' } }
method run_part3 { return { ccc => 'ccc' } }
package main;
my $p = My->new;
say Dumper $p->run;
the result (ofcourse) is:
$VAR1 = {
'ccc' => 'ccc'
};
I want the result:
$VAR1 = {
'aaa' => 'aaa'
'bbb' => 'bbb'
'ccc' => 'ccc'
};
so, the question is - how to merge the $self->result HashRef when setting it?
Yes, i can add new method add_result like:
method add_result($hr) {
use Hash::Merge::Simple qw(merge);
$self->result( merge $self->result, $hr );
}
and change everywhere in my code the $self->result to $self->add_result but wonder if there is another solution...
has 'result' => (
acccessor => '_result',
isa => 'HashRef',
default => sub{{}},
clearer => 'clear_result'
);
sub result {
my $self = shift;
if (#_) {
my ($hr) = #_;
return $self->_result( ... merged hash ...);
} else {
return $self->_result();
}
}